Compare commits

...

60 Commits

Author SHA1 Message Date
Simon Fairbairn dde451ab4e Merge branch 'release/1.2.4' 2022-05-19 08:36:00 +01:00
Simon Fairbairn ae97f0fbea Version Bump 2022-05-19 08:34:47 +01:00
Simon Fairbairn 040d7896fe Updating podspec 2022-05-19 08:34:43 +01:00
Simon Fairbairn 403285e46f Automatic fastlane Readme update 2022-05-19 08:32:25 +01:00
Simon Fairbairn af94dd615e Updates gemfile 2022-05-19 08:29:23 +01:00
Simon Fairbairn 99ded8446b Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2021-04-10 11:30:39 +12:00
Simon Fairbairn 2bbf1213a6 Merge pull request #110 from bensLine/master
Fix bold font size get's overridden by font descriptor
2021-04-10 11:28:53 +12:00
bensLine aa748f2cdd Prefer configured font size over bold/italic descriptor size 2021-03-22 15:22:11 +01:00
Simon Fairbairn 851b547dcd Merge branch 'release/1.2.3' into develop 2020-09-14 09:22:48 +12:00
Simon Fairbairn 5b0a1e7633 Merge branch 'release/1.2.3' 2020-09-14 09:22:43 +12:00
Simon Fairbairn 13955aac92 Updates fastlane readme 2020-09-14 09:21:58 +12:00
Simon Fairbairn 0b40036eda Version Bump 2020-09-14 09:20:57 +12:00
Simon Fairbairn 2e3d59324a Updating podspec 2020-09-14 09:20:54 +12:00
Simon Fairbairn 9d1457a96a Updating Fastfile 2020-09-14 09:19:10 +12:00
Simon Fairbairn eb9742b518 Updating Fastfile 2020-09-14 09:18:56 +12:00
Simon Fairbairn 569daaf779 Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2020-09-14 09:04:03 +12:00
Simon Fairbairn 649d7690e1 Merge pull request #94 from csauvage/hotfix/fix-carthage-build
Modify plist file (attempt to fix #93) Thanks for this!
2020-09-14 09:03:27 +12:00
Clément SAUVAGE 2b8df10a0d Modify plist 2020-09-02 11:42:59 +02:00
Simon Fairbairn c9fcecafc3 Merge branch 'release/1.2.2' into develop 2020-06-19 11:02:44 +12:00
Simon Fairbairn 6c64c16475 Merge branch 'release/1.2.2' 2020-06-19 11:02:41 +12:00
Simon Fairbairn d87627c957 Version Bump 2020-06-19 11:02:30 +12:00
Simon Fairbairn 1ecc196398 Updating podspec 2020-06-19 11:02:28 +12:00
Simon Fairbairn 6103068a26 Fixes issue with macOS support 2020-06-19 11:01:23 +12:00
Simon Fairbairn 429d557c9c Fixes issue with macOS 2020-06-19 11:00:38 +12:00
Simon Fairbairn 2e69439003 Updates Bundle 2020-06-19 10:58:12 +12:00
Simon Fairbairn a93ccedac5 Updating readme 2020-06-19 10:57:11 +12:00
Simon Fairbairn bc81245371 Merge pull request #87 from ramonilho/feature/underline-link-style
Added LinkStyles to customize underline properties
2020-06-19 10:56:39 +12:00
Simon Fairbairn ecebb6d8b8 Merge pull request #89 from bitsfabrik/master
fixed issue where underline style is not going to be reseted
2020-06-19 10:51:40 +12:00
Philip Messlehner 1df35418f2 added support for linespacing and paragraphspacing 2020-06-17 22:06:00 +02:00
Philip Messlehner 5e8c7d03ff fixed issue where underline style is not going to be resetted 2020-06-17 21:36:53 +02:00
Ramon Honório 6ab69d3d82 added underline color 2020-05-26 12:14:01 -03:00
Ramon Honório 9cf9d6bfbe added underline style into link 2020-05-26 12:05:26 -03:00
Simon Fairbairn f4529f5ff9 Merge branch 'release/1.2.1' 2020-04-15 14:14:16 +12:00
Simon Fairbairn 4592cdd4fa Version Bump 2020-04-15 14:13:39 +12:00
Simon Fairbairn 17a2b6b5ff Updating podspec 2020-04-15 14:13:37 +12:00
Simon Fairbairn 20d2827573 Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2020-04-15 14:11:45 +12:00
Simon Fairbairn 98f6beced3 Merge pull request #81 from julo15/fix-swift-build-error
Fix build break: "Cannot capture lastElement before it is declared"
2020-04-15 14:11:16 +12:00
Julian Lo fab59935e2 Fix build break: "Cannot capture lastElement before it is declared" 2020-04-14 11:28:48 -07:00
Simon Fairbairn 54e1f56cad Merge branch 'release/1.2.0' into develop 2020-04-11 16:59:39 +12:00
Simon Fairbairn abc0768701 Merge branch 'release/1.2.0' 2020-04-11 16:59:35 +12:00
Simon Fairbairn 95c1e59344 Version Bump 2020-04-11 16:59:21 +12:00
Simon Fairbairn 4581712c5e Updating podspec 2020-04-11 16:59:19 +12:00
Simon Fairbairn b0e9e36c6b Removes unneeded 2020-04-11 16:57:40 +12:00
Simon Fairbairn 61490b1e13 Cleans up codebase and class names 2020-04-11 16:08:02 +12:00
Simon Fairbairn bcbf3bd38c Merge branch 'feature/referencedLists' into develop 2020-04-11 16:02:31 +12:00
Simon Fairbairn 79467395b5 Updates readme 2020-04-11 16:02:15 +12:00
Simon Fairbairn 4e2f5c234e Fixes more weird character combinations 2020-04-11 15:04:33 +12:00
Simon Fairbairn 898a1ca2ed Fixes #75. Fixes #78. Fixes #79. 2020-04-11 14:51:04 +12:00
Simon Fairbairn ba5d8ce9c6 Marker before third attempt at parser 2020-04-08 14:10:26 +12:00
Simon Fairbairn 68336ab630 Adds new non-repeating system 2020-04-06 14:12:58 +12:00
Simon Fairbairn 368f0da628 Prior to implementing new link system 2020-04-06 10:58:49 +12:00
Simon Fairbairn 72ff31b6ff Adds additional tests 2020-02-04 11:52:04 +13:00
Simon Fairbairn 924d06bcfe Pulls token scanning out of tokeniser and into separate scanning class and adds performance log class 2020-02-04 09:18:21 +13:00
Simon Fairbairn a1aa1a0eac Massively increases performance of complex strings 2020-02-03 17:24:22 +13:00
Simon Fairbairn 71a64dee21 Passes all tests again 2020-02-03 16:34:08 +13:00
Simon Fairbairn 71eb29ada0 Completely overhauls tokenising engine 2020-02-03 14:18:15 +13:00
Simon Fairbairn 711850056d Refactors tests to make customisable rules possible 2020-02-01 15:43:26 +13:00
Simon Fairbairn d2cbd0ce96 Updates readme 2020-02-01 12:50:02 +13:00
Simon Fairbairn 61c6701518 Updates Readme 2020-02-01 12:45:24 +13:00
Simon Fairbairn 8518dfb406 Merge branch 'release/1.1.0' into develop 2020-02-01 12:39:27 +13:00
33 changed files with 3589 additions and 1898 deletions
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
F421DD991C8AF4E900B86D66 /* example.md in Resources */ = {isa = PBXBuildFile; fileRef = F421DD951C8AF34F00B86D66 /* example.md */; };
F4B4A44C23E4E17400550249 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B4A44B23E4E17400550249 /* AppDelegate.swift */; };
F4B4A44E23E4E17400550249 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B4A44D23E4E17400550249 /* ViewController.swift */; };
F4B4A45023E4E17400550249 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4B4A44F23E4E17400550249 /* Assets.xcassets */; };
@@ -20,6 +19,7 @@
F4CE98B61C8AEF7D00D735C1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */; };
F4CE98C11C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98C01C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift */; };
F4CE98CC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98CB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift */; };
F4EAB653244179FE00206782 /* example.md in Resources */ = {isa = PBXBuildFile; fileRef = F4576C2E2437F67B0013E2B6 /* example.md */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -62,7 +62,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
F421DD951C8AF34F00B86D66 /* example.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = "<group>"; };
F4576C2E2437F67B0013E2B6 /* example.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = "<group>"; };
F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftyMarkdownExample macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F4B4A44B23E4E17400550249 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F4B4A44D23E4E17400550249 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -172,7 +172,7 @@
F4CE98B21C8AEF7D00D735C1 /* Assets.xcassets */,
F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */,
F4CE98B71C8AEF7D00D735C1 /* Info.plist */,
F421DD951C8AF34F00B86D66 /* example.md */,
F4576C2E2437F67B0013E2B6 /* example.md */,
);
path = SwiftyMarkdownExample;
sourceTree = "<group>";
@@ -343,8 +343,8 @@
buildActionMask = 2147483647;
files = (
F4CE98B61C8AEF7D00D735C1 /* LaunchScreen.storyboard in Resources */,
F4EAB653244179FE00206782 /* example.md in Resources */,
F4CE98B31C8AEF7D00D735C1 /* Assets.xcassets in Resources */,
F421DD991C8AF4E900B86D66 /* example.md in Resources */,
F4CE98B11C8AEF7D00D735C1 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
+174 -109
View File
@@ -1,106 +1,152 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
activesupport (4.2.11.1)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
CFPropertyList (3.0.5)
rexml
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.1)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
cocoapods (1.8.4)
activesupport (>= 4.0.2, < 5)
aws-eventstream (1.2.0)
aws-partitions (1.588.0)
aws-sdk-core (3.131.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.8.4)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.6.6)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.11.1, < 2.0)
cocoapods-core (1.8.4)
activesupport (>= 4.0.2, < 6)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.1.0)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
concurrent-ruby (1.1.5)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.10)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
dotenv (2.7.6)
emoji_regex (3.2.3)
escape (0.0.4)
excon (0.72.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.141.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (~> 2.1.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
@@ -109,104 +155,123 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-api-client (0.36.4)
google-apis-androidpublisher_v3 (0.21.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.5.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.14.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
googleauth (1.1.3)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
highline (1.7.10)
http-cookie (1.0.3)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (0.9.5)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.3.0)
jwt (2.1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
minitest (5.14.0)
molinillo (0.6.6)
multi_json (1.14.1)
multi_xml (0.6.0)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.15.0)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.0)
naturally (2.2.1)
netrc (0.11.0)
os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (1.4.0)
rubyzip (1.3.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.7)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.7.0)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
tzinfo (1.2.6)
thread_safe (~> 0.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.14.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -216,4 +281,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.1.4
2.3.8
@@ -0,0 +1,285 @@
//: [Previous](@previous)
import Foundation
extension String {
func repeating( _ max : Int ) -> String {
var output = self
for _ in 1..<max {
output += self
}
return output
}
}
enum TagState {
case none
case open
case intermediate
case closed
}
struct TagString {
var state : TagState = .none
var preOpenString = ""
var openTagString = ""
var intermediateString = ""
var intermediateTagString = ""
var metadataString = ""
var closedTagString = ""
var postClosedString = ""
let rule : Rule
init( with rule : Rule ) {
self.rule = rule
}
mutating func append( _ string : String? ) {
guard let existentString = string else {
return
}
switch self.state {
case .none:
self.preOpenString += existentString
case .open:
self.intermediateString += existentString
case .intermediate:
self.metadataString += existentString
case .closed:
self.postClosedString += existentString
}
}
mutating func append( contentsOf tokenGroup: [TokenGroup] ) {
print(tokenGroup)
for token in tokenGroup {
switch token.state {
case .none:
self.append(token.string)
case .open:
if self.state != .none {
self.preOpenString += token.string
} else {
self.openTagString += token.string
}
case .intermediate:
if self.state != .open {
self.intermediateString += token.string
} else {
self.intermediateTagString += token.string
}
case .closed:
if self.rule.intermediateTag != nil && self.state != .intermediate {
self.metadataString += token.string
} else {
self.closedTagString += token.string
}
}
self.state = token.state
}
}
mutating func tokens() -> [Token] {
print(self)
var tokens : [Token] = []
if !self.preOpenString.isEmpty {
tokens.append(Token(type: .string, inputString: self.preOpenString))
}
if !self.openTagString.isEmpty {
tokens.append(Token(type: .openTag, inputString: self.openTagString))
}
if !self.intermediateString.isEmpty {
var token = Token(type: .string, inputString: self.intermediateString)
token.metadataString = self.metadataString
tokens.append(token)
}
if !self.intermediateTagString.isEmpty {
tokens.append(Token(type: .intermediateTag, inputString: self.intermediateTagString))
}
if !self.metadataString.isEmpty {
tokens.append(Token(type: .metadata, inputString: self.metadataString))
}
if !self.closedTagString.isEmpty {
tokens.append(Token(type: .closeTag, inputString: self.closedTagString))
}
self.preOpenString = ""
self.openTagString = ""
self.intermediateString = ""
self.intermediateTagString = ""
self.metadataString = ""
self.closedTagString = ""
self.postClosedString = ""
self.state = .none
return tokens
}
}
struct TokenGroup {
enum TokenGroupType {
case string
case tag
case escape
}
let string : String
let isEscaped : Bool
let type : TokenGroupType
var state : TagState = .none
}
func getTokenGroups( for string : inout String, with rule : Rule, shouldEmpty : Bool = false ) -> [TokenGroup] {
if string.isEmpty {
return []
}
let maxCount = rule.openTag.count * rule.maxTags
var groups : [TokenGroup] = []
let maxTag = rule.openTag.repeating(rule.maxTags)
if maxTag.contains(string) {
if string.count == maxCount || shouldEmpty {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .open
groups.append(token)
string.removeAll()
}
} else if string == rule.intermediateTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .intermediate
groups.append(token)
string.removeAll()
} else if string == rule.closingTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .closed
groups.append(token)
string.removeAll()
}
if shouldEmpty && !string.isEmpty {
let token = TokenGroup(string: string, isEscaped: false, type: .tag)
groups.append(token)
string.removeAll()
}
return groups
}
func scan( _ string : String, with rule : Rule) -> [Token] {
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
var tokens : [Token] = []
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
if let existentEscape = rule.escapeCharacter {
set.insert(charactersIn: String(existentEscape))
}
var openTag = rule.openTag.repeating(rule.maxTags)
var tagString = TagString(with: rule)
var openTagFound : TagState = .none
var regularCharacters = ""
var tagGroupCount = 0
while !scanner.isAtEnd {
tagGroupCount += 1
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let start = scanner.scanUpToCharacters(from: set) {
tagString.append(start)
}
} else {
var string : NSString?
scanner.scanUpToCharacters(from: set, into: &string)
if let existentString = string as String? {
tagString.append(existentString)
}
}
// The end of the string
let maybeFoundChars = scanner.scanCharacters(from: set )
guard let foundTag = maybeFoundChars else {
continue
}
if foundTag == rule.openTag && foundTag.count < rule.minTags {
tagString.append(foundTag)
continue
}
//:--
print(foundTag)
var tokenGroups : [TokenGroup] = []
var escapeCharacter : Character? = nil
var cumulatedString = ""
for char in foundTag {
if let existentEscapeCharacter = escapeCharacter {
// If any of the tags feature the current character
let escape = String(existentEscapeCharacter)
let nextTagCharacter = String(char)
if rule.openTag.contains(nextTagCharacter) || rule.intermediateTag?.contains(nextTagCharacter) ?? false || rule.closingTag?.contains(nextTagCharacter) ?? false {
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: true, type: .tag))
escapeCharacter = nil
} else if nextTagCharacter == escape {
// Doesn't apply to this rule
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: false, type: .escape))
}
continue
}
if let existentEscape = rule.escapeCharacter {
if char == existentEscape {
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
escapeCharacter = char
continue
}
}
cumulatedString.append(char)
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule))
}
if let remainingEscape = escapeCharacter {
tokenGroups.append(TokenGroup(string: String(remainingEscape), isEscaped: false, type: .escape))
}
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
tagString.append(contentsOf: tokenGroups)
if tagString.state == .closed {
tokens.append(contentsOf: tagString.tokens())
}
}
tokens.append(contentsOf: tagString.tokens())
return tokens
}
//: [Next](@next)
var string = "[]([[\\[Some Link]\\]](\\(\\(\\url) [Regular link](url)"
//string = "Text before [Regular link](url) Text after"
var output = "[]([[Some Link]] Regular link"
var tokens = scan(string, with: LinkRule())
print( tokens.filter( { $0.type == .string }).map({ $0.outputString }).joined())
//print( tokens )
//string = "**\\*\\Bold\\*\\***"
//output = "*\\Bold**"
//tokens = scan(string, with: AsteriskRule())
//print( tokens )
@@ -0,0 +1,32 @@
import Foundation
public protocol Rule {
var escapeCharacter : Character? { get }
var openTag : String { get }
var intermediateTag : String? { get }
var closingTag : String? { get }
var maxTags : Int { get }
var minTags : Int { get }
}
public struct LinkRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "["
public let intermediateTag : String? = "]("
public let closingTag : String? = ")"
public let maxTags : Int = 1
public let minTags : Int = 1
public init() { }
}
public struct AsteriskRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "*"
public let intermediateTag : String? = nil
public let closingTag : String? = nil
public let maxTags : Int = 3
public let minTags : Int = 1
public init() { }
}
@@ -74,6 +74,7 @@ public struct Token {
public let inputString : String
public var metadataString : String? = nil
public var characterStyles : [CharacterStyling] = []
public var group : Int = 0
public var count : Int = 0
public var shouldSkip : Bool = false
public var outputString : String {
@@ -5,5 +5,7 @@
<page name='Line Processing'/>
<page name='Tokenising'/>
<page name='Attributed String'/>
<page name='SKLabelNode'/>
<page name='Groups'/>
</pages>
</playground>
+53 -10
View File
@@ -92,6 +92,12 @@ label.attributedText = md.attributedString()
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
[Referenced Links][1]
![Referenced Images][2]
[1]: http://voyagetravelapps.com/
[2]: <Name of asset in bundle>
> Blockquotes
- Bulleted
@@ -140,7 +146,7 @@ On iOS, Specified font sizes will be adjusted relative to the the user's dynamic
![Screenshot](https://cl.ly/779e6964257a/swiftymarkdown-2020.png)
There's an example project included in the repository. Open the `.xcworkspace` file to get started.
There's an example project included in the repository. Open the `Example/SwiftyMarkdown.xcodeproj` file to get started.
## Front Matter
@@ -229,9 +235,12 @@ underlineLinks : Bool
bullet : String
```
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look.
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look. For example, perhaps you want blockquotes to default to having the italic style:
If you like a bit of chaos:
```swift
md.blockquotes.fontStyle = .italic
```
Or, if you like a bit of chaos:
```swift
md.bold.fontStyle = .italic
@@ -289,13 +298,46 @@ enum CharacterStyle : CharacterStyling {
}
static public var characterRules = [
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingTags: true, balancedTags: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
These Character Rules are defined by SwiftyMarkdown:
public struct CharacterRule : CustomStringConvertible {
public let primaryTag : CharacterRuleTag
public let tags : [CharacterRuleTag]
public let escapeCharacters : [Character]
public let styles : [Int : CharacterStyling]
public let minTags : Int
public let maxTags : Int
public var metadataLookup : Bool = false
public var definesBoundary = false
public var shouldCancelRemainingRules = false
public var balancedTags = false
}
1. `primaryTag`: Each rule must have at least one tag and it can be one of `repeating`, `open`, `close`, `metadataOpen`, or `metadataClose`. `repeating` tags are tags that have identical open and close characters (and often have more than 1 style depending on how many are in a group). For example, the `*` tag used in Markdown.
2. `tags`: An array of other tags that the rule can look for. This is where you would put the `close` tag for a custom rule, for example.
3. `escapeCharacters`: The characters that appear prior to any of the tag characters that tell the scanner to ignore the tag.
4. `styles`: The styles that should be applied to every character between the opening and closing tags.
5. `minTags`: The minimum number of repeating characters to be considered a successful match. For example, setting the `primaryTag` to `*` and the `minTag` to 2 would mean that `**foo**` would be a successful match wheras `*bar*` would not.
6. `maxTags`: The maximum number of repeating characters to be considered a successful match.
7. `metadataLookup`: Used for Markdown reference links. Tells the scanner to try to look up the metadata from this dictionary, rather than from the inline result.
8. `definesBoundary`: In order for open and close tags to be successful, the `boundaryCount` for a given location in the string needs to be the same. Setting this property to `true` means that this rule will increase the `boundaryCount` for every character between its opening and closing tags. For example, the `[` rule defines a boundary. After it is applied, the string `*foo[bar*]` becomes `*foobar*` with a boundaryCount `00001111`. Applying the `*` rule results in the output `*foobar*` because the opening `*` tag and the closing `*` tag now have different `boundaryCount` values. It's basically a way to fix the `**[should not be bold**](url)` problem in Markdown.
9. `shouldCancelRemainingTags`: A successful match will mark every character between the opening and closing tags as complete, thereby preventing any further rules from being applied to those characters.
10. `balancedTags`: This flag requires that the opening and closing tags be of exactly equal length. E.g. If this is set to true, `**foo*` would result in `**foo*`. If it was false, the output would be `*foo`.
#### Rule Subsets
If you want to only support a small subset of Markdown, it's now easy to do.
@@ -305,8 +347,8 @@ This example would only process strings with `*` and `_` characters, ignoring li
SwiftyMarkdown.lineRules = []
SwiftyMarkdown.characterRules = [
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
@@ -327,7 +369,7 @@ enum Characters : CharacterStyling {
}
let characterRules = [
CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
CharacterRule(primaryTag: CharacterRuleTag(tag: "%", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.elf])
]
let processor = SwiftyTokeniser( with : characterRules )
@@ -358,3 +400,4 @@ label.preferredMaxLayoutWidth = 500
label.numberOfLines = 0
label.attributedText = smd.attributedString()
```
+107 -2
View File
@@ -20,5 +20,110 @@ It also uses the system color `.label` as the default font color on iOS 13 and a
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
1. A List
1. A second item in the list
*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
1. Neat!
# SwiftyMarkdown 1.0
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
## Fully Rebuilt For 2020!
SwiftyMarkdown now features a more robust and reliable rules-based line processing and tokenisation engine. It has added support for images stored in the bundle (`![Image](<Name In bundle>)`), codeblocks, blockquotes, and unordered lists!
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be underlined by setting underlineLinks to `true`.
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
## Installation
### CocoaPods:
`pod 'SwiftyMarkdown'`
### SPM:
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
1. Neat!
+108
View File
@@ -0,0 +1,108 @@
//
// CharacterRule.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
public enum SpaceAllowed {
case no
case bothSides
case oneSide
case leadingSide
case trailingSide
}
public enum Cancel {
case none
case allRemaining
case currentSet
}
public enum CharacterRuleTagType {
case open
case close
case metadataOpen
case metadataClose
case repeating
}
public struct CharacterRuleTag {
let tag : String
let type : CharacterRuleTagType
public init( tag : String, type : CharacterRuleTagType ) {
self.tag = tag
self.type = type
}
}
public struct CharacterRule : CustomStringConvertible {
public let primaryTag : CharacterRuleTag
public let tags : [CharacterRuleTag]
public let escapeCharacters : [Character]
public let styles : [Int : CharacterStyling]
public let minTags : Int
public let maxTags : Int
public var metadataLookup : Bool = false
public var isRepeatingTag : Bool {
return self.primaryTag.type == .repeating
}
public var definesBoundary = false
public var shouldCancelRemainingRules = false
public var balancedTags = false
public var description: String {
return "Character Rule with Open tag: \(self.primaryTag.tag) and current styles : \(self.styles) "
}
public func tag( for type : CharacterRuleTagType ) -> CharacterRuleTag? {
return self.tags.filter({ $0.type == type }).first ?? nil
}
public init(primaryTag: CharacterRuleTag, otherTags: [CharacterRuleTag], escapeCharacters : [Character] = ["\\"], styles: [Int : CharacterStyling] = [:], minTags : Int = 1, maxTags : Int = 1, metadataLookup : Bool = false, definesBoundary : Bool = false, shouldCancelRemainingRules : Bool = false, balancedTags : Bool = false) {
self.primaryTag = primaryTag
self.tags = otherTags
self.escapeCharacters = escapeCharacters
self.styles = styles
self.metadataLookup = metadataLookup
self.definesBoundary = definesBoundary
self.shouldCancelRemainingRules = shouldCancelRemainingRules
self.minTags = maxTags < minTags ? maxTags : minTags
self.maxTags = minTags > maxTags ? minTags : maxTags
self.balancedTags = balancedTags
}
}
enum ElementType {
case tag
case escape
case string
case space
case newline
case metadata
}
struct Element {
let character : Character
var type : ElementType
var boundaryCount : Int = 0
var isComplete : Bool = false
var styles : [CharacterStyling] = []
var metadata : [String] = []
}
extension CharacterSet {
func containsUnicodeScalars(of character: Character) -> Bool {
return character.unicodeScalars.allSatisfy(contains(_:))
}
}
@@ -0,0 +1,44 @@
//
// PerfomanceLog.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
import os.log
class PerformanceLog {
var timer : TimeInterval = 0
let enablePerfomanceLog : Bool
let log : OSLog
let identifier : String
init( with environmentVariableName : String, identifier : String, log : OSLog ) {
self.log = log
self.enablePerfomanceLog = (ProcessInfo.processInfo.environment[environmentVariableName] != nil)
self.identifier = identifier
}
func start() {
guard enablePerfomanceLog else { return }
self.timer = Date().timeIntervalSinceReferenceDate
os_log("--- TIMER %{public}@ began", log: self.log, type: .info, self.identifier)
}
func tag( with string : String) {
guard enablePerfomanceLog else { return }
if timer == 0 {
self.start()
}
os_log("TIMER %{public}@: %f %@", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer, string)
}
func end() {
guard enablePerfomanceLog else { return }
self.timer = Date().timeIntervalSinceReferenceDate
os_log("--- TIMER %{public}@ finished. Total time: %f", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer)
self.timer = 0
}
}
@@ -7,6 +7,12 @@
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyLineProcessor"
static let swiftyLineProcessorPerformance = OSLog(subsystem: subsystem, category: "Swifty Line Processor Performance")
}
public protocol LineStyling {
var shouldTokeniseLine : Bool { get }
@@ -73,7 +79,9 @@ public class SwiftyLineProcessor {
let lineRules : [LineRule]
let frontMatterRules : [FrontMatterRule]
let perfomanceLog = PerformanceLog(with: "SwiftyLineProcessorPerformanceLogging", identifier: "Line Processor", log: OSLog.swiftyLineProcessorPerformance)
public init( rules : [LineRule], defaultRule: LineStyling, frontMatterRules : [FrontMatterRule] = []) {
self.lineRules = rules
self.defaultType = defaultRule
@@ -117,6 +125,10 @@ public class SwiftyLineProcessor {
return nil
}
if !text.contains(element.token) {
continue
}
switch element.removeFrom {
case .leading:
output = findLeadingLineElement(element, in: output)
@@ -199,9 +211,15 @@ public class SwiftyLineProcessor {
public func process( _ string : String ) -> [SwiftyLine] {
var foundAttributes : [SwiftyLine] = []
self.perfomanceLog.start()
var lines = string.components(separatedBy: CharacterSet.newlines)
lines = self.processFrontMatter(lines)
self.perfomanceLog.tag(with: "(Front matter completed)")
for heading in lines {
if processEmptyStrings == nil && heading.isEmpty {
@@ -220,6 +238,8 @@ public class SwiftyLineProcessor {
continue
}
foundAttributes.append(input)
self.perfomanceLog.tag(with: "(line completed: \(heading)")
}
return foundAttributes
}
@@ -130,10 +130,10 @@ extension SwiftyMarkdown {
}
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
font = UIFont(descriptor: italicDescriptor, size: 0)
font = UIFont(descriptor: italicDescriptor, size: fontSize ?? 0)
}
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: 0)
font = UIFont(descriptor: boldDescriptor, size: fontSize ?? 0)
}
return font
@@ -165,7 +165,8 @@ extension SwiftyMarkdown {
return blockquotes.color
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
return body.color
case .referencedLink:
return link.color
}
}
@@ -137,6 +137,8 @@ extension SwiftyMarkdown {
return body.color
case .yaml:
return body.color
case .referencedLink:
return body.color
}
}
+115 -39
View File
@@ -5,23 +5,30 @@
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import os.log
#if os(macOS)
import AppKit
#else
import UIKit
#endif
enum CharacterStyle : CharacterStyling {
extension OSLog {
private static var subsystem = "SwiftyMarkdown"
static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
}
public enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
case referencedLink
case referencedImage
case strikethrough
func isEqualTo(_ other: CharacterStyling) -> Bool {
public func isEqualTo(_ other: CharacterStyling) -> Bool {
guard let other = other as? CharacterStyle else {
return false
}
@@ -57,7 +64,7 @@ enum MarkdownLineStyle : LineStyling {
case orderedList
case orderedListIndentFirstOrder
case orderedListIndentSecondOrder
case referencedLink
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
@@ -97,6 +104,8 @@ enum MarkdownLineStyle : LineStyling {
@objc public protocol LineProperties {
var alignment : NSTextAlignment { get set }
var lineSpacing: CGFloat { get set }
var paragraphSpacing: CGFloat { get set }
}
@@ -126,14 +135,27 @@ If that is not set, then the system default will be used.
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
public var alignment: NSTextAlignment = .left
public var lineSpacing : CGFloat = 0.0
public var paragraphSpacing : CGFloat = 0.0
}
@objc open class LinkStyles : BasicStyles {
public var underlineStyle: NSUnderlineStyle = .single
#if os(macOS)
public lazy var underlineColor = self.color
#else
public lazy var underlineColor = self.color
#endif
}
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
@objc open class SwiftyMarkdown: NSObject {
static public var frontMatterRules = [
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
]
static public var lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
@@ -157,16 +179,30 @@ If that is not set, then the system default will be used.
]
static public var characterRules = [
CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2),
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
]
static public var frontMatterRules = [
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.image], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "(", type: .metadataOpen),
CharacterRuleTag(tag: ")", type: .metadataClose)
], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "(", type: .metadataOpen),
CharacterRuleTag(tag: ")", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingRules: true, balancedTags: true),
CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
@@ -197,7 +233,7 @@ If that is not set, then the system default will be used.
open var blockquotes = LineStyles()
/// The styles to apply to any links found in the Markdown
open var link = BasicStyles()
open var link = LinkStyles()
/// The styles to apply to any bold text found in the Markdown
open var bold = BasicStyles()
@@ -222,17 +258,18 @@ If that is not set, then the system default will be used.
var currentType : MarkdownLineStyle = .body
var string : String
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
var orderedListCount = 0
var orderedListIndentFirstOrderCount = 0
var orderedListIndentSecondOrderCount = 0
var previouslyFoundTokens : [Token] = []
var applyAttachments = true
let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance)
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
@@ -347,28 +384,58 @@ If that is not set, then the system default will be used.
}
/**
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
- returns: An NSAttributedString with the styles applied
*/
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
self.previouslyFoundTokens.removeAll()
self.perfomanceLog.start()
if let existentMarkdownString = markdownString {
self.string = existentMarkdownString
}
let attributedString = NSMutableAttributedString(string: "")
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
for (idx, line) in foundAttributes.enumerated() {
let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") })
let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) })
var keyValuePairs : [String : String] = [:]
for line in references {
let strings = line.line.components(separatedBy: "]:")
guard strings.count >= 2 else {
continue
}
var key : String = strings[0]
if !key.isEmpty {
let newstart = key.index(key.startIndex, offsetBy: 1)
let range : Range<String.Index> = newstart..<key.endIndex
key = String(key[range]).trimmingCharacters(in: .whitespacesAndNewlines)
}
keyValuePairs[key] = strings[1].trimmingCharacters(in: .whitespacesAndNewlines)
}
self.perfomanceLog.tag(with: "(line processing complete)")
self.tokeniser.metadataLookup = keyValuePairs
for (idx, line) in referencesRemoved.enumerated() {
if idx > 0 {
attributedString.append(NSAttributedString(string: "\n"))
}
let finalTokens = self.tokeniser.process(line.line)
self.previouslyFoundTokens.append(contentsOf: finalTokens)
self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)")
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
}
self.perfomanceLog.end()
return attributedString
}
@@ -471,13 +538,17 @@ extension SwiftyMarkdown {
lineProperties = body
case .body:
lineProperties = body
case .referencedLink:
lineProperties = body
}
let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
if lineProperties.alignment != .left {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = lineProperties.alignment
attributes[.paragraphStyle] = paragraphStyle
}
paragraphStyle.lineSpacing = lineProperties.lineSpacing
paragraphStyle.paragraphSpacing = lineProperties.paragraphSpacing
attributes[.paragraphStyle] = paragraphStyle
for token in finalTokens {
@@ -485,6 +556,7 @@ extension SwiftyMarkdown {
attributes[.link] = nil
attributes[.strikethroughStyle] = nil
attributes[.foregroundColor] = self.color(for: line)
attributes[.underlineStyle] = nil
guard let styles = token.characterStyles as? [CharacterStyle] else {
continue
}
@@ -497,16 +569,17 @@ extension SwiftyMarkdown {
attributes[.foregroundColor] = self.bold.color
}
if styles.contains(.link), let url = token.metadataString {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = url as AnyObject
if underlineLinks {
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
}
}
if let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = token.metadataStrings[linkIdx] as AnyObject
if underlineLinks {
attributes[.underlineStyle] = self.link.underlineStyle.rawValue as AnyObject
attributes[.underlineColor] = self.link.underlineColor
}
}
if styles.contains(.strikethrough) {
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
@@ -514,15 +587,18 @@ extension SwiftyMarkdown {
}
#if !os(watchOS)
if styles.contains(.image), let imageName = token.metadataString {
if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count {
if !self.applyAttachments {
continue
}
#if !os(macOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx])
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#elseif !os(watchOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = NSImage(named: imageName)
image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx])
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#endif
+564
View File
@@ -0,0 +1,564 @@
//
// SwiftyScanner.swift
//
//
// Created by Simon Fairbairn on 04/04/2020.
//
//
// SwiftyScanner.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyScanner"
static let swiftyScanner = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner")
static let swiftyScannerPerformance = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner Peformance")
}
enum RepeatingTagType {
case open
case either
case close
case neither
}
struct TagGroup {
let groupID = UUID().uuidString
var tagRanges : [ClosedRange<Int>]
var tagType : RepeatingTagType = .open
var count = 1
}
class SwiftyScanner {
var elements : [Element]
let rule : CharacterRule
let metadata : [String : String]
var pointer : Int = 0
var spaceAndNewLine = CharacterSet.whitespacesAndNewlines
var tagGroups : [TagGroup] = []
var isMetadataOpen = false
var enableLog = (ProcessInfo.processInfo.environment["SwiftyScannerScanner"] != nil)
let currentPerfomanceLog = PerformanceLog(with: "SwiftyScannerScannerPerformanceLogging", identifier: "Scanner", log: OSLog.swiftyScannerPerformance)
let log = PerformanceLog(with: "SwiftyScannerScanner", identifier: "Scanner", log: OSLog.swiftyScanner)
enum Position {
case forward(Int)
case backward(Int)
}
init( withElements elements : [Element], rule : CharacterRule, metadata : [String : String]) {
self.elements = elements
self.rule = rule
self.currentPerfomanceLog.start()
self.metadata = metadata
}
func elementsBetweenCurrentPosition( and newPosition : Position ) -> [Element]? {
let newIdx : Int
var isForward = true
switch newPosition {
case .backward(let positions):
isForward = false
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
let range : ClosedRange<Int> = ( isForward ) ? self.pointer...newIdx : newIdx...self.pointer
return Array(self.elements[range])
}
func element( for position : Position ) -> Element? {
let newIdx : Int
switch position {
case .backward(let positions):
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
return self.elements[newIdx]
}
func positionIsEqualTo( character : Character, direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return validElement.character == character
}
func positionContains( characters : [Character], direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return characters.contains(validElement.character)
}
func isEscaped() -> Bool {
let isEscaped = self.positionContains(characters: self.rule.escapeCharacters, direction: .backward(1))
if isEscaped {
self.elements[self.pointer - 1].type = .escape
}
return isEscaped
}
func range( for tag : String? ) -> ClosedRange<Int>? {
guard let tag = tag else {
return nil
}
guard let openChar = tag.first else {
return nil
}
if self.pointer == self.elements.count {
return nil
}
if self.elements[self.pointer].character != openChar {
return nil
}
if isEscaped() {
return nil
}
let range : ClosedRange<Int>
if tag.count > 1 {
guard let elements = self.elementsBetweenCurrentPosition(and: .forward(tag.count - 1) ) else {
return nil
}
// If it's already a tag, then it should be ignored
if elements.filter({ $0.type != .string }).count > 0 {
return nil
}
if elements.map( { String($0.character) }).joined() != tag {
return nil
}
let endIdx = (self.pointer + tag.count - 1)
for i in self.pointer...endIdx {
self.elements[i].type = .tag
}
range = self.pointer...endIdx
self.pointer += tag.count
} else {
// If it's already a tag, then it should be ignored
if self.elements[self.pointer].type != .string {
return nil
}
self.elements[self.pointer].type = .tag
range = self.pointer...self.pointer
self.pointer += 1
}
return range
}
func resetTagGroup( withID id : String ) {
if let idx = self.tagGroups.firstIndex(where: { $0.groupID == id }) {
for range in self.tagGroups[idx].tagRanges {
self.resetTag(in: range)
}
self.tagGroups.remove(at: idx)
}
self.isMetadataOpen = false
}
func resetTag( in range : ClosedRange<Int>) {
for idx in range {
self.elements[idx].type = .string
}
}
func resetLastTag( for range : inout [ClosedRange<Int>]) {
guard let last = range.last else {
return
}
for idx in last {
self.elements[idx].type = .string
}
}
func closeTag( _ tag : String, withGroupID id : String ) {
guard let tagIdx = self.tagGroups.firstIndex(where: { $0.groupID == id }) else {
return
}
var metadataString = ""
if self.isMetadataOpen {
let metadataCloseRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let metadataOpenRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if metadataOpenRange.upperBound + 1 == (metadataCloseRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
for idx in (metadataOpenRange.upperBound)...(metadataCloseRange.lowerBound) {
self.elements[idx].type = .metadata
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
}
let key = self.elements[metadataOpenRange.upperBound + 1..<metadataCloseRange.lowerBound].map( { String( $0.character )}).joined()
if self.rule.metadataLookup {
metadataString = self.metadata[key] ?? ""
} else {
metadataString = key
}
}
}
let closeRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let openRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if self.rule.balancedTags && closeRange.count != openRange.count {
self.tagGroups[tagIdx].tagRanges.append(openRange)
self.tagGroups[tagIdx].tagRanges.append(closeRange)
return
}
var shouldRemove = true
var styles : [CharacterStyling] = []
if openRange.upperBound + 1 == (closeRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
var remainingTags = min(openRange.upperBound - openRange.lowerBound, closeRange.upperBound - closeRange.lowerBound) + 1
while remainingTags > 0 {
if remainingTags >= self.rule.maxTags {
remainingTags -= self.rule.maxTags
if let style = self.rule.styles[ self.rule.maxTags ] {
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
if let style = self.rule.styles[remainingTags] {
remainingTags -= remainingTags
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
for idx in (openRange.upperBound)...(closeRange.lowerBound) {
self.elements[idx].styles.append(contentsOf: styles)
self.elements[idx].metadata.append(metadataString)
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
if self.rule.shouldCancelRemainingRules {
self.elements[idx].boundaryCount = 1000
}
}
if self.rule.isRepeatingTag {
let difference = ( openRange.upperBound - openRange.lowerBound ) - (closeRange.upperBound - closeRange.lowerBound)
switch difference {
case 1...:
shouldRemove = false
self.tagGroups[tagIdx].count = difference
self.tagGroups[tagIdx].tagRanges.append( openRange.upperBound - (abs(difference) - 1)...openRange.upperBound )
case ...(-1):
for idx in closeRange.upperBound - (abs(difference) - 1)...closeRange.upperBound {
self.elements[idx].type = .string
}
default:
break
}
}
}
if shouldRemove {
self.tagGroups.removeAll(where: { $0.groupID == id })
}
self.isMetadataOpen = false
}
func emptyRanges( _ ranges : inout [ClosedRange<Int>] ) {
while !ranges.isEmpty {
self.resetLastTag(for: &ranges)
ranges.removeLast()
}
}
func scanNonRepeatingTags() {
var groupID = ""
let closeTag = self.rule.tag(for: .close)?.tag
let metadataOpen = self.rule.tag(for: .metadataOpen)?.tag
let metadataClose = self.rule.tag(for: .metadataClose)?.tag
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if let range = self.range(for: metadataClose) {
if self.isMetadataOpen {
guard let groupIdx = self.tagGroups.firstIndex(where: { $0.groupID == groupID }) else {
self.pointer += 1
continue
}
guard !self.tagGroups.isEmpty else {
self.resetTagGroup(withID: groupID)
continue
}
guard self.isMetadataOpen else {
self.resetTagGroup(withID: groupID)
continue
}
if self.enableLog {
os_log("Closing metadata tag found. Closing tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.tagGroups[groupIdx].tagRanges.append(range)
self.closeTag(closeTag!, withGroupID: groupID)
self.isMetadataOpen = false
continue
} else {
self.resetTag(in: range)
self.pointer -= metadataClose!.count
}
}
if let openRange = self.range(for: self.rule.primaryTag.tag) {
if self.isMetadataOpen {
self.resetTagGroup(withID: groupID)
}
let tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
if self.enableLog {
os_log("New open tag found. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
if self.rule.isRepeatingTag {
}
self.tagGroups.append(tagGroup)
continue
}
if let range = self.range(for: closeTag) {
guard !self.tagGroups.isEmpty else {
if self.enableLog {
os_log("No open tags exist, resetting this close tag", log: OSLog.swiftyScanner, type:.info)
}
self.resetTag(in: range)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
groupID = self.tagGroups[self.tagGroups.count - 1].groupID
if self.enableLog {
os_log("New close tag found. Appending to group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
guard metadataOpen != nil else {
if self.enableLog {
os_log("No metadata tags exist, closing valid tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.closeTag(closeTag!, withGroupID: groupID)
continue
}
guard self.pointer != self.elements.count else {
continue
}
guard let range = self.range(for: metadataOpen) else {
if self.enableLog {
os_log("No metadata tag found, resetting group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTagGroup(withID: groupID)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
self.isMetadataOpen = true
continue
}
if let range = self.range(for: metadataOpen) {
if self.enableLog {
os_log("Multiple open metadata tags found!", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTag(in: range)
self.resetTagGroup(withID: groupID)
self.isMetadataOpen = false
continue
}
self.pointer += 1
}
}
func scanRepeatingTags() {
var groupID = ""
let escapeCharacters = "" //self.rule.escapeCharacters.map( { String( $0 ) }).joined()
let unionSet = spaceAndNewLine.union(CharacterSet(charactersIn: escapeCharacters))
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if var openRange = self.range(for: self.rule.primaryTag.tag) {
if self.elements[openRange].first?.boundaryCount == 1000 {
self.resetTag(in: openRange)
continue
}
var count = 1
var tagType : RepeatingTagType = .open
if let prevElement = self.element(for: .backward(self.rule.primaryTag.tag.count + 1)) {
if !unionSet.containsUnicodeScalars(of: prevElement.character) {
tagType = .either
}
} else {
tagType = .open
}
while let nextRange = self.range(for: self.rule.primaryTag.tag) {
count += 1
openRange = openRange.lowerBound...nextRange.upperBound
}
if self.rule.minTags > 1 {
if (openRange.upperBound - openRange.lowerBound) + 1 < self.rule.minTags {
self.resetTag(in: openRange)
os_log("Tag does not meet minimum length", log: .swiftyScanner, type: .info)
continue
}
}
var validTagGroup = true
if let nextElement = self.element(for: .forward(0)) {
if unionSet.containsUnicodeScalars(of: nextElement.character) {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
} else {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
if !validTagGroup {
if self.enableLog {
os_log("Tag has whitespace on both sides", log: .swiftyScanner, type: .info)
}
self.resetTag(in: openRange)
continue
}
if let idx = tagGroups.firstIndex(where: { $0.groupID == groupID }) {
if tagType == .either {
if tagGroups[idx].count == count {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
if let last = self.tagGroups.last {
groupID = last.groupID
}
continue
}
} else {
if let prevRange = tagGroups[idx].tagRanges.first {
if self.elements[prevRange].first?.boundaryCount == self.elements[openRange].first?.boundaryCount {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
}
}
continue
}
}
var tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
tagGroup.tagType = tagType
tagGroup.count = count
if self.enableLog {
os_log("New open tag found with characters %@. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info, self.elements[openRange].map( { String($0.character) }).joined(), groupID)
}
self.tagGroups.append(tagGroup)
continue
}
self.pointer += 1
}
}
func scan() -> [Element] {
guard self.elements.filter({ $0.type == .string }).map({ String($0.character) }).joined().contains(self.rule.primaryTag.tag) else {
return self.elements
}
self.currentPerfomanceLog.tag(with: "Beginning \(self.rule.primaryTag.tag)")
if self.enableLog {
os_log("RULE: %@", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
if self.rule.isRepeatingTag {
self.scanRepeatingTags()
} else {
self.scanNonRepeatingTags()
}
for tagGroup in self.tagGroups {
self.resetTagGroup(withID: tagGroup.groupID)
}
if self.enableLog {
for element in self.elements {
print(element)
}
}
return self.elements
}
}
+83 -758
View File
@@ -12,116 +12,7 @@ extension OSLog {
private static var subsystem = "SwiftyTokeniser"
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
static let styling = OSLog(subsystem: subsystem, category: "Styling")
}
// Tag definition
public protocol CharacterStyling {
func isEqualTo( _ other : CharacterStyling ) -> Bool
}
public enum SpaceAllowed {
case no
case bothSides
case oneSide
case leadingSide
case trailingSide
}
public enum Cancel {
case none
case allRemaining
case currentSet
}
public struct CharacterRule : CustomStringConvertible {
public let openTag : String
public let intermediateTag : String?
public let closingTag : String?
public let escapeCharacter : Character?
public let styles : [Int : [CharacterStyling]]
public var minTags : Int = 1
public var maxTags : Int = 1
public var spacesAllowed : SpaceAllowed = .oneSide
public var cancels : Cancel = .none
public var description: String {
return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) "
}
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], minTags : Int = 1, maxTags : Int = 1, cancels : Cancel = .none) {
self.openTag = openTag
self.intermediateTag = intermediateTag
self.closingTag = closingTag
self.escapeCharacter = escapeCharacter
self.styles = styles
self.minTags = minTags
self.maxTags = maxTags
self.cancels = cancels
}
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case string
case escape
case replacement
}
public struct Token {
public let id = UUID().uuidString
public let type : TokenType
public let inputString : String
public fileprivate(set) var metadataString : String? = nil
public fileprivate(set) var characterStyles : [CharacterStyling] = []
public fileprivate(set) var count : Int = 0
public fileprivate(set) var shouldSkip : Bool = false
public fileprivate(set) var tokenIndex : Int = -1
public fileprivate(set) var isProcessed : Bool = false
public fileprivate(set) var isMetadata : Bool = false
public var outputString : String {
get {
switch self.type {
case .repeatingTag:
if count <= 0 {
return ""
} else {
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
return String(inputString[range])
}
case .openTag, .closeTag, .intermediateTag:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .escape, .string:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .replacement:
return self.inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
}
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
newToken.metadataString = self.metadataString
newToken.isMetadata = self.isMetadata
newToken.isProcessed = self.isProcessed
return newToken
}
}
extension Sequence where Iterator.Element == Token {
var oslogDisplay: String {
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
}
static let performance = OSLog(subsystem: subsystem, category: "Peformance")
}
public class SwiftyTokeniser {
@@ -129,9 +20,23 @@ public class SwiftyTokeniser {
var replacements : [String : [Token]] = [:]
var enableLog = (ProcessInfo.processInfo.environment["SwiftyTokeniserLogging"] != nil)
let totalPerfomanceLog = PerformanceLog(with: "SwiftyTokeniserPerformanceLogging", identifier: "Tokeniser Total Run Time", log: OSLog.performance)
let currentPerfomanceLog = PerformanceLog(with: "SwiftyTokeniserPerformanceLogging", identifier: "Tokeniser Current", log: OSLog.performance)
public var metadataLookup : [String : String] = [:]
let newlines = CharacterSet.newlines
let spaces = CharacterSet.whitespaces
public init( with rules : [CharacterRule] ) {
self.rules = rules
self.totalPerfomanceLog.start()
}
deinit {
self.totalPerfomanceLog.end()
}
@@ -147,675 +52,95 @@ public class SwiftyTokeniser {
///
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
public func process( _ inputString : String ) -> [Token] {
let currentTokens = [Token(type: .string, inputString: inputString)]
guard rules.count > 0 else {
return [Token(type: .string, inputString: inputString)]
return currentTokens
}
var currentTokens : [Token] = []
var mutableRules = self.rules
if inputString.isEmpty {
return [Token(type: .string, inputString: "", characterStyles: [])]
}
self.currentPerfomanceLog.start()
var elementArray : [Element] = []
for char in inputString {
if newlines.containsUnicodeScalars(of: char) {
let element = Element(character: char, type: .newline)
elementArray.append(element)
continue
}
if spaces.containsUnicodeScalars(of: char) {
let element = Element(character: char, type: .space)
elementArray.append(element)
continue
}
let element = Element(character: char, type: .string)
elementArray.append(element)
}
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
if enableLog {
os_log("------------------------------", log: .tokenising, type: .info)
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
}
if currentTokens.isEmpty {
// This means it's the first time through
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
self.currentPerfomanceLog.tag(with: "(start rule %@)")
let scanner = SwiftyScanner(withElements: elementArray, rule: nextRule, metadata: self.metadataLookup)
elementArray = scanner.scan()
}
var output : [Token] = []
var lastElement = elementArray.first!
func empty( _ string : inout String, into tokens : inout [Token] ) {
guard !string.isEmpty else {
return
}
var token = Token(type: .string, inputString: string)
token.metadataStrings.append(contentsOf: lastElement.metadata)
token.characterStyles = lastElement.styles
string.removeAll()
tokens.append(token)
}
var accumulatedString = ""
for element in elementArray {
guard element.type != .escape else {
continue
}
var outerStringTokens : [Token] = []
var innerStringTokens : [Token] = []
var isOuter = true
for idx in 0..<currentTokens.count {
let nextToken = currentTokens[idx]
if nextToken.type == .openTag && nextToken.isProcessed {
isOuter = false
}
if nextToken.type == .closeTag {
let ref = UUID().uuidString
outerStringTokens.append(Token(type: .replacement, inputString: ref))
innerStringTokens.append(nextToken)
self.replacements[ref] = self.handleReplacementTokens(innerStringTokens, with: nextRule)
innerStringTokens.removeAll()
isOuter = true
continue
}
(isOuter) ? outerStringTokens.append(nextToken) : innerStringTokens.append(nextToken)
guard element.type == .string || element.type == .space || element.type == .newline else {
empty(&accumulatedString, into: &output)
continue
}
currentTokens = self.handleReplacementTokens(outerStringTokens, with: nextRule)
var finalTokens : [Token] = []
for token in currentTokens {
guard token.type == .replacement else {
finalTokens.append(token)
continue
}
if let hasReplacement = self.replacements[token.inputString] {
if enableLog {
os_log("Found replacement for %@", log: .tokenising, type: .info, token.inputString)
}
for var repToken in hasReplacement {
guard repToken.type == .string else {
finalTokens.append(repToken)
continue
}
for style in token.characterStyles {
if !repToken.characterStyles.contains(where: { $0.isEqualTo(style)}) {
repToken.characterStyles.append(contentsOf: token.characterStyles)
}
}
finalTokens.append(repToken)
}
}
if lastElement.styles as? [CharacterStyle] != element.styles as? [CharacterStyle] {
empty(&accumulatedString, into: &output)
}
currentTokens = finalTokens
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
// The one string token might then be exploded into multiple more tokens
accumulatedString.append(element.character)
lastElement = element
}
empty(&accumulatedString, into: &output)
self.currentPerfomanceLog.tag(with: "(finished all rules)")
if enableLog {
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
os_log("==================================", log: .tokenising, type: .info)
}
return currentTokens
return output
}
}
extension String {
func repeating( _ max : Int ) -> String {
var output = self
for _ in 1..<max {
output += self
}
return output
}
/// In order to reinsert the original replacements into the new string token, the replacements
/// need to be searched for in the incoming string one by one.
///
/// Using the `newToken(fromSubstring:isReplacement:)` function ensures that any metadata and character styles
/// are passed over into the newly created tokens.
///
/// E.g. A string token that has an `outputString` of "This string AAAAA-BBBBB-CCCCC replacements", with
/// a characterStyle of `bold` for the entire string, needs to be separated into the following tokens:
///
/// - `string`: "This string "
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
/// - `string`: " replacements"
///
/// Each of these need to have a character style of `bold`.
///
/// - Parameters:
/// - replacements: An array of `replacement` tokens
/// - token: The new `string` token that may contain replacement IDs contained in the `replacements` array
func reinsertReplacements(_ replacements : [Token], from stringToken : Token ) -> [Token] {
guard !stringToken.outputString.isEmpty && !replacements.isEmpty else {
return [stringToken]
}
var outputTokens : [Token] = []
let scanner = Scanner(string: stringToken.outputString)
scanner.charactersToBeSkipped = nil
// Remove any replacements that don't appear in the incoming string
var repTokens = replacements.filter({ stringToken.outputString.contains($0.inputString) })
var testString = "\n"
while !scanner.isAtEnd {
var outputString : String = ""
if repTokens.count > 0 {
testString = repTokens.removeFirst().inputString
}
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let nextString = scanner.scanUpToString(testString) {
outputString = nextString
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
if let outputToken = scanner.scanString(testString) {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else if let outputToken = scanner.scanString(testString) {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else {
var oldString : NSString? = nil
var tokenString : NSString? = nil
scanner.scanUpTo(testString, into: &oldString)
if let nextString = oldString {
outputString = nextString as String
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
scanner.scanString(testString, into: &tokenString)
if let outputToken = tokenString as String? {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else {
scanner.scanString(testString, into: &tokenString)
if let outputToken = tokenString as String? {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
}
}
}
return outputTokens
}
/// This function is necessary because a previously tokenised string might have
///
/// Consider a previously tokenised string, where AAAAA-BBBBB-CCCCC represents a replaced \[link\](url) instance.
///
/// The incoming tokens will look like this:
///
/// - `string`: "A \*\*Bold"
/// - `replacement` : "AAAAA-BBBBB-CCCCC"
/// - `string`: " with a trailing string**"
///
/// However, because the scanner can only tokenise individual strings, passing in the string values
/// of these tokens individually and applying the styles will not correctly detect the starting and
/// ending `repeatingTag` instances. (e.g. the scanner will see "A \*\*Bold", and then "AAAAA-BBBBB-CCCCC",
/// and finally " with a trailing string\*\*")
///
/// The strings need to be combined, so that they form a single string:
/// A \*\*Bold AAAAA-BBBBB-CCCCC with a trailing string\*\*.
/// This string is then parsed and tokenised so that it looks like this:
///
/// - `string`: "A "
/// - `repeatingTag`: "\*\*"
/// - `string`: "Bold AAAAA-BBBBB-CCCCC with a trailing string"
/// - `repeatingTag`: "\*\*"
///
/// Finally, the replacements from the original incoming token array are searched for and pulled out
/// of this new string, so the final result looks like this:
///
/// - `string`: "A "
/// - `repeatingTag`: "\*\*"
/// - `string`: "Bold "
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
/// - `string`: " with a trailing string"
/// - `repeatingTag`: "\*\*"
///
/// - Parameters:
/// - tokens: The tokens to be combined, scanned, re-tokenised, and merged
/// - rule: The character rule currently being applied
func scanReplacementTokens( _ tokens : [Token], with rule : CharacterRule ) -> [Token] {
guard tokens.count > 0 else {
return []
}
let combinedString = tokens.map({ $0.outputString }).joined()
let nextTokens = self.scan(combinedString, with: rule)
var replacedTokens = self.applyStyles(to: nextTokens, usingRule: rule)
/// It's necessary here to check to see if the first token (which will always represent the styles
/// to be applied from previous scans) has any existing metadata or character styles and apply them
/// to *all* the string and replacement tokens found by the new scan.
for idx in 0..<replacedTokens.count {
guard replacedTokens[idx].type == .string || replacedTokens[idx].type == .replacement else {
continue
}
if tokens.first!.metadataString != nil && replacedTokens[idx].metadataString == nil {
replacedTokens[idx].metadataString = tokens.first!.metadataString
}
replacedTokens[idx].characterStyles.append(contentsOf: tokens.first!.characterStyles)
}
// Swap the original replacement tokens back in
let replacements = tokens.filter({ $0.type == .replacement })
var outputTokens : [Token] = []
for token in replacedTokens {
guard token.type == .string else {
outputTokens.append(token)
continue
}
outputTokens.append(contentsOf: self.reinsertReplacements(replacements, from: token))
}
return outputTokens
}
/// This function ensures that only concurrent `string` and `replacement` tokens are processed together.
///
/// i.e. If there is an existing `repeatingTag` token between two strings, then those strings will be
/// processed individually. This prevents incorrect parsing of strings like "\*\*\_Should only be bold\*\*\_"
///
/// - Parameters:
/// - incomingTokens: A group of tokens whose string tokens and replacement tokens should be combined and re-tokenised
/// - rule: The current rule being processed
func handleReplacementTokens( _ incomingTokens : [Token], with rule : CharacterRule) -> [Token] {
// Only combine string and replacements that are next to each other.
var newTokenSet : [Token] = []
var currentTokenSet : [Token] = []
for i in 0..<incomingTokens.count {
guard incomingTokens[i].type == .string || incomingTokens[i].type == .replacement else {
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
newTokenSet.append(incomingTokens[i])
currentTokenSet.removeAll()
continue
}
guard !incomingTokens[i].isProcessed && !incomingTokens[i].isMetadata && !incomingTokens[i].shouldSkip else {
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
newTokenSet.append(incomingTokens[i])
currentTokenSet.removeAll()
continue
}
currentTokenSet.append(incomingTokens[i])
}
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
return newTokenSet
}
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
guard rule.closingTag != nil else {
return
}
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag && !$0.isProcessed }) else {
return
}
var metadataIndex = index
// If there's an intermediate tag, get the index of that
if rule.intermediateTag != nil {
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag && !$0.isProcessed }) else {
return
}
metadataIndex = nextTokenIdx
let styles : [CharacterStyling] = rule.styles[1] ?? []
for i in index..<nextTokenIdx {
for style in styles {
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
tokens[i].characterStyles.append(style)
}
}
}
}
var metadataString : String = ""
for i in metadataIndex..<closeTokenIdx {
if tokens[i].type == .string {
metadataString.append(tokens[i].outputString)
tokens[i].isMetadata = true
}
}
for i in index..<metadataIndex {
if tokens[i].type == .string {
tokens[i].metadataString = metadataString
}
}
tokens[closeTokenIdx].isProcessed = true
tokens[metadataIndex].isProcessed = true
tokens[index].isProcessed = true
}
/// This is here to manage how opening tags are matched with closing tags when they're all the same
/// character.
///
/// Of course, because Markdown is about as loose as a spec can be while still being considered any
/// kind of spec, the number of times this character repeats causes different effects. Then there
/// is the ill-defined way it should work if the number of opening and closing tags are different.
///
/// - Parameters:
/// - index: The index of the current token in the loop
/// - tokens: An inout variable of the loop tokens of interest
/// - rule: The character rule being applied
func handleClosingTagFromRepeatingTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule) {
let theToken = tokens[index]
if enableLog {
os_log("Found repeating tag with tag count: %i, tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString, rule.openTag )
}
guard theToken.count > 0 else {
return
}
let startIdx = index
var endIdx : Int? = nil
let maxCount = (theToken.count > rule.maxTags) ? rule.maxTags : theToken.count
// Try to find exact match first
if let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id && !$0.isProcessed }) {
endIdx = nextTokenIdx
}
if endIdx == nil, let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count >= 1 && $0.id != theToken.id && !$0.isProcessed }) {
endIdx = nextTokenIdx
}
guard let existentEnd = endIdx else {
return
}
let styles : [CharacterStyling] = rule.styles[maxCount] ?? []
for i in startIdx..<existentEnd {
for style in styles {
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
tokens[i].characterStyles.append(style)
}
}
if rule.cancels == .allRemaining {
tokens[i].shouldSkip = true
}
}
let maxEnd = (tokens[existentEnd].count > rule.maxTags) ? rule.maxTags : tokens[existentEnd].count
tokens[index].count = theToken.count - maxEnd
tokens[existentEnd].count = tokens[existentEnd].count - maxEnd
if maxEnd < rule.maxTags {
self.handleClosingTagFromRepeatingTag(withIndex: index, in: &tokens, following: rule)
} else {
tokens[existentEnd].isProcessed = true
tokens[index].isProcessed = true
}
}
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
var mutableTokens : [Token] = tokens
if enableLog {
os_log("Applying styles to tokens: %@", log: .tokenising, type: .info, tokens.oslogDisplay )
}
for idx in 0..<mutableTokens.count {
let token = mutableTokens[idx]
switch token.type {
case .escape:
if enableLog {
os_log("Found escape: %@", log: .tokenising, type: .info, token.inputString )
}
case .repeatingTag:
self.handleClosingTagFromRepeatingTag(withIndex: idx, in: &mutableTokens, following: rule)
case .openTag:
let theToken = mutableTokens[idx]
if enableLog {
os_log("Found repeating tag with tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.inputString, rule.openTag )
}
guard rule.closingTag != nil else {
// If there's an intermediate tag, get the index of that
// Get the index of the closing tag
continue
}
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
case .intermediateTag:
let theToken = mutableTokens[idx]
if enableLog {
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
}
case .closeTag:
let theToken = mutableTokens[idx]
if enableLog {
os_log("Found close tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
}
case .string:
let theToken = mutableTokens[idx]
if enableLog {
if theToken.isMetadata {
os_log("Found Metadata: %@", log: .tokenising, type: .info, theToken.inputString )
} else {
os_log("Found String: %@", log: .tokenising, type: .info, theToken.inputString )
}
if let hasMetadata = theToken.metadataString {
os_log("...with metadata: %@", log: .tokenising, type: .info, hasMetadata )
}
}
case .replacement:
if enableLog {
os_log("Found replacement with ID: %@", log: .tokenising, type: .info, mutableTokens[idx].inputString )
}
}
}
return mutableTokens
}
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
var tokens : [Token] = []
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
if let existentEscape = rule.escapeCharacter {
set.insert(charactersIn: String(existentEscape))
}
var openTagFound = false
var openingString = ""
while !scanner.isAtEnd {
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let start = scanner.scanUpToCharacters(from: set) {
openingString.append(start)
}
} else {
var string : NSString?
scanner.scanUpToCharacters(from: set, into: &string)
if let existentString = string as String? {
openingString.append(existentString)
}
// Fallback on earlier versions
}
let lastChar : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
} else {
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
} else {
lastChar = nil
}
}
let maybeFoundChars : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
maybeFoundChars = scanner.scanCharacters(from: set )
} else {
var string : NSString?
scanner.scanCharacters(from: set, into: &string)
maybeFoundChars = string as String?
}
let nextChar : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0,tvOS 13.0, *) {
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
} else {
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
} else {
nextChar = nil
}
}
guard let foundChars = maybeFoundChars else {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
continue
}
if foundChars == rule.openTag && foundChars.count < rule.minTags {
openingString.append(foundChars)
continue
}
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
if let hasIntermediateTag = rule.intermediateTag {
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
}
if let existentClosingTag = rule.closingTag {
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
}
openingString.append(escaped)
continue
}
var cumulativeString = ""
var openString = ""
var intermediateString = ""
var closedString = ""
var maybeEscapeNext = false
func addToken( for type : TokenType ) {
var inputString : String
switch type {
case .openTag:
inputString = openString
case .intermediateTag:
inputString = intermediateString
case .closeTag:
inputString = closedString
default:
inputString = ""
}
guard !inputString.isEmpty else {
return
}
guard inputString.count >= rule.minTags else {
return
}
if !openingString.isEmpty {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
}
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
var token = Token(type: actualType, inputString: inputString)
if rule.closingTag == nil {
token.count = inputString.count
}
tokens.append(token)
switch type {
case .openTag:
openString = ""
case .intermediateTag:
intermediateString = ""
case .closeTag:
closedString = ""
default:
break
}
}
// Here I am going through and adding the characters in the found set to a cumulative string.
// If there is an escape character, then the loop stops and any open tags are tokenised.
for char in foundChars {
cumulativeString.append(char)
if maybeEscapeNext {
var escaped = cumulativeString
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
}
openingString.append(escaped)
cumulativeString = ""
maybeEscapeNext = false
}
if let existentEscape = rule.escapeCharacter {
if cumulativeString == String(existentEscape) {
maybeEscapeNext = true
addToken(for: .openTag)
addToken(for: .intermediateTag)
addToken(for: .closeTag)
continue
}
}
if cumulativeString == rule.openTag {
openString.append(char)
cumulativeString = ""
openTagFound = true
} else if cumulativeString == rule.intermediateTag, openTagFound {
intermediateString.append(cumulativeString)
cumulativeString = ""
} else if cumulativeString == rule.closingTag, openTagFound {
closedString.append(char)
cumulativeString = ""
openTagFound = false
}
}
// If we're here, it means that an escape character was found but without a corresponding
// tag, which means it might belong to a different rule.
// It should be added to the next group of regular characters
addToken(for: .openTag)
addToken(for: .intermediateTag)
addToken(for: .closeTag)
openingString.append( cumulativeString )
}
if !openingString.isEmpty {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
}
return tokens
}
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
switch rule.spacesAllowed {
case .leadingSide:
guard nextCharacter != nil else {
return true
}
if nextCharacter == " " {
return false
}
case .trailingSide:
guard previousCharacter != nil else {
return true
}
if previousCharacter == " " {
return false
}
case .no:
switch (previousCharacter, nextCharacter) {
case (nil, nil), ( " ", _ ), ( _, " " ):
return false
default:
return true
}
case .oneSide:
switch (previousCharacter, nextCharacter) {
case (nil, " " ), (" ", nil), (" ", " " ):
return false
default:
return true
}
default:
break
}
return true
}
}
+81
View File
@@ -0,0 +1,81 @@
//
// Token.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
// Tag definition
public protocol CharacterStyling {
func isEqualTo( _ other : CharacterStyling ) -> Bool
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case string
case escape
case replacement
}
public struct Token {
public let id = UUID().uuidString
public let type : TokenType
public let inputString : String
public var metadataStrings : [String] = []
public internal(set) var group : Int = 0
public internal(set) var characterStyles : [CharacterStyling] = []
public internal(set) var count : Int = 0
public internal(set) var shouldSkip : Bool = false
public internal(set) var tokenIndex : Int = -1
public internal(set) var isProcessed : Bool = false
public internal(set) var isMetadata : Bool = false
public var children : [Token] = []
public var outputString : String {
get {
switch self.type {
case .repeatingTag:
if count <= 0 {
return ""
} else {
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
return String(inputString[range])
}
case .openTag, .closeTag, .intermediateTag:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .escape, .string:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .replacement:
return self.inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
if type == .repeatingTag {
self.count = inputString.count
}
}
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
newToken.metadataStrings = self.metadataStrings
newToken.isMetadata = self.isMetadata
newToken.isProcessed = self.isProcessed
return newToken
}
}
extension Sequence where Iterator.Element == Token {
var oslogDisplay: String {
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SwiftyMarkdown"
s.version = "1.1.0"
s.version = "1.2.4"
s.summary = "Converts Markdown to NSAttributed String"
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
s.license = 'MIT'
@@ -14,7 +14,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<string>1.2.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@@ -1,25 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.2.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>SwiftyMarkdownPerformanceTests</key>
<dict>
<key>testThatFilesAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -11,9 +11,11 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0217</real>
<real>0.212</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
<key>maxPercentRelativeStandardDeviation</key>
<real>10</real>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
@@ -4,6 +4,30 @@
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>88991ED5-B954-422F-B610-BDC9A4AEC008</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>400</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>8-Core Intel Core i9</string>
<key>cpuSpeedInMHz</key>
<integer>2400</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>16</integer>
<key>modelCode</key>
<string>MacBookPro16,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>x86_64h</string>
</dict>
<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
<dict>
<key>localComputer</key>
@@ -50,6 +50,43 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<EnvironmentVariables>
<EnvironmentVariable
key = "SwiftyTokeniserLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerScanner"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerScannerPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyLineProcessorPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyTokeniserPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyMarkdownPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -9,26 +9,90 @@
@testable import SwiftyMarkdown
import XCTest
class SwiftyMarkdownCharacterTests: XCTestCase {
class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
func testIsolatedCase() {
let challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [
Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: [])
func off_testIsolatedCase() {
challenge = TokenTest(input: "*\\***\\****b*\\***\\****\\", output: "***b***\\", tokens : [
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "*b**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "\\", characterStyles: [])
])
let results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
return
challenge = TokenTest(input: """
An asterisk: *
Line break
""", output: """
An asterisk: *
Line break
""", tokens: [
Token(type: .string, inputString: "An asterisk: *", characterStyles: []),
Token(type: .string, inputString: "Line break", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
return
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [referenced link][link]\n[notLink]: https://www.neverendingvoyage.com/", output: "A [referenced link][link]", tokens: [
Token(type: .string, inputString: "A [referenced link][link]", characterStyles: [])
])
results = self.attempt(challenge, rules: [.links, .images, .referencedLinks])
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 0)
}
func testThatBoldTraitsAreRecognised() {
var challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -38,8 +102,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -47,8 +117,29 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\\\*\\*A normal \\\\ string\\*\\*", output: "\\**A normal \\\\ string**", tokens: [
Token(type: .string, inputString: "\\**A normal \\\\ string**", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -56,8 +147,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -66,8 +163,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]),
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -77,19 +180,31 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatCodeTraitsAreRecognised() {
var challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [
Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code])
challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [
Token(type: .string, inputString: "Code (**should** not process internal tags)", characterStyles: [CharacterStyle.code])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -99,8 +214,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " (should not be indented)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -112,8 +233,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "instances", characterStyles: [CharacterStyle.code])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -121,8 +248,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "`A normal string`", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -130,8 +263,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -139,20 +278,47 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "A lonely backtick: `", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "Two backticks followed by a full stop ``.", output: "Two backticks followed by a full stop ``.", tokens: [
Token(type: .string, inputString: "Two backticks followed by a full stop ``.", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatItalicTraitsAreParsedCorrectly() {
var challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -162,8 +328,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " text", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -178,8 +350,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.italic])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -188,8 +366,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "_A normal string_", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -197,8 +381,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -213,21 +403,26 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "Line break", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatStrikethroughTraitsAreRecognised() {
var challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [
challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [
Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]),
Token(type: .string, inputString: "A crossed-out string", characterStyles: [])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
challenge = TokenTest(input: "A **Bold** string and a ~~removed~~crossed-out string", output: "A Bold string and a removedcrossed-out string", tokens: [
@@ -238,38 +433,56 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "crossed-out string", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [
Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
}
func testThatMixedTraitsAreRecognised() {
var challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [
challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "with a ", characterStyles: []),
Token(type: .string, inputString: " with a ", characterStyles: []),
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " bold ", characterStyles: []),
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.bold])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
challenge = TokenTest(input: "_An italic string_, **followed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, followed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]),
@@ -284,30 +497,103 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " variety.", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatExtraCharactersAreHandles() {
var challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
func testForExtremeEscapeCombinations() {
challenge = TokenTest(input: "\\****b\\****", output: "*b*", tokens : [
Token(type: .string, inputString: "*", characterStyles: []),
Token(type: .string, inputString: "b*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* word", tokens: [
challenge = TokenTest(input: "**\\**b*\\***", output: "*b*", tokens : [
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "b", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
// challenge = TokenTest(input: "Before *\\***\\****A bold string*\\***\\****\\ After", output: "Before ***A bold string***\\ After", tokens : [
// Token(type: .string, inputString: "Before ", characterStyles: []),
// Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
// Token(type: .string, inputString: "**", characterStyles: [CharacterStyle.bold]),
// Token(type: .string, inputString: "A bold string**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
// Token(type: .string, inputString: "\\ After", characterStyles: [])
// ])
// results = self.attempt(challenge)
// if results.stringTokens.count == challenge.tokens.count {
// for (idx, token) in results.stringTokens.enumerated() {
// XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
// XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
// }
// } else {
// XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
// }
// XCTAssertEqual(results.foundStyles, results.expectedStyles)
// XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatExtraCharactersAreHandles() {
challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ****bold**** word", output: "A string with a bold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "*bold italic*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -317,8 +603,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -329,21 +621,64 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with ```code`", output: "A string with ```code`", tokens : [
Token(type: .string, inputString: "A string with ```code`", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with ```code```", output: "A string with code", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -365,14 +700,20 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
func offtestAdvancedEscaping() {
var challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [
challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [
Token(type: .string, inputString: "**", characterStyles: []),
Token(type: .string, inputString: "A normal string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "**", characterStyles: [])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
@@ -382,8 +723,14 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
Token(type: .string, inputString: "** asterisks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
@@ -396,10 +743,9 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
let asteriskComma = "An asterisk followed by a full stop: *, *"
let backtickSpace = "A backtick followed by a space: `"
let backtickFullStop = "Two backticks followed by a full stop: ``."
let underscoreSpace = "An underscore followed by a space: _"
let backtickComma = "A backtick followed by a space: `, `"
let underscoreComma = "An underscore followed by a space: _, _"
@@ -407,6 +753,7 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
var md = SwiftyMarkdown(string: backtickSpace)
SwiftyMarkdown.characterRules = self.defaultRules
XCTAssertEqual(md.attributedString().string, backtickSpace)
md = SwiftyMarkdown(string: underscoreSpace)
@@ -415,9 +762,6 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
md = SwiftyMarkdown(string: asteriskFullStop)
XCTAssertEqual(md.attributedString().string, asteriskFullStop)
md = SwiftyMarkdown(string: backtickFullStop)
XCTAssertEqual(md.attributedString().string, backtickFullStop)
md = SwiftyMarkdown(string: underscoreFullStop)
XCTAssertEqual(md.attributedString().string, underscoreFullStop)
@@ -441,6 +785,22 @@ class SwiftyMarkdownCharacterTests: XCTestCase {
}
func testReportedCrashingStrings() {
challenge = TokenTest(input: "[**\\!bang**](https://duckduckgo.com/bang)", output: "\\!bang", tokens: [
Token(type: .string, inputString: "\\!bang", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
}
@@ -189,12 +189,7 @@ A break
}
func testReportedCrashingStrings() {
let text = "[**\\!bang**](https://duckduckgo.com/bang) "
let expected = "\\!bang"
let output = SwiftyMarkdown(string: text).attributedString().string
XCTAssertEqual(output, expected)
}
func testThatYAMLMetadataIsRemoved() {
let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!\n\nWith A Heading\n---", expectedOutput: "Finally some Markdown!\n\nWith A Heading")
@@ -9,35 +9,142 @@
@testable import SwiftyMarkdown
import XCTest
class SwiftyMarkdownLinkTests: XCTestCase {
class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
func testForLinks() {
var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [
Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link])
func testSingleLinkPositions() {
challenge = TokenTest(input: "[a](b)", output: "a", tokens: [
Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge, rules: [.links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/")
XCTAssertEqual(existentOpen.metadataStrings.first, "b")
} else {
XCTFail("Failed to find an open link tag")
}
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link])
challenge = TokenTest(input: "[Link at](http://voyagetravelapps.com/) start", output: "Link at start", tokens: [
Token(type: .string, inputString: "Link at", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " start")
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Failed to find an open link tag")
}
challenge = TokenTest(input: "A [link at end](http://voyagetravelapps.com/)", output: "A link at end", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "link at end", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [link in the](http://voyagetravelapps.com/) middle", output: "A link in the middle", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "link in the", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " middle", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testEscapedLinks() {
challenge = TokenTest(input: "\\[a](b)", output: "[a](b)", tokens: [
Token(type: .string, inputString: "[a](b)", characterStyles: [])
])
results = self.attempt(challenge, rules: [.images, .referencedLinks, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "![a](b)", output: "!a", tokens: [
Token(type: .string, inputString: "!", characterStyles: []),
Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testMultipleLinkPositions() {
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/)[Link 2](https://www.neverendingvoyage.com/)", output: "Link 1Link 2", tokens: [
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
@@ -46,73 +153,402 @@ class SwiftyMarkdownLinkTests: XCTestCase {
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 2)
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "String at start [Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "String at start Link 1, Link 2", tokens: [
Token(type: .string, inputString: "String at start ", characterStyles: []),
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "String at start [Link 1](http://voyagetravelapps.com/)[Link 2](https://www.neverendingvoyage.com/)", output: "String at start Link 1Link 2", tokens: [
Token(type: .string, inputString: "String at start ", characterStyles: []),
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
func testForAlternativeURLs() {
challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [
Token(type: .string, inputString: "Email us at ", characterStyles: []),
Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " Twitter", characterStyles: []),
Token(type: .string, inputString: " Twitter ", characterStyles: []),
Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "mailto:simon@voyagetravelapps.com")
XCTAssertEqual(links[1].metadataStrings.first, "twitter://user?screen_name=VoyageTravelApp")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
func testForLinksMixedWithTokenCharacters() {
challenge = TokenTest(input: "Link ([Surrounded by parentheses](https://www.neverendingvoyage.com/))", output: "Link (Surrounded by parentheses)", tokens: [
Token(type: .string, inputString: "Link (", characterStyles: []),
Token(type: .string, inputString: "Surrounded by parentheses", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ")", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 1 {
XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "[[Surrounded by square brackets](https://www.neverendingvoyage.com/)]", output: "[Surrounded by square brackets]", tokens: [
Token(type: .string, inputString: "[", characterStyles: []),
Token(type: .string, inputString: "Surrounded by square brackets", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "]", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 2)
XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com")
XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp")
if links.count == 1 {
XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
func testMalformedLinks() {
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []),
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
Token(type: .string, inputString: "[Link with missing parenthesis](http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "[Link](", characterStyles: []),
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
Token(type: .string, inputString: "A [Link](http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[A link](((url)", output: "[A link](((url)", tokens: [
Token(type: .string, inputString: "[A link](((url)", characterStyles: [])
])
results = self.attempt(challenge, rules: [.images, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 0)
challenge = TokenTest(input: "[[a](((b)](c)", output: "[a](((b)", tokens: [
Token(type: .string, inputString: "[a](((b)", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.images, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "c")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[Link with [second opening](http://voyagetravelapps.com/)", output: "[Link with second opening", tokens: [
Token(type: .string, inputString: "[Link with ", characterStyles: []),
Token(type: .string, inputString: "second opening", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "A [Link(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testMalformedLinksWithValidLinks() {
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a [valid link](http://voyagetravelapps.com/)", output: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a valid link", tokens: [
Token(type: .string, inputString: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/ followed by a [valid link](http://voyagetravelapps.com/)", output: "A [Link](http://voyagetravelapps.com/ followed by a valid link", tokens: [
Token(type: .string, inputString: "A [Link](http://voyagetravelapps.com/ followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/) followed by a [valid link](http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/) followed by a valid link", tokens: [
Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/) followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/) followed by a [valid link](http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/) followed by a valid link", tokens: [
Token(type: .string, inputString: "A [Link(http://voyagetravelapps.com/) followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testLinksWithOtherStyles() {
challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A Bold ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [ CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " ", characterStyles: []),
@@ -121,59 +557,178 @@ class SwiftyMarkdownLinkTests: XCTestCase {
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testLinksWithOtherStyles() {
var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
// XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 1)
if links.count == 1 {
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A Bold ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
}
func testForImages() {
let challenge = TokenTest(input: "An ![Image](imageName)", output: "An Image", tokens: [
Token(type: .string, inputString: "An Image", characterStyles: []),
Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image])
challenge = TokenTest(input: "An ![Image](imageName)", output: "An ", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
])
let results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[0].metadataString, "imageName")
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.images.count)")
}
challenge = TokenTest(input: "An [![Image](imageName)](https://www.neverendingvoyage.com/)", output: "An ", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image, CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.images.count)")
}
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.last, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForReferencedImages() {
challenge = TokenTest(input: "A ![referenced image][image]\n[image]: imageName", output: "A ", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced image", characterStyles: [CharacterStyle.image])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForReferencedLinks() {
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [referenced link][link]\n [link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "An *\\*italic\\** [referenced link][a]\n[a]: link", output: "An *italic* referenced link", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "*italic*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.asterisks, .links, .referencedLinks])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "link")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForMixedLinkStyles() {
challenge = TokenTest(input: "A [referenced link][link] and a [regular link](http://voyagetravelapps.com/)\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link and a regular link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " and a ", characterStyles: []),
Token(type: .string, inputString: "regular link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 2 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
XCTAssertEqual(results.links[1].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
}
@@ -33,7 +33,7 @@ class SwiftyMarkdownPerformanceTests: XCTestCase {
}
}
func testThatVeryLongStringsAreProcessedQuickly() {
func testThatVeryLongStringsAreProcessedQuickly() {
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
let md = SwiftyMarkdown(string: string)
measure {
@@ -9,6 +9,79 @@
import XCTest
@testable import SwiftyMarkdown
struct ChallengeReturn {
let tokens : [Token]
let stringTokens : [Token]
let links : [Token]
let images : [Token]
let attributedString : NSAttributedString
let foundStyles : [[CharacterStyle]]
let expectedStyles : [[CharacterStyle]]
}
enum Rule {
case asterisks
case backticks
case underscores
case images
case links
case referencedLinks
case referencedImages
case tildes
func asCharacterRule() -> CharacterRule {
switch self {
case .images:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && !$0.metadataLookup }).first!
case .links:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && !$0.metadataLookup }).first!
case .backticks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "`" }).first!
case .tildes:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "~" }).first!
case .asterisks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "*" }).first!
case .underscores:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "_" }).first!
case .referencedLinks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && $0.metadataLookup }).first!
case .referencedImages:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && $0.metadataLookup }).first!
}
}
}
class SwiftyMarkdownCharacterTests : XCTestCase {
let defaultRules = SwiftyMarkdown.characterRules
var challenge : TokenTest!
var results : ChallengeReturn!
func attempt( _ challenge : TokenTest, rules : [Rule]? = nil ) -> ChallengeReturn {
if let validRules = rules {
SwiftyMarkdown.characterRules = validRules.map({ $0.asCharacterRule() })
} else {
SwiftyMarkdown.characterRules = self.defaultRules
}
let md = SwiftyMarkdown(string: challenge.input)
md.applyAttachments = false
let attributedString = md.attributedString()
let tokens : [Token] = md.previouslyFoundTokens
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let linkTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
let imageTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
return ChallengeReturn(tokens: tokens, stringTokens: stringTokens, links : linkTokens, images: imageTokens, attributedString: attributedString, foundStyles: existentTokenStyles, expectedStyles : expectedStyles)
}
}
extension XCTestCase {
func resourceURL(for filename : String ) -> URL {
@@ -17,15 +90,7 @@ extension XCTestCase {
return thisDirectory.appendingPathComponent("Resources", isDirectory: true).appendingPathComponent(filename)
}
func attempt( _ challenge : TokenTest ) -> (tokens : [Token], stringTokens: [Token], attributedString : NSAttributedString, foundStyles : [[CharacterStyle]], expectedStyles : [[CharacterStyle]] ) {
let md = SwiftyMarkdown(string: challenge.input)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
let tokens = tokeniser.process(challenge.input)
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
return (tokens, stringTokens, md.attributedString(), existentTokenStyles, expectedStyles)
}
}
+1 -1
View File
@@ -20,4 +20,4 @@ $scheme = "SwiftyMarkdown"
$spec = "SwiftyMarkdown.podspec"
$project = './SwiftyMarkdown.xcodeproj'
import "../../Fastlane/FastfilePods"
import "/Users/simon/Developer/Fastlane/FastfilePods"
+35 -20
View File
@@ -1,25 +1,26 @@
fastlane documentation
================
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
```sh
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## iOS
### ios patch
```sh
[bundle exec] fastlane ios patch
```
fastlane ios patch
```
This does the following:
@@ -29,10 +30,13 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the patch version
### ios minor
```sh
[bundle exec] fastlane ios minor
```
fastlane ios minor
```
This does the following:
@@ -42,10 +46,13 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the minor version
### ios major
```sh
[bundle exec] fastlane ios major
```
fastlane ios major
```
This does the following:
@@ -55,19 +62,27 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the major version
### ios test
```
fastlane ios test
```sh
[bundle exec] fastlane ios test
```
### ios submit_pod
```sh
[bundle exec] fastlane ios submit_pod
```
fastlane ios submit_pod
```
Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to run tests, bump versions, and commit changes.
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).