Compare commits

...

155 Commits

Author SHA1 Message Date
Simon Fairbairn 36f0c4d9c7 Merge branch 'release/1.0.2' 2020-01-30 12:38:25 +13:00
Simon Fairbairn 3b9f770ea9 Version Bump 2020-01-30 12:37:47 +13:00
Simon Fairbairn a1dfe192af Updating podspec 2020-01-30 12:37:44 +13:00
Simon Fairbairn 050ca10694 Merge branch 'feature/moreMarkdown' into develop 2020-01-30 12:36:29 +13:00
Simon Fairbairn 45d03f88b7 Updates gitignore 2020-01-30 12:36:22 +13:00
Simon Fairbairn 586cecff2f Fixes macOS version 2020-01-30 12:35:49 +13:00
Simon Fairbairn 124e0f0a48 Fixes #70 2020-01-30 12:05:34 +13:00
Simon Fairbairn d510f69d10 Fixes #68 2020-01-30 11:51:34 +13:00
Simon Fairbairn 19c473e81d Fixes #69 2020-01-30 11:46:12 +13:00
Simon Fairbairn a209277d97 Merge branch 'release/1.0.1' into develop 2020-01-13 15:00:11 +13:00
Simon Fairbairn 3546711696 Merge branch 'release/1.0.1' 2020-01-13 15:00:08 +13:00
Simon Fairbairn 3aa40eaa38 Version Bump 2020-01-13 14:59:45 +13:00
Simon Fairbairn 0bd12bd5a7 Updating podspec 2020-01-13 14:59:43 +13:00
Simon Fairbairn d3a820b6e9 Fixes issue with link styling not being removed between links 2020-01-13 14:58:01 +13:00
Simon Fairbairn 56f3c87925 Merge branch 'release/1.0.0' into develop 2019-12-21 17:30:53 +13:00
Simon Fairbairn aac0c4a74c Merge branch 'release/1.0.0' 2019-12-21 17:30:50 +13:00
Simon Fairbairn 757e3fd212 Version Bump 2019-12-21 17:30:37 +13:00
Simon Fairbairn 2c2da71337 Updating podspec 2019-12-21 17:30:35 +13:00
Simon Fairbairn d779ce978d Updates Gemfile 2019-12-21 17:29:23 +13:00
Simon Fairbairn 0da260626a Updates .h file to remove possible issues with testing 2019-12-21 17:28:30 +13:00
Simon Fairbairn 732a01872f More readme fixes 2019-12-21 17:27:37 +13:00
Simon Fairbairn e7d60503cd Updates to readme 2019-12-21 14:00:01 +13:00
Simon Fairbairn 9c9e6c3597 Updates readme and adds support for customising the default rule sets 2019-12-21 08:09:34 +13:00
Simon Fairbairn ff36dbb07f Fixes issues preventing the Cocoapods linter from passing the package 2019-12-20 22:02:53 +13:00
Simon Fairbairn d149b1aa0e Fixes pod lint errors on watchOS 2019-12-20 21:57:22 +13:00
Simon Fairbairn e117006ad2 Updates readme 2019-12-20 21:50:21 +13:00
Simon Fairbairn cb49124b9c Adds better support for different repeating tag combinations and improves character style management 2019-12-20 16:38:47 +13:00
Simon Fairbairn c2325bac2f Fixes issue where **[Link](url)** would not be bolded 2019-12-20 10:56:03 +13:00
Simon Fairbairn 81857b5f4d Adds support for macOS, watchOS, and tvOS 2019-12-17 13:47:39 +13:00
Simon Fairbairn 4c1743ab80 Merge branch 'feature/refactorProcessing' into develop 2019-12-17 13:15:15 +13:00
Simon Fairbairn 35352af0a8 Adds support for changing the string after first initialisation 2019-12-17 13:14:58 +13:00
Simon Fairbairn 1d25236fec Adds performance tests 2019-12-17 13:09:36 +13:00
Simon Fairbairn d95ee23795 Updating bundle 2019-12-17 11:19:16 +13:00
Simon Fairbairn e7d6f0ec35 Fixes #61. Fixes #60. Fixes #59. Fixes #58. Fixes #57. Fixes #56. Fixes #52. Fixes #2. Fixes #15. Fixes #25. Fixes #31. Fixes #51 2019-12-17 11:07:13 +13:00
Simon Fairbairn c88e81a51f Passes (most) of the original tests 2019-12-17 08:28:08 +13:00
Simon Fairbairn 6e6de412c9 Fixes issues with incorrect parsing, adds support for skipping additional processing, other bug fixes 2019-12-17 07:09:45 +13:00
Simon Fairbairn 6d6f83d4b0 Implements new tokenisation engine and updates readme 2019-12-16 20:15:26 +13:00
Simon Fairbairn 9ab2d38c7d Refactors processing to use new line and tokenisation classes 2019-12-16 15:07:49 +13:00
Simon Fairbairn e339833936 Merge branch 'release/0.6.2' into develop 2019-12-07 11:09:25 +13:00
Simon Fairbairn bddcfa43a1 Merge branch 'release/0.6.2' 2019-12-07 11:09:21 +13:00
Simon Fairbairn 12ce9a365f Version Bump 2019-12-07 11:09:14 +13:00
Simon Fairbairn 616ede7202 Updating podspec 2019-12-07 11:09:12 +13:00
Simon Fairbairn b000ab0be5 Updates the podspec 2019-12-07 11:08:35 +13:00
Simon Fairbairn 5ddb58a95d Removes the .swift_version file and updates the podspec 2019-12-07 11:07:44 +13:00
Simon Fairbairn f4520a11b5 Merge branch 'release/0.6.1' into develop 2019-12-07 11:02:56 +13:00
Simon Fairbairn abaac24b16 Merge branch 'release/0.6.1' 2019-12-07 11:02:53 +13:00
Simon Fairbairn 57293f4261 Version Bump 2019-12-07 11:02:43 +13:00
Simon Fairbairn 1e94e474ed Updating podspec 2019-12-07 11:02:40 +13:00
Simon Fairbairn 7ffab1963b Temporarily removes known crashing string from tests 2019-12-07 11:01:47 +13:00
Simon Fairbairn a63fb0dfe4 Merge branch 'feature/iOS13Fixes' into develop 2019-12-07 11:00:19 +13:00
Simon Fairbairn e0e3f9a555 Adds support for Swift 5 and fixes errors around Swift 3 @objc inference 2019-12-07 11:00:11 +13:00
Simon Fairbairn 77b51a9592 Fixes #64 2019-12-07 10:51:58 +13:00
Simon Fairbairn f1ead65825 Updates Gemfile 2019-12-07 10:37:38 +13:00
Simon Fairbairn 7bb5c1232d Merge pull request #65 from SimonFairbairn/dependabot/bundler/rubyzip-1.3.0
Bump rubyzip from 1.2.2 to 1.3.0
2019-12-07 10:34:56 +13:00
Simon Fairbairn 1db338f7da Merge pull request #63 from fchauvin/swiftpm
Swift Package Manager Support
2019-12-07 10:34:13 +13:00
dependabot[bot] 65f1c9a552 Bump rubyzip from 1.2.2 to 1.3.0
Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/rubyzip/rubyzip/releases)
- [Changelog](https://github.com/rubyzip/rubyzip/blob/master/Changelog.md)
- [Commits](https://github.com/rubyzip/rubyzip/compare/v1.2.2...v1.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-30 07:18:58 +00:00
Forrest Chauvin f6d752dec9 Fix Swift Package Manager support 2019-08-02 13:35:39 -04:00
Forrest Chauvin 1a594e6841 Adds swift package manager support 2019-08-02 12:43:56 -04:00
Simon Fairbairn cb2ff2e3ea Updating gemfile 2018-10-11 12:38:58 +01:00
Simon Fairbairn 1fff74b946 Merge branch 'release/0.6.0' into develop 2018-10-11 12:32:58 +01:00
Simon Fairbairn db1c0f7902 Merge branch 'release/0.6.0' 2018-10-11 12:32:52 +01:00
Simon Fairbairn 9a2cd21b15 Fixing rubyzip vulnerability 2018-10-11 12:32:41 +01:00
Simon Fairbairn fdbfbcda54 Version Bump 2018-10-11 12:31:12 +01:00
Simon Fairbairn 04b2f41c00 Updating podspec 2018-10-11 12:31:09 +01:00
Simon Fairbairn 957ec2b442 Updating fastlane 2018-10-11 12:29:58 +01:00
Simon Fairbairn 3848d54e45 Updating to Swift 4.2 2018-10-11 12:29:03 +01:00
Simon Fairbairn 85b1a0b944 Merge branch 'release/0.5.1' into develop 2017-12-04 14:08:41 +04:00
Simon Fairbairn 289925e34d Merge branch 'release/0.5.1' 2017-12-04 14:08:35 +04:00
Simon Fairbairn f8567bd974 Version Bump 2017-12-04 14:08:16 +04:00
Simon Fairbairn 10ed2abd88 Updating podspec 2017-12-04 14:08:13 +04:00
Simon Fairbairn 0e7ede23c3 Updating readme 2017-12-04 14:07:18 +04:00
Simon Fairbairn d316d83848 Merge branch 'master' into release/0.5.1 2017-12-04 13:56:58 +04:00
Simon Fairbairn 3c149a65e7 Updating readme 2017-12-04 13:56:40 +04:00
Simon Fairbairn 7a00026c2d Merge pull request #47 from xzoky/master
Fix String.characters warning
2017-12-04 16:52:16 +07:00
Simon Fairbairn 5f8a6ba7dd Updating Gemfile 2017-12-04 13:48:31 +04:00
Camille Kander 6797288c0e Fix String.characters warning 2017-12-03 23:48:07 -05:00
Simon Fairbairn 96e6ac313c Version Bump 2017-09-24 09:15:40 +08:00
Simon Fairbairn 76f76d8178 Updating podspec 2017-09-24 09:15:36 +08:00
Simon Fairbairn ea0717903e Using new subscript syntax 2017-09-24 09:14:05 +08:00
Simon Fairbairn 845985236a Updating .swift-version to indacte that podspec should use Swift 4 2017-09-24 09:08:39 +08:00
Simon Fairbairn e3e89715f8 Version Bump to 19 2017-09-15 13:55:58 +08:00
Simon Fairbairn b077d32f52 Updating podspec 2017-09-15 13:55:56 +08:00
Simon Fairbairn fc9d8b2646 Updating tests and example 2017-09-15 13:54:38 +08:00
Simon Fairbairn 43f94723c8 Removing line breaks at the end of extra lines. 2017-09-15 13:50:22 +08:00
Simon Fairbairn ea306673e8 Reverting some changes in #43 2017-09-15 13:30:55 +08:00
Simon Fairbairn 15f2a353be Version Bump to 18 2017-09-03 09:16:18 +08:00
Simon Fairbairn e9fed1ced9 Updating podspec 2017-09-03 09:16:16 +08:00
Simon Fairbairn 67297197e8 Merge pull request #44 from enhorn/fix/handle-international-characters
Fixes an erroneous identification of `Alt-H2` headers.
2017-09-03 09:14:23 +08:00
Robin Enhorn 6ee2001d43 Fixes an erroneous identification of Alt-H2 headers. 2017-08-30 17:04:17 +02:00
Simon Fairbairn 4bd6bf498f Version Bump to 17 2017-08-27 09:05:28 +08:00
Simon Fairbairn 1f5a6d4e50 Updating podspec 2017-08-27 09:05:26 +08:00
Simon Fairbairn 04de344a43 Updating project to support latest settings 2017-08-27 09:03:44 +08:00
Simon Fairbairn f386286a7e Adding example.md 2017-08-27 09:01:08 +08:00
Simon Fairbairn 7c07c37f10 Merge pull request #43 from lucasromanomr/master
Add fixed font size for type and support Objc
2017-08-27 08:59:06 +08:00
Lucas Romano 07e9cf69a4 Set font name and font color for all styles 2017-08-24 15:06:05 -03:00
Lucas Romano c86ae343f1 Set font size for all styles 2017-08-24 14:54:21 -03:00
Lucas Romano 170e4b6fa2 add options on asicStyles for objective c 2017-08-24 14:42:35 -03:00
Lucas Romano d62bf1dee9 Ajustes Objc 2017-08-24 13:57:50 -03:00
Lucas Romano c8e6bc6b44 Ajust class to Objc 2017-08-24 13:55:06 -03:00
Lucas Romano 18a1f2922a Transform struct to style in BasicStyle 2017-08-24 13:54:28 -03:00
Lucas Romano 6d1ec33196 Ajusts for public attributes 2017-08-24 13:47:47 -03:00
Lucas Romano 63f489eebe add suport for objc 2017-08-23 17:31:37 -03:00
Lucas Romano 01d153b3ca add fixed font size
add fixed font size for all text
2017-08-23 17:22:55 -03:00
Simon Fairbairn 5148e8ad77 Merge pull request #40 from Nonchalant/feature/fix_markdown
Fix markdown for swift3. Thanks to @Nonchalant
2017-05-04 15:58:43 +08:00
Nonchalant 994af3ad6d fix markdown for swift3 2017-04-24 12:32:13 +09:00
Simon Fairbairn 2b7bfc81d7 Version Bump to 16 2017-04-01 11:48:31 +02:00
Simon Fairbairn aaf8f3618a Updating podspec 2017-04-01 11:48:29 +02:00
Simon Fairbairn a030f210b3 Tidying up 2017-04-01 11:47:39 +02:00
Simon Fairbairn be2eaf522c Fixes issue #14. Fixes issue #33 2017-04-01 11:41:36 +02:00
Simon Fairbairn d48d6b1f90 Version Bump to 15 2017-04-01 10:26:52 +02:00
Simon Fairbairn 100bb3df20 Updating podspec 2017-04-01 10:26:50 +02:00
Simon Fairbairn 29da129a24 Updating gems 2017-04-01 10:24:59 +02:00
Simon Fairbairn 9fcdddf484 Fixing warnings in Xcode 8.3 2017-04-01 10:15:58 +02:00
Simon Fairbairn e653548441 Version Bump 2017-01-21 09:51:30 +02:00
Simon Fairbairn ff0fce7eb7 Updating podspec 2017-01-21 09:51:30 +02:00
Simon Fairbairn 99726212be Rolling back a change made in pr 37 that was causing tests to fail 2017-01-21 09:50:16 +02:00
Simon Fairbairn 0bfeba8188 Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown 2017-01-21 09:44:38 +02:00
Simon Fairbairn 7985b57341 Updating bundle 2017-01-21 09:44:25 +02:00
Simon Fairbairn cb0ecbd67d Merge pull request #37 from qdequele/master
Fix last \n and empty lines. 

Thanks very much for this!
2017-01-21 09:39:11 +02:00
Simon Fairbairn 1d4fb92429 Fixes issue #36 2017-01-21 09:36:51 +02:00
Quentin_dQ 7f276de733 fix the possible error in range on tagFromScanner 2017-01-12 14:02:44 +01:00
Quentin_dQ 5870b838e7 change empty line by one space for NSString.boundingRect parsing 2017-01-06 10:26:49 +01:00
Quentin_dQ b2dc325510 remove \n on the last line 2017-01-06 10:24:32 +01:00
Simon Fairbairn 195f1a5491 Merge pull request #34 from stupergenius/feature/carthage
Adding shared scheme for Carthage
2016-11-23 11:38:08 +01:00
Benjamin Snider cac0c00e0f enabling bitcode. 2016-11-18 20:33:54 -07:00
Benjamin Snider e211af53f5 Setting codesign to developer and enabling WMO on all build types. 2016-11-18 20:16:51 -07:00
Benjamin Snider 15c6b77288 Adding the BITCODE_GENERATION_MODE=bitcode setting for Carthage builds. 2016-11-18 19:49:19 -07:00
Simon Fairbairn 4c0a60faa0 Version Bump 2016-11-04 15:04:18 +01:00
Simon Fairbairn b0bcb587cb Updating podspec 2016-11-04 15:04:18 +01:00
Simon Fairbairn 02837925b9 Updating fastfile 2016-11-04 15:02:13 +01:00
Simon Fairbairn cc2ea97b4c Fixes issue #35 2016-11-04 09:16:46 +01:00
Benjamin Snider 0ab154977a Adding shared scheme for Carthage to pickup. 2016-10-27 15:36:04 -06:00
Simon Fairbairn ac99c2efba Version Bump to 14 2016-10-04 17:59:37 +02:00
Simon Fairbairn 39073280de Updating podspec 2016-10-04 17:59:36 +02:00
Simon Fairbairn 0bca66728f Updating fastfile 2016-10-04 17:58:34 +02:00
Simon Fairbairn 38e2c0d733 Adding Gemfile 2016-10-04 17:56:34 +02:00
Simon Fairbairn f4f7c7bd55 Updating fastfile 2016-10-04 17:54:35 +02:00
Simon Fairbairn cd30189674 Merge branch 'swift3' of https://github.com/SimonFairbairn/SwiftyMarkdown into swift3 2016-10-04 17:49:29 +02:00
Simon Fairbairn b51f13054c Fixing for Swift 3 2016-10-04 17:48:46 +02:00
Simon Fairbairn 6487a98d90 Fixing for Swift 3 2016-08-29 15:05:04 +02:00
Simon Fairbairn b9a01c7f87 Merge pull request #21 from CocoaBob/master
Good to know! Thanks for testing it out!

Added support of tvOS in podspec
2016-06-24 13:44:15 +02:00
CocoaBob c70c5671c8 Update SwiftyMarkdown.podspec 2016-06-23 15:16:05 +02:00
Simon Fairbairn 7bdd5d35ad Merge pull request #18 from mrigdon/patch-1
show that it's on cocoapods in readme
2016-06-02 16:20:59 +02:00
mrigdon 76b27f4571 show that it's on cocoapods in readme 2016-06-01 08:41:17 -07:00
Simon Fairbairn c27ff1d9a5 Merge pull request #13 from ranunez/patch-1
Updates README to specify supported features. Thanks to @ranunez.
2016-04-11 09:21:02 +07:00
Ricardo Nunez 0ff9fe8c1c Updates README to specify supported features 2016-04-08 23:14:50 -07:00
Simon Fairbairn eb63a4ea5e Updating podspec 2016-04-08 10:18:08 +07:00
Simon Fairbairn aa01b6bc58 Version Bump to 13 2016-04-08 10:18:07 +07:00
Simon Fairbairn 37f08f09ae Updating build 2016-04-08 10:17:53 +07:00
Simon Fairbairn c96f17edd0 Merge pull request #10 from mightea/fix-swift3-warnings
Fix swift3 warnings. Thanks to @mightea.
2016-04-08 10:13:41 +07:00
Tobias Herrmann 6288214d26 adds .DS_Store to gitignore 2016-04-07 13:40:03 +02:00
Tobias Herrmann 66f5db3d74 fixes various Swift 3.0 depreciation warnings 2016-04-07 13:38:14 +02:00
Simon Fairbairn 057cf6a4e9 Merge pull request #9 from ravidsrk/patch-1
Added code syntax highlighting to Readme.
2016-04-06 15:45:27 +07:00
Ravindra Kumar 1a4cfe06aa Added code syntax highlighting to Readme 2016-04-04 09:00:16 -04:00
Simon Fairbairn 455a844912 Fixing incorrect homepage 2016-03-23 12:56:32 +07:00
55 changed files with 6143 additions and 1054 deletions
+4
View File
@@ -36,6 +36,7 @@ playground.xcworkspace
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.build/
.swiftpm/
# CocoaPods
#
@@ -61,3 +62,6 @@ Carthage/Build
fastlane/report.xml
fastlane/screenshots
# OS X
.DS_Store
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
# A sample Gemfile
source "https://rubygems.org"
# gem "rails"
gem "cocoapods"
gem 'fastlane'
+219
View File
@@ -0,0 +1,219 @@
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)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.1)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
cocoapods (1.8.4)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.8.4)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 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)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.11.1, < 2.0)
cocoapods-core (1.8.4)
activesupport (>= 4.0.2, < 6)
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)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.1.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)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
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)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.140.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
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)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.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)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-api-client (0.36.4)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.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)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
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)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
json (2.3.0)
jwt (2.1.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)
multipart-post (2.0.0)
nanaimo (0.2.6)
nap (1.1.0)
naturally (2.2.0)
netrc (0.11.0)
os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
ruby-macho (1.4.0)
rubyzip (1.3.0)
security (0.1.3)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.7)
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)
tty-cursor (0.7.1)
tty-screen (0.7.0)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
tzinfo (1.2.6)
thread_safe (~> 0.1)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
word_wrap (1.0.0)
xcodeproj (1.14.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
cocoapods
fastlane
BUNDLED WITH
2.1.4
+18
View File
@@ -0,0 +1,18 @@
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "SwiftyMarkdown",
platforms: [
.iOS(SupportedPlatform.IOSVersion.v11),
.tvOS(SupportedPlatform.TVOSVersion.v11),
.macOS(.v10_12),
.watchOS(.v4)
],
products: [
.library(name: "SwiftyMarkdown", targets: ["SwiftyMarkdown"]),
],
targets: [
.target(name: "SwiftyMarkdown", path: "SwiftyMarkdown"),
]
)
+321 -14
View File
@@ -1,29 +1,336 @@
# SwiftyMarkdown
# SwiftyMarkdown 1.0
SwiftyMarkdown converts Markdown files and strings into NSAttributedString 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
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.
## Usage
- [What's New](#fully-rebuilt-for-2020)
- [Installation](#installation)
- [How to Use](#how-to-use-swiftymarkdown)
- [Screenshot](#screenshot)
- [Appendix](#appendix)
Text string
## Fully Rebuilt For 2020!
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
SwiftyMarkdown now features a more robust and reliable rules-based line processing and character 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 optionally 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.
Support for all of Apple's platforms has been enabled.
## Installation
### CocoaPods:
`pod 'SwiftyMarkdown'`
### SPM:
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
## How To Use SwiftyMarkdown
Read Markdown from a text string...
```swift
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
md.attributedString()
```
...or from a URL.
```swift
if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
md.attributedString()
}
```
URL
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
if let url = NSBundle.mainBundle().URLForResource("file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
md.attributedString()
}
```swift
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")
```
The attributed string can then be assigned to any label or text control that has support for attributed text.
```swift
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
let label = UILabel()
label.attributedText = md.attributedString()
```
## Supported Markdown Features
*italics* or _italics_
**bold** or __bold__
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
`code`
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
Compound rules also work, for example:
It recognises **[Bold Links](http://voyagetravelapps.com/)**
Or [**Bold Links**](http://voyagetravelapps.com/)
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
## Customisation
md.body.fontName = "AvenirNextCondensed-Medium"
Set the attributes of every paragraph and character style type using straightforward dot syntax:
md.h1.color = UIColor.redColor()
md.h1.fontName = "AvenirNextCondensed-Bold"
```swift
md.body.fontName = "AvenirNextCondensed-Medium"
md.italic.color = UIColor.blueColor()
md.h1.color = UIColor.redColor()
md.h1.fontName = "AvenirNextCondensed-Bold"
md.h1.fontSize = 16
md.h1.alignmnent = .center
md.italic.color = UIColor.blueColor()
md.underlineLinks = true
```
On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.
## Screenshot
![Screenshot](http://f.cl.ly/items/12332k3f2s0s0C281h2u/swiftymarkdown.png)
![Screenshot](https://cl.ly/779e6964257a/swiftymarkdown-2020.png)
There's an example project included in the repository. Open the `.xcworkspace` file to get started.
## Appendix
### A) All Customisable Properties
```swift
h1.fontName : String
h1.fontSize : CGFloat
h1.color : UI/NSColor
h1.fontStyle : FontStyle
h1.alignment : NSTextAlignment
h2.fontName : String
h2.fontSize : CGFloat
h2.color : UI/NSColor
h2.fontStyle : FontStyle
h2.alignment : NSTextAlignment
h3.fontName : String
h3.fontSize : CGFloat
h3.color : UI/NSColor
h3.fontStyle : FontStyle
h3.alignment : NSTextAlignment
h4.fontName : String
h4.fontSize : CGFloat
h4.color : UI/NSColor
h4.fontStyle : FontStyle
h4.alignment : NSTextAlignment
h5.fontName : String
h5.fontSize : CGFloat
h5.color : UI/NSColor
h5.fontStyle : FontStyle
h5.alignment : NSTextAlignment
h6.fontName : String
h6.fontSize : CGFloat
h6.color : UI/NSColor
h6.fontStyle : FontStyle
h6.alignment : NSTextAlignment
body.fontName : String
body.fontSize : CGFloat
body.color : UI/NSColor
body.fontStyle : FontStyle
body.alignment : NSTextAlignment
blockquotes.fontName : String
blockquotes.fontSize : CGFloat
blockquotes.color : UI/NSColor
blockquotes.fontStyle : FontStyle
blockquotes.alignment : NSTextAlignment
link.fontName : String
link.fontSize : CGFloat
link.color : UI/NSColor
link.fontStyle : FontStyle
bold.fontName : String
bold.fontSize : CGFloat
bold.color : UI/NSColor
bold.fontStyle : FontStyle
italic.fontName : String
italic.fontSize : CGFloat
italic.color : UI/NSColor
italic.fontStyle : FontStyle
code.fontName : String
code.fontSize : CGFloat
code.color : UI/NSColor
code.fontStyle : FontStyle
underlineLinks : Bool
```
`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.
If you like a bit of chaos:
```swift
md.bold.fontStyle = .italic
md.italic.fontStyle = .bold
```
### B) Advanced Customisation
SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules.
For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:
```swift
enum MarkdownLineStyle : LineStyling {
case h1
case h2
case previousH1
case codeblock
case body
var shouldTokeniseLine: Bool {
switch self {
case .codeblock:
return false
default:
return true
}
}
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
case .previousH1:
return MarkdownLineStyle.h1
default :
return nil
}
}
}
static public var lineRules = [
LineRule(token: " ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)
```
Similarly, the character styles all follow rules:
```swift
enum CharacterStyle : CharacterStyling {
case link, bold, italic, code
}
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)
]
```
#### Rule Subsets
If you want to only support a small subset of Markdown, it's now easy to do.
This example would only process strings with `*` and `_` characters, ignoring links, images, code, and all line-level attributes (headings, blockquotes, etc.)
```swift
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)
]
```
#### Custom Rules
If you wanted to create a rule that applied a style of `Elf` to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:
```swift
enum Characters : CharacterStyling {
case elf
func isEqualTo( _ other : CharacterStyling) -> Bool {
if let other = other as? Characters else {
return false
}
return other == self
}
}
let characterRules = [
CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
]
let processor = SwiftyTokeniser( with : characterRules )
let string = "The elf will speak now: %Here is my elf speaking%"
let tokens = processor.process(string)
```
The output is an array of tokens would be equivalent to:
```swift
[
Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
Token(type: .repeatingTag, inputString: "%", characterStyles: []),
Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
Token(type: .repeatingTag, inputString: "%", characterStyles: [])
]
```
### C) SpriteKit Support
Did you know that `SKLabelNode` supports attributed text? I didn't.
```swift
let smd = SwiftyMarkdown(string: "My Character's **Dialogue**")
let label = SKLabelNode()
label.preferredMaxLayoutWidth = 500
label.numberOfLines = 0
label.attributedText = smd.attributedString()
```
-313
View File
@@ -1,313 +0,0 @@
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 600))
XCPlaygroundPage.currentPage.liveView = containerView
let label = UITextView(frame: containerView.frame)
containerView.addSubview(label)
public protocol FontProperties {
var fontName : String { get set }
var color : UIColor { get set }
}
public struct BasicStyles : FontProperties {
public var fontName = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
public var color = UIColor.blackColor()
}
enum LineType : Int {
case H1, H2, H3, H4, H5, H6, Body, Italic, Bold, Code
}
public class SwiftyMarkdown {
public var h1 = BasicStyles()
public var h2 = BasicStyles()
public var h3 = BasicStyles()
public var h4 = BasicStyles()
public var h5 = BasicStyles()
public var h6 = BasicStyles()
public var body = BasicStyles()
public var link = BasicStyles()
public var italic = BasicStyles()
public var code = BasicStyles()
public var bold = BasicStyles()
let string : String
let instructionSet = NSCharacterSet(charactersInString: "\\*_`")
public init(string : String ) {
self.string = string
}
public init?(url : NSURL ) {
do {
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
} catch {
self.string = ""
fatalError("Couldn't read string")
return nil
}
}
public func attributedString() -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: "")
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
var lineCount = 0
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
var skipLine = false
for line in lines {
lineCount++
if skipLine {
skipLine = false
continue
}
var headingFound = false
for heading in headings {
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
let endHeadingHash = " " + heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
let finalHeadingString = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingHash, withString: "")
// Make Hx where x == current index
let string = attributedStringFromString(finalHeadingString, withType: LineType(rawValue: headings.indexOf(heading)!)!)
attributedString.appendAttributedString(string)
headingFound = true
}
}
if headingFound {
continue
}
if lineCount < lines.count {
let nextLine = lines[lineCount]
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
// Make H1
let string = attributedStringFromString(line, withType: .H1)
attributedString.appendAttributedString(string)
skipLine = true
continue
}
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
// Make H1
let string = attributedStringFromString(line, withType: .H2)
attributedString.appendAttributedString(string)
skipLine = true
continue
}
}
if line.characters.count > 0 {
let scanner = NSScanner(string: line)
scanner.charactersToBeSkipped = nil
while !scanner.atEnd {
var followingString : NSString?
var string : NSString?
// Get all the characters up to the ones we are interested in
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
if let hasString = string as? String {
let bodyString = attributedStringFromString(hasString, withType: .Body)
attributedString.appendAttributedString(bodyString)
var matchedCharacters = self.tagFromScanner(scanner)
let location = scanner.scanLocation
// If the next string after the characters is a space, then add it to the final string and continue
if !scanner.scanUpToString(" ", intoString: nil) {
let charAtts = attributedStringFromString(matchedCharacters, withType: .Body)
attributedString.appendAttributedString(charAtts)
} else {
scanner.scanLocation = location
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
if let hasString = followingString as? String {
let attString : NSAttributedString
if matchedCharacters.containsString("\\") {
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
} else if matchedCharacters == "**" || matchedCharacters == "__" {
attString = attributedStringFromString(hasString, withType: .Bold)
} else {
attString = attributedStringFromString(hasString, withType: .Italic)
}
attributedString.appendAttributedString(attString)
}
matchedCharacters = self.tagFromScanner(scanner)
if matchedCharacters.containsString("\\") {
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
attributedString.appendAttributedString(attString)
}
}
}
} else {
var matchedCharacters = self.tagFromScanner(scanner)
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
if let hasString = followingString as? String {
let attString : NSAttributedString
if matchedCharacters.containsString("\\") {
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
} else if matchedCharacters == "**" || matchedCharacters == "__" {
attString = attributedStringFromString(hasString, withType: .Bold)
} else {
attString = attributedStringFromString(hasString, withType: .Italic)
}
attributedString.appendAttributedString(attString)
}
matchedCharacters = self.tagFromScanner(scanner)
if matchedCharacters.containsString("\\") {
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
attributedString.appendAttributedString(attString)
}
}
}
}
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
}
return attributedString
}
func tagFromScanner( scanner : NSScanner ) -> String {
var matchedCharacters : String = ""
var tempCharacters : NSString?
// Scan the ones we are interested in
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
if let chars = tempCharacters as? String {
matchedCharacters = matchedCharacters + chars
}
}
return matchedCharacters
}
// Make H1
func attributedStringFromString(string : String, withType type : LineType ) -> NSAttributedString {
var attributes : [String : AnyObject]
let textStyle : String
let fontName : String
var appendNewLine = true
switch type {
case .H1:
fontName = h1.fontName
if #available(iOS 9, *) {
textStyle = UIFontTextStyleTitle1
}
attributes = [NSForegroundColorAttributeName : h1.color]
case .H2:
fontName = h2.fontName
textStyle = UIFontTextStyleTitle2
attributes = [NSForegroundColorAttributeName : h2.color]
case .H3:
fontName = h3.fontName
textStyle = UIFontTextStyleTitle3
attributes = [NSForegroundColorAttributeName : h3.color]
case .H4:
fontName = h4.fontName
textStyle = UIFontTextStyleHeadline
attributes = [NSForegroundColorAttributeName : h4.color]
case .H5:
fontName = h5.fontName
textStyle = UIFontTextStyleSubheadline
attributes = [NSForegroundColorAttributeName : h5.color]
case .H6:
fontName = h6.fontName
textStyle = UIFontTextStyleFootnote
attributes = [NSForegroundColorAttributeName : h6.color]
case .Italic:
fontName = italic.fontName
attributes = [NSForegroundColorAttributeName : italic.color]
textStyle = UIFontTextStyleBody
appendNewLine = false
case .Bold:
fontName = bold.fontName
attributes = [NSForegroundColorAttributeName : bold.color]
appendNewLine = false
textStyle = UIFontTextStyleBody
default:
appendNewLine = false
fontName = body.fontName
textStyle = UIFontTextStyleBody
attributes = [NSForegroundColorAttributeName:body.color]
break
}
let font = UIFont.preferredFontForTextStyle(textStyle)
let styleDescriptor = font.fontDescriptor()
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
var finalFont : UIFont
if let font = UIFont(name: fontName, size: styleSize) {
finalFont = font
} else {
finalFont = UIFont.preferredFontForTextStyle(textStyle)
}
let finalFontDescriptor = finalFont.fontDescriptor()
if type == .Italic {
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
}
if type == .Bold {
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
}
attributes[NSFontAttributeName] = finalFont
if appendNewLine {
return NSAttributedString(string: string + "\n", attributes: attributes)
} else {
return NSAttributedString(string: string, attributes: attributes)
}
}
}
if let url = NSBundle.mainBundle().URLForResource("test", withExtension: "md"), md = SwiftyMarkdown(url: url) {
label.attributedText = md.attributedString()
}
@@ -0,0 +1,428 @@
//: [Previous](@previous)
import UIKit
enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
}
enum MarkdownLineStyle : LineStyling {
var shouldTokeniseLine: Bool {
switch self {
case .codeblock:
return false
default:
return true
}
}
case h1
case h2
case h3
case h4
case h5
case h6
case previousH1
case previousH2
case body
case blockquote
case codeblock
case unorderedList
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
case .previousH1:
return MarkdownLineStyle.h1
case .previousH2:
return MarkdownLineStyle.h2
default :
return nil
}
}
}
@objc public protocol FontProperties {
var fontName : String? { get set }
var color : UIColor { get set }
var fontSize : CGFloat { get set }
}
/**
A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
If that is not set, then the system default will be used.
*/
@objc open class BasicStyles : NSObject, FontProperties {
public var fontName : String?
public var color = UIColor.black
public var fontSize : CGFloat = 0.0
}
/// 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 let lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]
static let 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: [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)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
/// The styles to apply to any H1 headers found in the Markdown
open var h1 = BasicStyles()
/// The styles to apply to any H2 headers found in the Markdown
open var h2 = BasicStyles()
/// The styles to apply to any H3 headers found in the Markdown
open var h3 = BasicStyles()
/// The styles to apply to any H4 headers found in the Markdown
open var h4 = BasicStyles()
/// The styles to apply to any H5 headers found in the Markdown
open var h5 = BasicStyles()
/// The styles to apply to any H6 headers found in the Markdown
open var h6 = BasicStyles()
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
open var body = BasicStyles()
/// The styles to apply to any links found in the Markdown
open var link = BasicStyles()
/// The styles to apply to any bold text found in the Markdown
open var bold = BasicStyles()
/// The styles to apply to any italic text found in the Markdown
open var italic = BasicStyles()
/// The styles to apply to any code blocks or inline code text found in the Markdown
open var code = BasicStyles()
var currentType : MarkdownLineStyle = .body
let string : String
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
- returns: An initialized SwiftyMarkdown object
*/
public init(string : String ) {
self.string = string
}
/**
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
- parameter url: The location of the file to read
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
*/
public init?(url : URL ) {
do {
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
} catch {
self.string = ""
return nil
}
}
/**
Set font size for all styles
- parameter size: size of font
*/
open func setFontSizeForAllStyles(with size: CGFloat) {
h1.fontSize = size
h2.fontSize = size
h3.fontSize = size
h4.fontSize = size
h5.fontSize = size
h6.fontSize = size
body.fontSize = size
italic.fontSize = size
code.fontSize = size
link.fontSize = size
}
open func setFontColorForAllStyles(with color: UIColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
code.color = color
link.color = color
}
open func setFontNameForAllStyles(with name: String) {
h1.fontName = name
h2.fontName = name
h3.fontName = name
h4.fontName = name
h5.fontName = name
h6.fontName = name
body.fontName = name
italic.fontName = name
code.fontName = name
link.fontName = name
}
/**
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() -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: "")
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
var strings : [String] = []
for line in foundAttributes {
let finalTokens = self.tokeniser.process(line.line)
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
}
return attributedString
}
}
extension SwiftyMarkdown {
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
let textStyle : UIFont.TextStyle
var fontName : String?
var fontSize : CGFloat?
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
fontName = h1.fontName
fontSize = h1.fontSize
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title1
} else {
textStyle = UIFont.TextStyle.headline
}
case .h2:
fontName = h2.fontName
fontSize = h2.fontSize
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.headline
}
case .h3:
fontName = h3.fontName
fontSize = h3.fontSize
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.subheadline
}
case .h4:
fontName = h4.fontName
fontSize = h4.fontSize
textStyle = UIFont.TextStyle.headline
case .h5:
fontName = h5.fontName
fontSize = h5.fontSize
textStyle = UIFont.TextStyle.subheadline
case .h6:
fontName = h6.fontName
fontSize = h6.fontSize
textStyle = UIFont.TextStyle.footnote
default:
fontName = body.fontName
fontSize = body.fontSize
textStyle = UIFont.TextStyle.body
}
if fontName == nil {
fontName = body.fontName
}
if let characterOverride = characterOverride {
switch characterOverride {
case .code:
fontName = code.fontName ?? fontName
case .link:
fontName = link.fontName ?? fontName
default:
break
}
}
fontSize = fontSize == 0.0 ? nil : fontSize
var font : UIFont
if let existentFontName = fontName {
font = UIFont.preferredFont(forTextStyle: textStyle)
let finalSize : CGFloat
if let existentFontSize = fontSize {
finalSize = existentFontSize
} else {
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
}
if let customFont = UIFont(name: existentFontName, size: finalSize) {
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
font = fontMetrics.scaledFont(for: customFont)
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
return font
}
func color( for line : SwiftyLine ) -> UIColor {
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1, .previousH1:
return h1.color
case .h2, .previousH2:
return h2.color
case .h3:
return h3.color
case .h4:
return h4.color
case .h5:
return h5.color
case .h6:
return h6.color
case .body:
return body.color
case .codeblock:
return code.color
case .blockquote:
return body.color
case .unorderedList:
return body.color
}
}
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
var outputLine = line.line
if let style = line.lineStyle as? MarkdownLineStyle, style == .codeblock {
outputLine = "\t\(outputLine)"
}
var attributes : [NSAttributedString.Key : AnyObject] = [:]
let finalAttributedString = NSMutableAttributedString()
for token in tokens {
var font = self.font(for: line)
attributes[.foregroundColor] = self.color(for: line)
guard let styles = token.characterStyles as? [CharacterStyle] else {
continue
}
if styles.contains(.italic) {
if let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
font = UIFont(descriptor: italicDescriptor, size: 0)
}
}
if styles.contains(.bold) {
if let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: 0)
}
}
attributes[.font] = font
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 styles.contains(.image), let imageName = token.metadataString {
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
continue
}
if styles.contains(.code) {
attributes[.foregroundColor] = self.code.color
attributes[.font] = self.font(for: line, characterOverride: .code)
} else {
// Switch back to previous font
}
let str = NSAttributedString(string: token.outputString, attributes: attributes)
finalAttributedString.append(str)
}
return finalAttributedString
}
}
let image = UIImage(named: "bubble")
let image1Attachment = NSTextAttachment()
image1Attachment.image = image
let att = NSAttributedString(attachment: image1Attachment)
var str = "# Hello, *playground* `code` **bold** ![Image](bubble)"
let md = SwiftyMarkdown(string: str)
md.body.color = .red
md.h1.color = .white
md.h1.fontName = "Noteworthy-Light"
md.link.color = .red
md.code.fontName = "CourierNewPSMT"
md.attributedString()
//: [Next](@next)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,55 @@
import Foundation
enum MarkdownLineStyle : LineStyling {
var shouldTokeniseLine: Bool {
switch self {
case .codeblock:
return false
default:
return true
}
}
case h1
case h2
case h3
case h4
case h5
case h6
case previousH1
case previousH2
case body
case blockquote
case codeblock
case unorderedList
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
case .previousH1:
return MarkdownLineStyle.h1
case .previousH2:
return MarkdownLineStyle.h2
default :
return nil
}
}
}
let rules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]
let lineProcesser = SwiftyLineProcessor(rules: rules, defaultRule: MarkdownLineStyle.body)
print(lineProcesser.process("#### Heading 4 ###").first?.line ?? "")
@@ -0,0 +1,33 @@
//: [Previous](@previous)
import Foundation
import SpriteKit
import PlaygroundSupport
class GameScene : SKScene {
var str = "# Text\n## Speaker 1\nHello, **playground**. *I* don't want to be here, you know. *I* want to be somewhere else."
override func didMove(to view: SKView) {
let md = SwiftyMarkdown(string: str)
md.h2.alignment = .center
md.body.alignment = .center
let label = SKLabelNode(attributedText: md.attributedString())
label.position = CGPoint(x: 100, y: 100)
label.numberOfLines = 0
label.preferredMaxLayoutWidth = 400
label.horizontalAlignmentMode = .left
self.addChild(label)
}
}
let view = SKView(frame: CGRect(x: 0, y: 0, width: 600, height: 500))
let scene = GameScene(size: view.frame.size)
scene.scaleMode = .aspectFit
view.presentScene(scene)
PlaygroundPage.current.liveView = view
//: [Next](@next)
@@ -0,0 +1,7 @@
//: [Previous](@previous)
import Foundation
//: [Next](@next)
@@ -0,0 +1,680 @@
//: [Previous](@previous)
//
// SwiftyTokeniser.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
import os.log
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 {
}
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 {
public let openTag : String
public let intermediateTag : String?
public let closingTag : String?
public let escapeCharacter : Character?
public let styles : [Int : [CharacterStyling]]
public var maxTags : Int = 1
public var spacesAllowed : SpaceAllowed = .oneSide
public var cancels : Cancel = .none
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
self.openTag = openTag
self.intermediateTag = intermediateTag
self.closingTag = closingTag
self.escapeCharacter = escapeCharacter
self.styles = styles
self.maxTags = maxTags
self.cancels = cancels
}
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case processed
case string
case escape
case metadata
}
public struct Token {
public let id = UUID().uuidString
public var type : TokenType
public let inputString : String
public var metadataString : String? = nil
public var characterStyles : [CharacterStyling] = []
public var count : Int = 0
public var shouldSkip : 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 inputString
case .metadata, .processed:
return ""
case .escape, .string:
return inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
}
}
public class SwiftyTokeniser {
let rules : [CharacterRule]
public init( with rules : [CharacterRule] ) {
self.rules = rules
}
public func process( _ inputString : String ) -> [Token] {
guard rules.count > 0 else {
return [Token(type: .string, inputString: inputString)]
}
var currentTokens : [Token] = []
var mutableRules = self.rules
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
if currentTokens.isEmpty {
// This means it's the first time through
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
continue
}
// 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
var replacements : [Int : [Token]] = [:]
for (idx,token) in currentTokens.enumerated() {
switch token.type {
case .string:
if !token.shouldSkip {
let nextTokens = self.scan(token.outputString, with: nextRule)
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
}
default:
break
}
}
// This replaces the individual string tokens with the new token arrays
// making sure to apply any previously found styles to the new tokens.
for key in replacements.keys.sorted(by: { $0 > $1 }) {
let existingToken = currentTokens[key]
var newTokens : [Token] = []
for token in replacements[key]! {
var newToken = token
if existingToken.metadataString != nil {
newToken.metadataString = existingToken.metadataString
}
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
newTokens.append(newToken)
}
currentTokens.replaceSubrange(key...key, with: newTokens)
}
}
return currentTokens
}
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 }) 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 }) else {
return
}
metadataIndex = nextTokenIdx
let styles : [CharacterStyling] = rule.styles[1] ?? []
for i in index..<nextTokenIdx {
for style in styles {
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].type = .metadata
}
}
for i in index..<metadataIndex {
if tokens[i].type == .string {
tokens[i].metadataString = metadataString
}
}
tokens[closeTokenIdx].type = .processed
tokens[metadataIndex].type = .processed
tokens[index].type = .processed
}
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
var mutableTokens : [Token] = tokens
print( tokens.map( { ( $0.outputString, $0.count )}))
for idx in 0..<mutableTokens.count {
let token = mutableTokens[idx]
switch token.type {
case .escape:
print( "Found escape (\(token.inputString))" )
case .repeatingTag:
let theToken = mutableTokens[idx]
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
guard theToken.count > 0 else {
continue
}
let startIdx = idx
var endIdx : Int? = nil
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
endIdx = nextTokenIdx
}
guard let existentEnd = endIdx else {
continue
}
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
for i in startIdx..<existentEnd {
for style in styles {
mutableTokens[i].characterStyles.append(style)
}
if rule.cancels == .allRemaining {
mutableTokens[i].shouldSkip = true
}
}
mutableTokens[idx].count = 0
mutableTokens[existentEnd].count = 0
case .openTag:
let theToken = mutableTokens[idx]
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(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]
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
case .closeTag:
let theToken = mutableTokens[idx]
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
case .string:
let theToken = mutableTokens[idx]
print ("Found String: \(theToken.inputString)" )
if let hasMetadata = theToken.metadataString {
print ("With metadata: \(hasMetadata)" )
}
case .metadata:
let theToken = mutableTokens[idx]
print ("Found metadata: \(theToken.inputString)" )
case .processed:
let theToken = mutableTokens[idx]
print ("Found already processed tag: \(theToken.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, *) {
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, *) {
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
} else {
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
}
let maybeFoundChars : String?
if #available(iOS 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, *) {
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
} else {
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
}
guard let foundChars = maybeFoundChars else {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
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
}
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
}
}
// Example customisation
public enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
}
var str = "A standard paragraph with an *italic*, * spaced asterisk, \\*escaped asterisks\\*, _underscored italics_, \\_escaped underscores\\_, **bold** \\*\\*escaped double asterisks\\*\\*, __underscored bold__, _ spaced underscore \\_\\_escaped double underscores\\_\\_ and a `code block *with an italic that should be ignored*`."
//str = "**AAAA*BB\\*BB*AAAAAA**"
str = "*_*Bold and italic*_*"
str = "*Italic* `Code block with *ignored* italic` __Bold__"
struct TokenTest {
let input : String
let output : String
let tokens : [Token]
}
let challenge1 = TokenTest(input: "*_*italic*_*", output: "italic", tokens: [
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic])
])
let challenge2 = TokenTest(input: "*Italic* `Code block with *ignored* italic` __Bold__", output : "Italic `Code block with *ignored* italic` Bold", tokens : [
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "Code block with *ignored* italic", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
])
let challenge3 = TokenTest(input: " * ", output : " * ", tokens : [
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "*", characterStyles: []),
Token(type: .string, inputString: " ", characterStyles: [])
])
let challenge4 = TokenTest(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [
Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "BB*BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]),
])
let challenge5 = TokenTest(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " __Not Bold__ ", characterStyles: []),
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
])
let challenge6 = TokenTest(input: " *\\** ", output : " *** ", tokens : [
Token(type: .string, inputString: " *** ", characterStyles: [])
])
let challenge7 = TokenTest(input: " *\\**Italic*\\** ", output : " *Italic* ", tokens : [
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "*Italic*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
])
let challenge8 = TokenTest(input: "[*Link*](https://www.neverendingvoyage.com/)", output : "Link", tokens : [
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
])
let challenge9 = TokenTest(input: "`Code (should not be indented)`", output: "Code (should not be indented)", tokens: [
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
])
let challenge10 = 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", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
let oneEscapedAsteriskOneNormalAtStart = 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]),
])
let escapedBoldAtStart = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
])
let escapedBoldWithin = TokenTest(input: "A string with \\*\\*escaped\\*\\* asterisks", output: "A string with **escaped** asterisks", tokens: [
Token(type: .string, inputString: "A string with **escaped** asterisks", characterStyles: [])
])
let oneEscapedAsteriskOneNormalWithin = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
Token(type: .string, inputString: "A string with one *", characterStyles: []),
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
])
let oneEscapedAsteriskTwoNormalWithin = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "** asterisks", characterStyles: [])
])
"Given an array of integers, return **indices** of the two numbers such that they add up to a specific target.\n\nYou may assume that each input would have **_exactly_** one solution, and you may not use the _same_ element twice.\n\n**Example:**\n\nGiven nums = [2, 7, 11, 15], target = 9,\n\nBecause nums[**0**] + nums[**1**] = 2 + 7 = 9,\nreturn [**0**, **1**]."
let challenges = [challenge8]
var images = CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1)
var links = CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1)
var codeblock = CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1)
codeblock.cancels = .allRemaining
let asterisks = CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
let underscores = CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
let scan = SwiftyTokeniser(with: [ images, links, codeblock, asterisks])
for challenge in challenges {
let finalTokens = scan.process(challenge.input)
let stringTokens = finalTokens.filter({ $0.type == .string })
guard stringTokens.count == challenge.tokens.count else {
print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(stringTokens.count)")
print("-------EXPECTED--------")
for token in challenge.tokens {
switch token.type {
case .string:
print("\(token.outputString): \(token.characterStyles)")
default:
print("\(token.outputString)")
}
}
print("-------OUTPUT--------")
for token in finalTokens {
switch token.type {
case .string:
print("\(token.outputString): \(token.characterStyles)")
default:
if !token.outputString.isEmpty {
print("\(token.outputString)")
}
}
}
continue
}
print("-----EXPECTATIONS-----")
for (idx, token) in stringTokens.enumerated() {
let expected = challenge.tokens[idx]
if expected.type != token.type {
print("Failure: Token types are different. Expected: \(expected.type), found: \(token.type)")
}
switch token.type {
case .string:
print("Expected: \(expected.outputString): \(expected.characterStyles)")
print("Found: \(token.outputString): \(token.characterStyles)")
if token.metadataString != nil {
print("Metadata: \(token.metadataString!)")
}
default:
break
}
}
let string = finalTokens.map({ $0.outputString }).joined()
print("-------OUTPUT VS INPUT--------")
print("Input: \(challenge.input)")
print("Expected: \(challenge.output)")
print("Output: \(string)")
}
//: [Next](@next)
@@ -0,0 +1,36 @@
// Code inside modules can be shared between pages and other source files.
import Foundation
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
public extension StringProtocol {
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.lowerBound
}
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.upperBound
}
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
var indices: [Index] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
indices.append(range.lowerBound)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return indices
}
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
@@ -0,0 +1,157 @@
//
// SwiftyLineProcessor.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
public protocol LineStyling {
var shouldTokeniseLine : Bool { get }
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
}
public struct SwiftyLine : CustomStringConvertible {
public let line : String
public let lineStyle : LineStyling
public var description: String {
return self.line
}
}
extension SwiftyLine : Equatable {
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
return lhs.line == rhs.line
}
}
public enum Remove {
case leading
case trailing
case both
case entireLine
case none
}
public enum ChangeApplication {
case current
case previous
}
public struct LineRule {
let token : String
let removeFrom : Remove
let type : LineStyling
let shouldTrim : Bool
let changeAppliesTo : ChangeApplication
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
self.token = token
self.type = type
self.removeFrom = removeFrom
self.shouldTrim = shouldTrim
self.changeAppliesTo = changeAppliesTo
}
}
public class SwiftyLineProcessor {
let defaultType : LineStyling
public var processEmptyStrings : LineStyling?
let lineRules : [LineRule]
public init( rules : [LineRule], defaultRule: LineStyling) {
self.lineRules = rules
self.defaultType = defaultRule
}
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
var output = string
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
output.removeSubrange(output.startIndex..<range)
return output
}
return output
}
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
var output = string
let token = element.token.trimmingCharacters(in: .whitespaces)
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
output.removeSubrange(range..<output.endIndex)
return output
}
return output
}
func processLineLevelAttributes( _ text : String ) -> SwiftyLine {
if text.isEmpty, let style = processEmptyStrings {
return SwiftyLine(line: "", lineStyle: style)
}
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
for element in previousLines {
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
let charSet = CharacterSet(charactersIn: element.token )
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
return SwiftyLine(line: "", lineStyle: element.type)
}
}
for element in lineRules {
guard element.token.count > 0 else {
continue
}
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
let unprocessed = output
switch element.removeFrom {
case .leading:
output = findLeadingLineElement(element, in: output)
case .trailing:
output = findTrailingLineElement(element, in: output)
case .both:
output = findLeadingLineElement(element, in: output)
output = findTrailingLineElement(element, in: output)
default:
break
}
// Only if the output has changed in some way
guard unprocessed != output else {
continue
}
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
return SwiftyLine(line: output, lineStyle: element.type)
}
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
}
public func process( _ string : String ) -> [SwiftyLine] {
var foundAttributes : [SwiftyLine] = []
for heading in string.split(separator: "\n") {
if processEmptyStrings == nil, heading.isEmpty {
continue
}
let input : SwiftyLine
input = processLineLevelAttributes(String(heading))
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
let updatedPrevious = foundAttributes.last!
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
}
continue
}
foundAttributes.append(input)
}
return foundAttributes
}
}
@@ -0,0 +1,496 @@
//
// SwiftyTokeniser.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
import os.log
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 {
}
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 {
public let openTag : String
public let intermediateTag : String?
public let closingTag : String?
public let escapeCharacter : Character?
public let styles : [Int : [CharacterStyling]]
public var maxTags : Int = 1
public var spacesAllowed : SpaceAllowed = .oneSide
public var cancels : Cancel = .none
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
self.openTag = openTag
self.intermediateTag = intermediateTag
self.closingTag = closingTag
self.escapeCharacter = escapeCharacter
self.styles = styles
self.maxTags = maxTags
self.cancels = cancels
}
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case processed
case string
case escape
case metadata
}
public struct Token {
public let id = UUID().uuidString
public var type : TokenType
public let inputString : String
public var metadataString : String? = nil
public var characterStyles : [CharacterStyling] = []
public var count : Int = 0
public var shouldSkip : 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 inputString
case .metadata, .processed:
return ""
case .escape, .string:
return inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
}
}
public class SwiftyTokeniser {
let rules : [CharacterRule]
public init( with rules : [CharacterRule] ) {
self.rules = rules
}
public func process( _ inputString : String ) -> [Token] {
guard rules.count > 0 else {
return [Token(type: .string, inputString: inputString)]
}
var currentTokens : [Token] = []
var mutableRules = self.rules
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
if currentTokens.isEmpty {
// This means it's the first time through
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
continue
}
// 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
var replacements : [Int : [Token]] = [:]
for (idx,token) in currentTokens.enumerated() {
switch token.type {
case .string:
if !token.shouldSkip {
let nextTokens = self.scan(token.outputString, with: nextRule)
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
}
default:
break
}
}
// This replaces the individual string tokens with the new token arrays
// making sure to apply any previously found styles to the new tokens.
for key in replacements.keys.sorted(by: { $0 > $1 }) {
let existingToken = currentTokens[key]
var newTokens : [Token] = []
for token in replacements[key]! {
var newToken = token
if existingToken.metadataString != nil {
newToken.metadataString = existingToken.metadataString
}
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
newTokens.append(newToken)
}
currentTokens.replaceSubrange(key...key, with: newTokens)
}
}
return currentTokens
}
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 }) 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 }) else {
return
}
metadataIndex = nextTokenIdx
let styles : [CharacterStyling] = rule.styles[1] ?? []
for i in index..<nextTokenIdx {
for style in styles {
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].type = .metadata
}
}
for i in index..<metadataIndex {
if tokens[i].type == .string {
tokens[i].metadataString = metadataString
}
}
tokens[closeTokenIdx].type = .processed
tokens[metadataIndex].type = .processed
tokens[index].type = .processed
}
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
var mutableTokens : [Token] = tokens
print( tokens.map( { ( $0.outputString, $0.count )}))
for idx in 0..<mutableTokens.count {
let token = mutableTokens[idx]
switch token.type {
case .escape:
print( "Found escape (\(token.inputString))" )
case .repeatingTag:
let theToken = mutableTokens[idx]
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
guard theToken.count > 0 else {
continue
}
let startIdx = idx
var endIdx : Int? = nil
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
endIdx = nextTokenIdx
}
guard let existentEnd = endIdx else {
continue
}
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
for i in startIdx..<existentEnd {
for style in styles {
mutableTokens[i].characterStyles.append(style)
}
if rule.cancels == .allRemaining {
mutableTokens[i].shouldSkip = true
}
}
mutableTokens[idx].count = 0
mutableTokens[existentEnd].count = 0
case .openTag:
let theToken = mutableTokens[idx]
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(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]
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
case .closeTag:
let theToken = mutableTokens[idx]
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
case .string:
let theToken = mutableTokens[idx]
print ("Found String: \(theToken.inputString)" )
if let hasMetadata = theToken.metadataString {
print ("With metadata: \(hasMetadata)" )
}
case .metadata:
let theToken = mutableTokens[idx]
print ("Found metadata: \(theToken.inputString)" )
case .processed:
let theToken = mutableTokens[idx]
print ("Found already processed tag: \(theToken.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, *) {
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, *) {
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
} else {
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
}
let maybeFoundChars : String?
if #available(iOS 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, *) {
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
} else {
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
}
guard let foundChars = maybeFoundChars else {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
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
}
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
}
}
@@ -0,0 +1,519 @@
import Foundation
import UIKit
enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
}
enum MarkdownLineStyle : LineStyling {
var shouldTokeniseLine: Bool {
switch self {
case .codeblock:
return false
default:
return true
}
}
case h1
case h2
case h3
case h4
case h5
case h6
case previousH1
case previousH2
case body
case blockquote
case codeblock
case unorderedList
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
case .previousH1:
return MarkdownLineStyle.h1
case .previousH2:
return MarkdownLineStyle.h2
default :
return nil
}
}
}
@objc public enum FontStyle : Int {
case normal
case bold
case italic
case boldItalic
}
@objc public protocol FontProperties {
var fontName : String? { get set }
var color : UIColor { get set }
var fontSize : CGFloat { get set }
var fontStyle : FontStyle { get set }
}
@objc public protocol LineProperties {
var alignment : NSTextAlignment { get set }
}
/**
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
If that is not set, then the system default will be used.
*/
@objc open class BasicStyles : NSObject, FontProperties {
public var fontName : String?
public var color = UIColor.black
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
}
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
public var fontName : String?
public var color = UIColor.black
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
public var alignment: NSTextAlignment = .left
}
/// 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 let lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]
static let 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: [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)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
/// The styles to apply to any H1 headers found in the Markdown
open var h1 = LineStyles()
/// The styles to apply to any H2 headers found in the Markdown
open var h2 = LineStyles()
/// The styles to apply to any H3 headers found in the Markdown
open var h3 = LineStyles()
/// The styles to apply to any H4 headers found in the Markdown
open var h4 = LineStyles()
/// The styles to apply to any H5 headers found in the Markdown
open var h5 = LineStyles()
/// The styles to apply to any H6 headers found in the Markdown
open var h6 = LineStyles()
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
open var body = LineStyles()
/// The styles to apply to any blockquotes found in the Markdown
open var blockquotes = LineStyles()
/// The styles to apply to any links found in the Markdown
open var link = BasicStyles()
/// The styles to apply to any bold text found in the Markdown
open var bold = BasicStyles()
/// The styles to apply to any italic text found in the Markdown
open var italic = BasicStyles()
/// The styles to apply to any code blocks or inline code text found in the Markdown
open var code = BasicStyles()
public var underlineLinks : Bool = false
var currentType : MarkdownLineStyle = .body
let string : String
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
- returns: An initialized SwiftyMarkdown object
*/
public init(string : String ) {
self.string = string
super.init()
if #available(iOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
}
/**
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
- parameter url: The location of the file to read
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
*/
public init?(url : URL ) {
do {
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
} catch {
self.string = ""
return nil
}
super.init()
if #available(iOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
}
/**
Set font size for all styles
- parameter size: size of font
*/
open func setFontSizeForAllStyles(with size: CGFloat) {
h1.fontSize = size
h2.fontSize = size
h3.fontSize = size
h4.fontSize = size
h5.fontSize = size
h6.fontSize = size
body.fontSize = size
italic.fontSize = size
bold.fontSize = size
code.fontSize = size
link.fontSize = size
link.fontSize = size
}
open func setFontColorForAllStyles(with color: UIColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
bold.color = color
code.color = color
link.color = color
blockquotes.color = color
}
open func setFontNameForAllStyles(with name: String) {
h1.fontName = name
h2.fontName = name
h3.fontName = name
h4.fontName = name
h5.fontName = name
h6.fontName = name
body.fontName = name
italic.fontName = name
bold.fontName = name
code.fontName = name
link.fontName = name
blockquotes.fontName = name
}
/**
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() -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: "")
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
for line in foundAttributes {
let finalTokens = self.tokeniser.process(line.line)
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
attributedString.append(NSAttributedString(string: "\n"))
}
return attributedString
}
}
extension SwiftyMarkdown {
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
let textStyle : UIFont.TextStyle
var fontName : String?
var fontSize : CGFloat?
var globalBold = false
var globalItalic = false
let style : FontProperties
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
style = self.h1
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title1
} else {
textStyle = UIFont.TextStyle.headline
}
case .h2:
style = self.h2
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.headline
}
case .h3:
style = self.h3
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.subheadline
}
case .h4:
style = self.h4
textStyle = UIFont.TextStyle.headline
case .h5:
style = self.h5
textStyle = UIFont.TextStyle.subheadline
case .h6:
style = self.h6
textStyle = UIFont.TextStyle.footnote
case .codeblock:
style = self.code
textStyle = UIFont.TextStyle.body
case .blockquote:
style = self.blockquotes
textStyle = UIFont.TextStyle.body
default:
style = self.body
textStyle = UIFont.TextStyle.body
}
fontName = style.fontName
fontSize = style.fontSize
switch style.fontStyle {
case .bold:
globalBold = true
case .italic:
globalItalic = true
case .boldItalic:
globalItalic = true
globalBold = true
case .normal:
break
}
if fontName == nil {
fontName = body.fontName
}
if let characterOverride = characterOverride {
switch characterOverride {
case .code:
fontName = code.fontName ?? fontName
fontSize = code.fontSize
case .link:
fontName = link.fontName ?? fontName
fontSize = link.fontSize
case .bold:
fontName = bold.fontName ?? fontName
fontSize = bold.fontSize
globalBold = true
case .italic:
fontName = italic.fontName ?? fontName
fontSize = italic.fontSize
globalItalic = true
default:
break
}
}
fontSize = fontSize == 0.0 ? nil : fontSize
var font : UIFont
if let existentFontName = fontName {
font = UIFont.preferredFont(forTextStyle: textStyle)
let finalSize : CGFloat
if let existentFontSize = fontSize {
finalSize = existentFontSize
} else {
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
}
if let customFont = UIFont(name: existentFontName, size: finalSize) {
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
font = fontMetrics.scaledFont(for: customFont)
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
font = UIFont(descriptor: italicDescriptor, size: 0)
}
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: 0)
}
return font
}
func color( for line : SwiftyLine ) -> UIColor {
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1, .previousH1:
return h1.color
case .h2, .previousH2:
return h2.color
case .h3:
return h3.color
case .h4:
return h4.color
case .h5:
return h5.color
case .h6:
return h6.color
case .body:
return body.color
case .codeblock:
return code.color
case .blockquote:
return blockquotes.color
case .unorderedList:
return body.color
}
}
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
var finalTokens = tokens
let finalAttributedString = NSMutableAttributedString()
var attributes : [NSAttributedString.Key : AnyObject] = [:]
let lineProperties : LineProperties
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
lineProperties = self.h1
case .h2:
lineProperties = self.h2
case .h3:
lineProperties = self.h3
case .h4:
lineProperties = self.h4
case .h5:
lineProperties = self.h5
case .h6:
lineProperties = self.h6
case .codeblock:
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .blockquote:
lineProperties = self.blockquotes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .unorderedList:
lineProperties = body
finalTokens.insert(Token(type: .string, inputString: ""), at: 0)
default:
lineProperties = body
break
}
if lineProperties.alignment != .left {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = lineProperties.alignment
attributes[.paragraphStyle] = paragraphStyle
}
for token in finalTokens {
attributes[.font] = self.font(for: line)
attributes[.foregroundColor] = self.color(for: line)
guard let styles = token.characterStyles as? [CharacterStyle] else {
continue
}
if styles.contains(.italic) {
attributes[.font] = self.font(for: line, characterOverride: .italic)
attributes[.foregroundColor] = self.italic.color
}
if styles.contains(.bold) {
attributes[.font] = self.font(for: line, characterOverride: .bold)
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 styles.contains(.image), let imageName = token.metadataString {
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
continue
}
if styles.contains(.code) {
attributes[.foregroundColor] = self.code.color
attributes[.font] = self.font(for: line, characterOverride: .code)
} else {
// Switch back to previous font
}
let str = NSAttributedString(string: token.outputString, attributes: attributes)
finalAttributedString.append(str)
}
return finalAttributedString
}
}
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios'>
<timeline fileName='timeline.xctimeline'/>
<playground version='6.0' target-platform='ios' last-migration='1120'>
<pages>
<page name='Scanner Tests'/>
<page name='Line Processing'/>
<page name='Tokenising'/>
<page name='Attributed String'/>
</pages>
</playground>
+8 -3
View File
@@ -1,16 +1,21 @@
Pod::Spec.new do |s|
s.name = "SwiftyMarkdown"
s.version = "0.2.2"
s.version = "1.0.2"
s.summary = "Converts Markdown to NSAttributed String"
s.homepage = "https://github.com/SimonFairbairn/Stormcloud"
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
s.license = 'MIT'
s.author = { "Simon Fairbairn" => "simon@voyagetravelapps.com" }
s.source = { :git => "https://github.com/SimonFairbairn/SwiftyMarkdown.git", :tag => s.version }
s.social_media_url = 'https://twitter.com/SimonFairbairn'
s.ios.deployment_target = '8.0'
s.ios.deployment_target = "11.0"
s.tvos.deployment_target = "11.0"
s.osx.deployment_target = "10.12"
s.watchos.deployment_target = "4.0"
s.requires_arc = true
s.source_files = 'SwiftyMarkdown/'
s.swift_version = "5.0"
end
+102 -14
View File
@@ -7,9 +7,19 @@
objects = {
/* Begin PBXBuildFile section */
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */; };
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */; };
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */; };
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */; };
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */; };
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */; };
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */; };
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */; };
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */; };
F4A48ABB23A8521C00713437 /* test.md in Resources */ = {isa = PBXBuildFile; fileRef = F4A48ABA23A8518900713437 /* test.md */; };
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */; settings = {ATTRIBUTES = (Public, ); }; };
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; };
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */; };
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */; };
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */; };
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E61C8AF01300D735C1 /* LICENSE */; };
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */; };
@@ -26,16 +36,27 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = "<group>"; };
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = "<group>"; };
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = "<group>"; };
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdownAttributedStringTests copy.swift"; sourceTree = "<group>"; };
F4A48ABA23A8518900713437 /* test.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = test.md; sourceTree = "<group>"; };
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyMarkdown.h; sourceTree = "<group>"; };
F4CE98861C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownTests.swift; sourceTree = "<group>"; };
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = "<group>"; };
F4CE98921C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
F4CE98E61C8AF01300D735C1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
F4CE98E71C8AF01300D735C1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
F4FEF07423E13365007219EF /* metadataTest.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = metadataTest.md; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -84,6 +105,11 @@
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */,
F4CE98861C8A921300D735C1 /* Info.plist */,
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */,
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */,
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */,
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */,
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */,
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */,
);
path = SwiftyMarkdown;
sourceTree = "<group>";
@@ -91,8 +117,14 @@
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */ = {
isa = PBXGroup;
children = (
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */,
F4A48ABA23A8518900713437 /* test.md */,
F4FEF07423E13365007219EF /* metadataTest.md */,
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */,
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */,
F4CE98921C8A921300D735C1 /* Info.plist */,
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */,
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */,
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */,
);
path = SwiftyMarkdownTests;
sourceTree = "<group>";
@@ -154,23 +186,27 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 0720;
LastUpgradeCheck = 1120;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4CE98801C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
};
F4CE988A1C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
DevelopmentTeam = 52T262DA8V;
LastSwiftMigration = 1120;
};
};
};
buildConfigurationList = F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = F4CE98771C8A921300D735C1;
productRefGroup = F4CE98821C8A921300D735C1 /* Products */;
@@ -197,6 +233,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4A48ABB23A8521C00713437 /* test.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -207,7 +244,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */,
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */,
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */,
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */,
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */,
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -215,7 +257,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */,
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */,
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */,
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */,
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */,
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -238,18 +284,28 @@
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -267,7 +323,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -286,18 +342,28 @@
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -309,9 +375,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -322,57 +389,78 @@
F4CE98961C8A921300D735C1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BITCODE_GENERATION_MODE = bitcode;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 12;
DYLIB_CURRENT_VERSION = 19;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "-fembed-bitcode";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F4CE98971C8A921300D735C1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BITCODE_GENERATION_MODE = bitcode;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 12;
DYLIB_CURRENT_VERSION = 19;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "-fembed-bitcode";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Release;
};
F4CE98991C8A921300D735C1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = 52T262DA8V;
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F4CE989A1C8A921300D735C1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = 52T262DA8V;
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,32 @@
<?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.01346</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.0064581</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,46 @@
<?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>
<key>maxPercentRelativeStandardDeviation</key>
<real>5</real>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.01</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
<key>maxPercentRelativeStandardDeviation</key>
<real>5</real>
</dict>
</dict>
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0392</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,71 @@
<?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>runDestinationsByUUID</key>
<dict>
<key>3EC61D89-CC18-40D5-9AC6-F4504ECE42B5</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_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone10,6</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
<key>8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4</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_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone11,8</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE988A1C8A921300D735C1"
BuildableName = "SwiftyMarkdownTests.xctest"
BlueprintName = "SwiftyMarkdownTests"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+2 -2
View File
@@ -15,11 +15,11 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.2.1</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>12</string>
<string>19</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
@@ -0,0 +1,44 @@
//
// String+SwiftyMarkdown.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 08/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
/// Some helper functions based on this:
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
extension StringProtocol {
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.lowerBound
}
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.upperBound
}
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
var indices: [Index] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
indices.append(range.lowerBound)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return indices
}
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
+176
View File
@@ -0,0 +1,176 @@
//
// SwiftyLineProcessor.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
public protocol LineStyling {
var shouldTokeniseLine : Bool { get }
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
}
public struct SwiftyLine : CustomStringConvertible {
public let line : String
public let lineStyle : LineStyling
public var description: String {
return self.line
}
}
extension SwiftyLine : Equatable {
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
return lhs.line == rhs.line
}
}
public enum Remove {
case leading
case trailing
case both
case entireLine
case none
}
public enum ChangeApplication {
case current
case previous
case untilClose
}
public struct LineRule {
let token : String
let removeFrom : Remove
let type : LineStyling
let shouldTrim : Bool
let changeAppliesTo : ChangeApplication
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
self.token = token
self.type = type
self.removeFrom = removeFrom
self.shouldTrim = shouldTrim
self.changeAppliesTo = changeAppliesTo
}
}
public class SwiftyLineProcessor {
var closeToken : String? = nil
let defaultType : LineStyling
public var processEmptyStrings : LineStyling?
let lineRules : [LineRule]
public init( rules : [LineRule], defaultRule: LineStyling) {
self.lineRules = rules
self.defaultType = defaultRule
}
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
var output = string
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
output.removeSubrange(output.startIndex..<range)
return output
}
return output
}
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
var output = string
let token = element.token.trimmingCharacters(in: .whitespaces)
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
output.removeSubrange(range..<output.endIndex)
return output
}
return output
}
func processLineLevelAttributes( _ text : String ) -> SwiftyLine? {
if text.isEmpty, let style = processEmptyStrings {
return SwiftyLine(line: "", lineStyle: style)
}
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
for element in lineRules {
guard element.token.count > 0 else {
continue
}
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
let unprocessed = output
if let hasToken = self.closeToken, unprocessed != hasToken {
return nil
}
switch element.removeFrom {
case .leading:
output = findLeadingLineElement(element, in: output)
case .trailing:
output = findTrailingLineElement(element, in: output)
case .both:
output = findLeadingLineElement(element, in: output)
output = findTrailingLineElement(element, in: output)
case .entireLine:
let maybeOutput = output.replacingOccurrences(of: element.token, with: "")
output = ( maybeOutput.isEmpty ) ? maybeOutput : output
default:
break
}
// Only if the output has changed in some way
guard unprocessed != output else {
continue
}
if element.changeAppliesTo == .untilClose {
self.closeToken = (self.closeToken == nil) ? element.token : nil
return nil
}
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
return SwiftyLine(line: output, lineStyle: element.type)
}
for element in previousLines {
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
let charSet = CharacterSet(charactersIn: element.token )
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
return SwiftyLine(line: "", lineStyle: element.type)
}
}
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
}
public func process( _ string : String ) -> [SwiftyLine] {
var foundAttributes : [SwiftyLine] = []
for heading in string.components(separatedBy: CharacterSet.newlines) {
if processEmptyStrings == nil && heading.isEmpty {
continue
}
guard let input = processLineLevelAttributes(String(heading)) else {
continue
}
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
let updatedPrevious = foundAttributes.last!
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
}
continue
}
foundAttributes.append(input)
}
return foundAttributes
}
}
+169
View File
@@ -0,0 +1,169 @@
//
// SwiftyMarkdown+macOS.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
#if !os(macOS)
import UIKit
extension SwiftyMarkdown {
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
let textStyle : UIFont.TextStyle
var fontName : String?
var fontSize : CGFloat?
var globalBold = false
var globalItalic = false
let style : FontProperties
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
style = self.h1
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title1
} else {
textStyle = UIFont.TextStyle.headline
}
case .h2:
style = self.h2
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.headline
}
case .h3:
style = self.h3
if #available(iOS 9, *) {
textStyle = UIFont.TextStyle.title2
} else {
textStyle = UIFont.TextStyle.subheadline
}
case .h4:
style = self.h4
textStyle = UIFont.TextStyle.headline
case .h5:
style = self.h5
textStyle = UIFont.TextStyle.subheadline
case .h6:
style = self.h6
textStyle = UIFont.TextStyle.footnote
case .codeblock:
style = self.code
textStyle = UIFont.TextStyle.body
case .blockquote:
style = self.blockquotes
textStyle = UIFont.TextStyle.body
default:
style = self.body
textStyle = UIFont.TextStyle.body
}
fontName = style.fontName
fontSize = style.fontSize
switch style.fontStyle {
case .bold:
globalBold = true
case .italic:
globalItalic = true
case .boldItalic:
globalItalic = true
globalBold = true
case .normal:
break
}
if fontName == nil {
fontName = body.fontName
}
if let characterOverride = characterOverride {
switch characterOverride {
case .code:
fontName = code.fontName ?? fontName
fontSize = code.fontSize
case .link:
fontName = link.fontName ?? fontName
fontSize = link.fontSize
case .bold:
fontName = bold.fontName ?? fontName
fontSize = bold.fontSize
globalBold = true
case .italic:
fontName = italic.fontName ?? fontName
fontSize = italic.fontSize
globalItalic = true
default:
break
}
}
fontSize = fontSize == 0.0 ? nil : fontSize
var font : UIFont
if let existentFontName = fontName {
font = UIFont.preferredFont(forTextStyle: textStyle)
let finalSize : CGFloat
if let existentFontSize = fontSize {
finalSize = existentFontSize
} else {
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
}
if let customFont = UIFont(name: existentFontName, size: finalSize) {
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
font = fontMetrics.scaledFont(for: customFont)
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
} else {
font = UIFont.preferredFont(forTextStyle: textStyle)
}
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
font = UIFont(descriptor: italicDescriptor, size: 0)
}
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: 0)
}
return font
}
func color( for line : SwiftyLine ) -> UIColor {
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .yaml:
return body.color
case .h1, .previousH1:
return h1.color
case .h2, .previousH2:
return h2.color
case .h3:
return h3.color
case .h4:
return h4.color
case .h5:
return h5.color
case .h6:
return h6.color
case .body:
return body.color
case .codeblock:
return code.color
case .blockquote:
return blockquotes.color
case .unorderedList:
return body.color
}
}
}
#endif
+144
View File
@@ -0,0 +1,144 @@
//
// SwiftyMarkdown+macOS.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
#if os(macOS)
import AppKit
extension SwiftyMarkdown {
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont {
var fontName : String?
var fontSize : CGFloat?
var globalBold = false
var globalItalic = false
let style : FontProperties
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
style = self.h1
case .h2:
style = self.h2
case .h3:
style = self.h3
case .h4:
style = self.h4
case .h5:
style = self.h5
case .h6:
style = self.h6
case .codeblock:
style = self.code
case .blockquote:
style = self.blockquotes
default:
style = self.body
}
fontName = style.fontName
fontSize = style.fontSize
switch style.fontStyle {
case .bold:
globalBold = true
case .italic:
globalItalic = true
case .boldItalic:
globalItalic = true
globalBold = true
case .normal:
break
}
if fontName == nil {
fontName = body.fontName
}
if let characterOverride = characterOverride {
switch characterOverride {
case .code:
fontName = code.fontName ?? fontName
fontSize = code.fontSize
case .link:
fontName = link.fontName ?? fontName
fontSize = link.fontSize
case .bold:
fontName = bold.fontName ?? fontName
fontSize = bold.fontSize
globalBold = true
case .italic:
fontName = italic.fontName ?? fontName
fontSize = italic.fontSize
globalItalic = true
default:
break
}
}
fontSize = fontSize == 0.0 ? nil : fontSize
let finalSize : CGFloat
if let existentFontSize = fontSize {
finalSize = existentFontSize
} else {
finalSize = NSFont.systemFontSize
}
var font : NSFont
if let existentFontName = fontName {
if let customFont = NSFont(name: existentFontName, size: finalSize) {
font = customFont
} else {
font = NSFont.systemFont(ofSize: finalSize)
}
} else {
font = NSFont.systemFont(ofSize: finalSize)
}
if globalItalic {
let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic)
font = NSFont(descriptor: italicDescriptor, size: 0) ?? font
}
if globalBold {
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
font = NSFont(descriptor: boldDescriptor, size: 0) ?? font
}
return font
}
func color( for line : SwiftyLine ) -> NSColor {
// What type are we and is there a font name set?
switch line.lineStyle as! MarkdownLineStyle {
case .h1, .previousH1:
return h1.color
case .h2, .previousH2:
return h2.color
case .h3:
return h3.color
case .h4:
return h4.color
case .h5:
return h5.color
case .h6:
return h6.color
case .body:
return body.color
case .codeblock:
return code.color
case .blockquote:
return blockquotes.color
case .unorderedList:
return body.color
case .yaml:
return body.color
}
}
}
#endif
+3 -3
View File
@@ -6,13 +6,13 @@
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for SwiftyMarkdown.
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
//FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
//! Project version string for SwiftyMarkdown.
FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
//FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SwiftyMarkdown/PublicHeader.h>
+352 -303
View File
@@ -6,93 +6,199 @@
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
#if os(macOS)
import AppKit
#else
import UIKit
#endif
enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
func isEqualTo(_ other: CharacterStyling) -> Bool {
guard let other = other as? CharacterStyle else {
return false
}
return other == self
}
}
public protocol FontProperties {
enum MarkdownLineStyle : LineStyling {
var shouldTokeniseLine: Bool {
switch self {
case .codeblock:
return false
default:
return true
}
}
case yaml
case h1
case h2
case h3
case h4
case h5
case h6
case previousH1
case previousH2
case body
case blockquote
case codeblock
case unorderedList
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
switch self {
case .previousH1:
return MarkdownLineStyle.h1
case .previousH2:
return MarkdownLineStyle.h2
default :
return nil
}
}
}
@objc public enum FontStyle : Int {
case normal
case bold
case italic
case boldItalic
}
#if os(macOS)
@objc public protocol FontProperties {
var fontName : String? { get set }
var color : NSColor { get set }
var fontSize : CGFloat { get set }
var fontStyle : FontStyle { get set }
}
#else
@objc public protocol FontProperties {
var fontName : String? { get set }
var color : UIColor { get set }
var fontSize : CGFloat { get set }
var fontStyle : FontStyle { get set }
}
#endif
@objc public protocol LineProperties {
var alignment : NSTextAlignment { get set }
}
/**
A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
If that is not set, then the system default will be used.
*/
public struct BasicStyles : FontProperties {
public var fontName : String? = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
public var color = UIColor.blackColor()
@objc open class BasicStyles : NSObject, FontProperties {
public var fontName : String?
#if os(macOS)
public var color = NSColor.black
#else
public var color = UIColor.black
#endif
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
}
enum LineType : Int {
case H1, H2, H3, H4, H5, H6, Body
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
public var fontName : String?
#if os(macOS)
public var color = NSColor.black
#else
public var color = UIColor.black
#endif
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
public var alignment: NSTextAlignment = .left
}
enum LineStyle : Int {
case None
case Italic
case Bold
case Code
case Link
static func styleFromString(string : String ) -> LineStyle {
if string == "**" || string == "__" {
return .Bold
} else if string == "*" || string == "_" {
return .Italic
} else if string == "`" {
return .Code
} else if string == "[" {
return .Link
} else {
return .None
}
}
}
/// 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.
public class SwiftyMarkdown {
@objc open class SwiftyMarkdown: NSObject {
static public var lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
]
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: [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)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
/// The styles to apply to any H1 headers found in the Markdown
public var h1 = BasicStyles()
open var h1 = LineStyles()
/// The styles to apply to any H2 headers found in the Markdown
public var h2 = BasicStyles()
open var h2 = LineStyles()
/// The styles to apply to any H3 headers found in the Markdown
public var h3 = BasicStyles()
open var h3 = LineStyles()
/// The styles to apply to any H4 headers found in the Markdown
public var h4 = BasicStyles()
open var h4 = LineStyles()
/// The styles to apply to any H5 headers found in the Markdown
public var h5 = BasicStyles()
open var h5 = LineStyles()
/// The styles to apply to any H6 headers found in the Markdown
public var h6 = BasicStyles()
open var h6 = LineStyles()
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
public var body = BasicStyles()
open var body = LineStyles()
/// The styles to apply to any blockquotes found in the Markdown
open var blockquotes = LineStyles()
/// The styles to apply to any links found in the Markdown
public var link = BasicStyles()
open var link = BasicStyles()
/// The styles to apply to any bold text found in the Markdown
public var bold = BasicStyles()
open var bold = BasicStyles()
/// The styles to apply to any italic text found in the Markdown
public var italic = BasicStyles()
open var italic = BasicStyles()
/// The styles to apply to any code blocks or inline code text found in the Markdown
public var code = BasicStyles()
open var code = BasicStyles()
var currentType : LineType = .Body
public var bullet : String = ""
let string : String
let instructionSet = NSCharacterSet(charactersInString: "[\\*_`")
public var underlineLinks : Bool = false
var currentType : MarkdownLineStyle = .body
var string : String
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
/**
@@ -102,6 +208,14 @@ public class SwiftyMarkdown {
*/
public init(string : String ) {
self.string = string
super.init()
#if os(macOS)
self.setFontColorForAllStyles(with: .labelColor)
#elseif !os(watchOS)
if #available(iOS 13.0, tvOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
#endif
}
/**
@@ -111,297 +225,232 @@ public class SwiftyMarkdown {
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
*/
public init?(url : NSURL ) {
public init?(url : URL ) {
do {
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
} catch {
self.string = ""
fatalError("Couldn't read string")
return nil
}
super.init()
#if os(macOS)
self.setFontColorForAllStyles(with: .labelColor)
#elseif !os(watchOS)
if #available(iOS 13.0, tvOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
#endif
}
/**
Set font size for all styles
- parameter size: size of font
*/
open func setFontSizeForAllStyles(with size: CGFloat) {
h1.fontSize = size
h2.fontSize = size
h3.fontSize = size
h4.fontSize = size
h5.fontSize = size
h6.fontSize = size
body.fontSize = size
italic.fontSize = size
bold.fontSize = size
code.fontSize = size
link.fontSize = size
link.fontSize = size
}
#if os(macOS)
open func setFontColorForAllStyles(with color: NSColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
bold.color = color
code.color = color
link.color = color
blockquotes.color = color
}
#else
open func setFontColorForAllStyles(with color: UIColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
bold.color = color
code.color = color
link.color = color
blockquotes.color = color
}
#endif
open func setFontNameForAllStyles(with name: String) {
h1.fontName = name
h2.fontName = name
h3.fontName = name
h4.fontName = name
h5.fontName = name
h6.fontName = name
body.fontName = name
italic.fontName = name
bold.fontName = name
code.fontName = name
link.fontName = name
blockquotes.fontName = name
}
/**
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
*/
public func attributedString() -> NSAttributedString {
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
if let existentMarkdownString = markdownString {
self.string = existentMarkdownString
}
let attributedString = NSMutableAttributedString(string: "")
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
var lineCount = 0
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
var skipLine = false
for theLine in lines {
lineCount++
if skipLine {
skipLine = false
continue
}
var line = theLine
for heading in headings {
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
// Remove ending
let endHeadingString = heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
line = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingString, withString: "").stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
currentType = LineType(rawValue: headings.indexOf(heading)!)!
// We found a heading so break out of the inner loop
break
}
for (idx, line) in foundAttributes.enumerated() {
if idx > 0 {
attributedString.append(NSAttributedString(string: "\n"))
}
let finalTokens = self.tokeniser.process(line.line)
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
// Look for underlined headings
if lineCount < lines.count {
let nextLine = lines[lineCount]
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
// Make H1
currentType = .H1
// We need to skip the next line
skipLine = true
}
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
// Make H2
currentType = .H2
// We need to skip the next line
skipLine = true
}
}
// If this is not an empty line...
if line.characters.count > 0 {
// ...start scanning
let scanner = NSScanner(string: line)
// We want to be aware of spaces
scanner.charactersToBeSkipped = nil
while !scanner.atEnd {
var string : NSString?
// Get all the characters up to the ones we are interested in
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
if let hasString = string as? String {
let bodyString = attributedStringFromString(hasString, withStyle: .None)
attributedString.appendAttributedString(bodyString)
let location = scanner.scanLocation
let matchedCharacters = tagFromScanner(scanner).foundCharacters
// If the next string after the characters is a space, then add it to the final string and continue
let set = NSMutableCharacterSet.whitespaceCharacterSet()
set.formUnionWithCharacterSet(NSCharacterSet.punctuationCharacterSet())
if scanner.scanUpToCharactersFromSet(set, intoString: nil) {
scanner.scanLocation = location
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
} else if matchedCharacters == "[" {
scanner.scanLocation = location
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
} else {
let charAtts = attributedStringFromString(matchedCharacters, withStyle: .None)
attributedString.appendAttributedString(charAtts)
}
}
} else {
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner, atStartOfLine: true))
}
}
}
// Append a new line character to the end of the processed line
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
currentType = .Body
}
return attributedString
}
func attributedStringFromScanner( scanner : NSScanner, atStartOfLine start : Bool = false) -> NSAttributedString {
var followingString : NSString?
}
let results = self.tagFromScanner(scanner)
var style = LineStyle.styleFromString(results.foundCharacters)
var attributes = [String : AnyObject]()
if style == .Link {
var linkText : NSString?
var linkURL : NSString?
let linkCharacters = NSCharacterSet(charactersInString: "]()")
scanner.scanUpToCharactersFromSet(linkCharacters, intoString: &linkText)
scanner.scanCharactersFromSet(linkCharacters, intoString: nil)
scanner.scanUpToCharactersFromSet(linkCharacters, intoString: &linkURL)
scanner.scanCharactersFromSet(linkCharacters, intoString: nil)
if let hasLink = linkText, hasURL = linkURL {
followingString = hasLink as String
attributes[NSLinkAttributeName] = hasURL as String
} else {
style = .None
}
} else {
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
}
let attributedString = attributedStringFromString(results.escapedCharacters, withStyle: style).mutableCopy() as! NSMutableAttributedString
if let hasString = followingString as? String {
let prefix = ( style == .Code && start ) ? "\t" : ""
let attString = attributedStringFromString(prefix + hasString, withStyle: style, attributes: attributes)
attributedString.appendAttributedString(attString)
}
let suffix = self.tagFromScanner(scanner)
attributedString.appendAttributedString(attributedStringFromString(suffix.escapedCharacters, withStyle: style))
return attributedString
}
extension SwiftyMarkdown {
func tagFromScanner( scanner : NSScanner ) -> (foundCharacters : String, escapedCharacters : String) {
var matchedCharacters : String = ""
var tempCharacters : NSString?
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
// Scan the ones we are interested in
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
if let chars = tempCharacters as? String {
matchedCharacters = matchedCharacters + chars
}
}
var foundCharacters : String = ""
while matchedCharacters.containsString("\\") {
if let hasRange = matchedCharacters.rangeOfString("\\") {
let newRange = Range(start: hasRange.startIndex, end: hasRange.endIndex.advancedBy(1))
foundCharacters = foundCharacters + matchedCharacters.substringWithRange(newRange)
var finalTokens = tokens
let finalAttributedString = NSMutableAttributedString()
var attributes : [NSAttributedString.Key : AnyObject] = [:]
matchedCharacters.removeRange(newRange)
}
let lineProperties : LineProperties
switch line.lineStyle as! MarkdownLineStyle {
case .h1:
lineProperties = self.h1
case .h2:
lineProperties = self.h2
case .h3:
lineProperties = self.h3
case .h4:
lineProperties = self.h4
case .h5:
lineProperties = self.h5
case .h6:
lineProperties = self.h6
}
return (matchedCharacters, foundCharacters.stringByReplacingOccurrencesOfString("\\", withString: ""))
}
// Make H1
func attributedStringFromString(string : String, withStyle style : LineStyle, var attributes : [String : AnyObject] = [:] ) -> NSAttributedString {
let textStyle : String
var fontName : String?
// What type are we and is there a font name set?
switch currentType {
case .H1:
fontName = h1.fontName
if #available(iOS 9, *) {
textStyle = UIFontTextStyleTitle1
} else {
textStyle = UIFontTextStyleHeadline
}
attributes[NSForegroundColorAttributeName] = h1.color
case .H2:
fontName = h2.fontName
if #available(iOS 9, *) {
textStyle = UIFontTextStyleTitle2
} else {
textStyle = UIFontTextStyleHeadline
}
attributes[NSForegroundColorAttributeName] = h2.color
case .H3:
fontName = h3.fontName
if #available(iOS 9, *) {
textStyle = UIFontTextStyleTitle2
} else {
textStyle = UIFontTextStyleSubheadline
}
attributes[NSForegroundColorAttributeName] = h3.color
case .H4:
fontName = h4.fontName
textStyle = UIFontTextStyleHeadline
attributes[NSForegroundColorAttributeName] = h4.color
case .H5:
fontName = h5.fontName
textStyle = UIFontTextStyleSubheadline
attributes[NSForegroundColorAttributeName] = h5.color
case .H6:
fontName = h6.fontName
textStyle = UIFontTextStyleFootnote
attributes[NSForegroundColorAttributeName] = h6.color
case .codeblock:
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .blockquote:
lineProperties = self.blockquotes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
paragraphStyle.headIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .unorderedList:
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
let nonOptions = [NSTextTab.OptionKey: Any]()
paragraphStyle.tabStops = [
NSTextTab(textAlignment: .left, location: 20, options: nonOptions)]
paragraphStyle.defaultTabInterval = 20
paragraphStyle.headIndent = 20
attributes[.paragraphStyle] = paragraphStyle
finalTokens.insert(Token(type: .string, inputString: "\(self.bullet)\t"), at: 0)
default:
fontName = body.fontName
textStyle = UIFontTextStyleBody
attributes[NSForegroundColorAttributeName] = body.color
lineProperties = body
break
}
// Check for code
if style == .Code {
fontName = code.fontName
attributes[NSForegroundColorAttributeName] = code.color
if lineProperties.alignment != .left {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = lineProperties.alignment
attributes[.paragraphStyle] = paragraphStyle
}
if style == .Link {
fontName = link.fontName
attributes[NSForegroundColorAttributeName] = link.color
}
// Fallback to body
if let _ = fontName {
for token in finalTokens {
attributes[.font] = self.font(for: line)
attributes[.link] = nil
attributes[.foregroundColor] = self.color(for: line)
guard let styles = token.characterStyles as? [CharacterStyle] else {
continue
}
if styles.contains(.italic) {
attributes[.font] = self.font(for: line, characterOverride: .italic)
attributes[.foregroundColor] = self.italic.color
}
if styles.contains(.bold) {
attributes[.font] = self.font(for: line, characterOverride: .bold)
attributes[.foregroundColor] = self.bold.color
}
} else {
fontName = body.fontName
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 !os(watchOS)
if styles.contains(.image), let imageName = token.metadataString {
#if !os(macOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#elseif !os(watchOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = NSImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#endif
continue
}
#endif
if styles.contains(.code) {
attributes[.foregroundColor] = self.code.color
attributes[.font] = self.font(for: line, characterOverride: .code)
} else {
// Switch back to previous font
}
let str = NSAttributedString(string: token.outputString, attributes: attributes)
finalAttributedString.append(str)
}
let font = UIFont.preferredFontForTextStyle(textStyle)
let styleDescriptor = font.fontDescriptor()
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
var finalFont : UIFont
if let finalFontName = fontName, font = UIFont(name: finalFontName, size: styleSize) {
finalFont = font
} else {
finalFont = UIFont.preferredFontForTextStyle(textStyle)
}
let finalFontDescriptor = finalFont.fontDescriptor()
if style == .Italic {
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
}
if style == .Bold {
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
}
attributes[NSFontAttributeName] = finalFont
return NSAttributedString(string: string, attributes: attributes)
return finalAttributedString
}
}
+784
View File
@@ -0,0 +1,784 @@
//
// SwiftyTokeniser.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
import os.log
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 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]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
self.openTag = openTag
self.intermediateTag = intermediateTag
self.closingTag = closingTag
self.escapeCharacter = escapeCharacter
self.styles = styles
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: "\", \""))\"]"
}
}
public class SwiftyTokeniser {
let rules : [CharacterRule]
var replacements : [String : [Token]] = [:]
public init( with rules : [CharacterRule] ) {
self.rules = rules
}
/// This goes through every CharacterRule in order and applies it to the input string, tokenising the string
/// if there are any matches.
///
/// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to
/// those outside them.
///
/// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately.
///
/// This is to prevent situations like **\[link**\](url) from returing a bold string.
///
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
public func process( _ inputString : String ) -> [Token] {
guard rules.count > 0 else {
return [Token(type: .string, inputString: inputString)]
}
var currentTokens : [Token] = []
var mutableRules = self.rules
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
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)
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)
}
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] {
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)
}
}
}
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
}
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
os_log("==================================", log: .tokenising, type: .info)
return currentTokens
}
/// 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]
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
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:
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]
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]
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
case .closeTag:
let theToken = mutableTokens[idx]
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 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:
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 !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
}
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
}
}
@@ -233,25 +233,28 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 0720;
LastUpgradeCheck = 1120;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4CE98A71C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
};
F4CE98BB1C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
TestTargetID = F4CE98A71C8AEF7D00D735C1;
};
F4CE98C61C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
TestTargetID = F4CE98A71C8AEF7D00D735C1;
};
};
};
buildConfigurationList = F4CE98A31C8AEF7D00D735C1 /* Build configuration list for PBXProject "SwiftyMarkdownExample" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -385,17 +388,28 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@@ -417,7 +431,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -430,17 +444,28 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@@ -456,9 +481,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -472,6 +498,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -483,6 +511,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -494,6 +524,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
};
name = Debug;
@@ -506,6 +538,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
};
name = Release;
@@ -517,6 +551,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = SwiftyMarkdownExample;
USES_XCTRUNNER = YES;
};
@@ -529,6 +565,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = SwiftyMarkdownExample;
USES_XCTRUNNER = YES;
};
@@ -14,30 +14,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(application: UIApplication) {
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(application: UIApplication) {
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(application: UIApplication) {
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(application: UIApplication) {
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@@ -1,5 +1,15 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
@@ -30,6 +40,16 @@
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
@@ -59,6 +79,16 @@
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "bubble.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
@@ -14,32 +15,65 @@
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<view key="view" opaque="NO" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qZP-CU-74n">
<rect key="frame" x="20" y="20" width="560" height="580"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<rect key="frame" x="16" y="0.0" width="343" height="583"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<dataDetectorType key="dataDetectorTypes" link="YES"/>
</textView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="je8-G1-qej">
<rect key="frame" x="24" y="591" width="327" height="68"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="EOv-1r-ixy">
<rect key="frame" x="0.0" y="0.0" width="327" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="XzJ-SY-mRx">
<rect key="frame" x="0.0" y="34" width="327" height="34"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ksq-jO-8Gt">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="34"/>
<state key="normal" title="Reload File"/>
<connections>
<action selector="reloadText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="dg3-xi-352"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JK2-Lo-xRa">
<rect key="frame" x="163.5" y="0.0" width="163.5" height="34"/>
<state key="normal" title="Process Text"/>
<connections>
<action selector="processText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="e9I-rJ-ifc"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="qZP-CU-74n" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="1yU-8N-26a"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" id="7lJ-OQ-4eh"/>
<constraint firstAttribute="trailingMargin" secondItem="qZP-CU-74n" secondAttribute="trailing" id="F5p-iG-zTB"/>
<constraint firstAttribute="trailingMargin" secondItem="je8-G1-qej" secondAttribute="trailing" constant="8" id="JKP-2n-p24"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="je8-G1-qej" secondAttribute="bottom" constant="8" id="cvL-k7-iKp"/>
<constraint firstItem="je8-G1-qej" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" constant="8" id="gnq-BF-hFJ"/>
<constraint firstItem="je8-G1-qej" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="8" id="kJ0-Hf-RwN"/>
<constraint firstItem="qZP-CU-74n" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="srf-u0-j0n"/>
</constraints>
</view>
<connections>
<outlet property="textField" destination="EOv-1r-ixy" id="k2p-vH-CR0"/>
<outlet property="textView" destination="qZP-CU-74n" id="VO1-kx-Lpd"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="24.800000000000001" y="34.632683658170919"/>
</scene>
</scenes>
</document>
@@ -11,31 +11,46 @@ import SwiftyMarkdown
class ViewController: UIViewController {
@IBOutlet weak var textField : UITextField!
@IBOutlet weak var textView : UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// This is to help debugging.
reloadText(nil)
self.textView.dataDetectorTypes = UIDataDetectorTypes.All
if let url = NSBundle.mainBundle().URLForResource("example", withExtension: "md"), md = SwiftyMarkdown(url: url) {
self.textField.text = "Yo I'm a *single* line **string**. How do I look?"
}
@IBAction func processText( _ sender : UIButton? ) {
guard let existentText = self.textField.text else {
return
}
self.textView.attributedText = SwiftyMarkdown(string: existentText).attributedString()
}
@IBAction func reloadText( _ sender : UIButton? ) {
self.textView.dataDetectorTypes = UIDataDetectorTypes.all
if let url = Bundle.main.url(forResource: "example", withExtension: "md"), let md = SwiftyMarkdown(url: url) {
md.h2.fontName = "AvenirNextCondensed-Bold"
md.h2.color = UIColor.redColor()
md.h2.color = UIColor.red
md.h2.alignment = .center
md.code.fontName = "CourierNewPSMT"
md.blockquotes.fontStyle = .italic
md.underlineLinks = true
self.textView.attributedText = md.attributedString()
} else {
fatalError("Error loading file")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
@@ -1,17 +1,35 @@
# Swifty Markdown
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even if you use custom fonts.
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even with custom fonts.
## Features
Show Images From Your App Bundle!
---
![Image](bubble)
Customise fonts and colors easily in a Swift-like way:
md.code.fontName = "CourierNewPSMT"
md.h2.fontName = "AvenirNextCondensed-Medium"
md.h2.color = UIColor.redColor()
md.h2.alignment = .center
It supports the standard Markdown syntax, like *italics*, _underline italics_, **bold**, `backticks for code` and headings.
It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and \`backticks\`. It also supports inline Markdown [Links](http://voyagetravelapps.com/).
> It also now supports blockquotes
> and it supports whole-line italic and bold styles so you can go completely wild with styling! Wow! Such styles! Much fun!
**List**
- And
- Unordered
- Lists
- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Customise fonts and colours easily in a Swift-like way:
`md.code.fontName = "CourierNewPSMT"`
`md.h2.fontName = "AvenirNextCondensed-Medium"`
`md.h2.color = UIColor.redColor()`
*An italic line*
It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and has error handling for mismatched tags (\*\*bold\* == **bold*). It also supports inline Markdown [Links](http://voyagetravelapps.com/)
@@ -28,7 +28,7 @@ class SwiftyMarkdownExampleTests: XCTestCase {
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock {
self.measure {
// Put the code you want to measure the time of here.
}
}
+2 -2
View File
@@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.1</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>12</string>
<string>19</string>
</dict>
</plist>
@@ -0,0 +1,37 @@
//
// SwiftyMarkdownAttributedStringTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
class SwiftyMarkdownAttributedStringTests: XCTestCase {
func testThatAttributesAreAppliedCorrectly() {
let string = """
# Heading 1
A more *complicated* example. This one has **it all**. Here is a [link](http://voyagetravelapps.com/).
## Heading 2
## Heading 3
> This one is a blockquote
"""
let md = SwiftyMarkdown(string: string)
let attributedString = md.attributedString()
XCTAssertNotNil(attributedString)
XCTAssertEqual(attributedString.string, "Heading 1\n\nA more complicated example. This one has it all. Here is a link.\n\nHeading 2\n\nHeading 3\n\nThis one is a blockquote")
}
}
@@ -0,0 +1,557 @@
//
// SwiftyMarkdownCharacterTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
@testable import SwiftyMarkdown
import UIKit
import XCTest
class SwiftyMarkdownCharacterTests: XCTestCase {
func testIsolatedCase() {
let 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: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
])
let 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 testThatRegularTraitsAreParsedCorrectly() {
var 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)
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", 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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
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])
])
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 string with `code` (should not be indented)", output: "A string with code (should not be indented)", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic])
])
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 string with *italicised* text", output: "A string with italicised text", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
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: "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])
])
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 code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [
Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " with multiple ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " ", characterStyles: []),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_ with a *mix* _of_ italic *styles*", output: "An italic string with a mix of italic styles", tokens : [
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " with a ", characterStyles: []),
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " italic ", characterStyles: []),
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)
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 : [
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]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "with some code", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: ", **and some** _escaped_ `characters`, ", characterStyles: []),
Token(type: .string, inputString: "ending", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "with", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "more", characterStyles: [CharacterStyle.bold]),
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)
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])
])
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)
challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* 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: " word", 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 string with a ****bold italic*** word", output: "A string with a *bold italic 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: " word", 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 string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: " italic", characterStyles: [CharacterStyle.italic]),
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)
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: "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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
// The new version of SwiftyMarkdown is a lot more strict than the old version, although this may change in future
func offtestThatMarkdownMistakesAreHandledAppropriately() {
let mismatchedBoldCharactersAtStart = "**This should be bold*"
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
XCTAssertEqual(md.attributedString().string, "This should be bold")
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
XCTAssertEqual(md.attributedString().string, "A string that should be italic")
}
func testThatEscapedCharactersAreEscapedCorrectly() {
var challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
])
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)
challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [
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)
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)
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 string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [
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)
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)
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 string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [
Token(type: .string, inputString: "*", characterStyles: []),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
Token(type: .string, inputString: "A string with one *", characterStyles: []),
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func offtestAdvancedEscaping() {
var 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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
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)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
let asteriskSpace = """
An asterisk followed by a space: *
Line break
"""
let backtickSpace = "A backtick followed by a space: `"
let underscoreSpace = "An underscore followed by a space: _"
let asteriskFullStop = "Two asterisks followed by a full stop: **."
let backtickFullStop = "Two backticks followed by a full stop: ``."
let underscoreFullStop = "Two underscores followed by a full stop: __."
let asteriskComma = "An asterisk followed by a full stop: *, *"
let backtickComma = "A backtick followed by a space: `, `"
let underscoreComma = "An underscore followed by a space: _, _"
let asteriskWithBold = "A **bold** word followed by an asterisk * "
let backtickWithCode = "A `code` word followed by a backtick ` "
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
var md = SwiftyMarkdown(string: asteriskSpace)
XCTAssertEqual(md.attributedString().string, asteriskSpace)
md = SwiftyMarkdown(string: backtickSpace)
XCTAssertEqual(md.attributedString().string, backtickSpace)
md = SwiftyMarkdown(string: underscoreSpace)
XCTAssertEqual(md.attributedString().string, underscoreSpace)
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)
md = SwiftyMarkdown(string: asteriskComma)
XCTAssertEqual(md.attributedString().string, asteriskComma)
md = SwiftyMarkdown(string: backtickComma)
XCTAssertEqual(md.attributedString().string, backtickComma)
md = SwiftyMarkdown(string: underscoreComma)
XCTAssertEqual(md.attributedString().string, underscoreComma)
md = SwiftyMarkdown(string: asteriskWithBold)
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk *")
md = SwiftyMarkdown(string: backtickWithCode)
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick `")
md = SwiftyMarkdown(string: underscoreWithItalic)
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _")
}
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])
])
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)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/")
} 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])
])
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: "[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]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "Link 2", 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)
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/")
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: "@VoyageTravelApp", 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)
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")
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)
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: [])
])
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](", characterStyles: []),
Token(type: .string, inputString: "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: "[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: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
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/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(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.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])
])
let 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)
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[0].metadataString, "imageName")
}
}
@@ -0,0 +1,148 @@
//
// SwiftyMarkdownTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
struct StringTest {
let input : String
let expectedOutput : String
var acutalOutput : String = ""
}
struct TokenTest {
let input : String
let output : String
let tokens : [Token]
}
class SwiftyMarkdownTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testThatOctothorpeHeadersAreHandledCorrectly() {
let heading1 = StringTest(input: "# Heading 1", expectedOutput: "Heading 1")
var smd = SwiftyMarkdown(string:heading1.input )
XCTAssertEqual(smd.attributedString().string, heading1.expectedOutput)
let heading2 = StringTest(input: "## Heading 2", expectedOutput: "Heading 2")
smd = SwiftyMarkdown(string:heading2.input )
XCTAssertEqual(smd.attributedString().string, heading2.expectedOutput)
let heading3 = StringTest(input: "### #Heading #3", expectedOutput: "#Heading #3")
smd = SwiftyMarkdown(string:heading3.input )
XCTAssertEqual(smd.attributedString().string, heading3.expectedOutput)
let heading4 = StringTest(input: " #### #Heading 4 ####", expectedOutput: "#Heading 4")
smd = SwiftyMarkdown(string:heading4.input )
XCTAssertEqual(smd.attributedString().string, heading4.expectedOutput)
let heading5 = StringTest(input: " ##### Heading 5 #### ", expectedOutput: "Heading 5 ####")
smd = SwiftyMarkdown(string:heading5.input )
XCTAssertEqual(smd.attributedString().string, heading5.expectedOutput)
let heading6 = StringTest(input: " ##### Heading 5 #### More ", expectedOutput: "Heading 5 #### More")
smd = SwiftyMarkdown(string:heading6.input )
XCTAssertEqual(smd.attributedString().string, heading6.expectedOutput)
let heading7 = StringTest(input: "# **Bold Header 1** ", expectedOutput: "Bold Header 1")
smd = SwiftyMarkdown(string:heading7.input )
XCTAssertEqual(smd.attributedString().string, heading7.expectedOutput)
let heading8 = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
smd = SwiftyMarkdown(string:heading8.input )
XCTAssertEqual(smd.attributedString().string, heading8.expectedOutput)
let heading9 = StringTest(input: " # Heading 1", expectedOutput: "# Heading 1")
smd = SwiftyMarkdown(string:heading9.input )
XCTAssertEqual(smd.attributedString().string, heading9.expectedOutput)
let allHeaders = [heading1, heading2, heading3, heading4, heading5, heading6, heading7, heading8, heading9]
smd = SwiftyMarkdown(string: allHeaders.map({ $0.input }).joined(separator: "\n"))
XCTAssertEqual(smd.attributedString().string, allHeaders.map({ $0.expectedOutput}).joined(separator: "\n"))
let headerString = StringTest(input: "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6", expectedOutput: "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6")
smd = SwiftyMarkdown(string: headerString.input)
XCTAssertEqual(smd.attributedString().string, headerString.expectedOutput)
let headerStringWithBold = StringTest(input: "# **Bold Header 1**", expectedOutput: "Bold Header 1")
smd = SwiftyMarkdown(string: headerStringWithBold.input)
XCTAssertEqual(smd.attributedString().string, headerStringWithBold.expectedOutput )
let headerStringWithItalic = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
smd = SwiftyMarkdown(string: headerStringWithItalic.input)
XCTAssertEqual(smd.attributedString().string, headerStringWithItalic.expectedOutput)
}
func testThatUndelinedHeadersAreHandledCorrectly() {
let h1String = StringTest(input: "Header 1\n===\nSome following text", expectedOutput: "Header 1\nSome following text")
var md = SwiftyMarkdown(string: h1String.input)
XCTAssertEqual(md.attributedString().string, h1String.expectedOutput)
let h2String = StringTest(input: "Header 2\n---\nSome following text", expectedOutput: "Header 2\nSome following text")
md = SwiftyMarkdown(string: h2String.input)
XCTAssertEqual(md.attributedString().string, h2String.expectedOutput)
let h1StringWithBold = StringTest(input: "Header 1 **With Bold**\n===\nSome following text", expectedOutput: "Header 1 With Bold\nSome following text")
md = SwiftyMarkdown(string: h1StringWithBold.input)
XCTAssertEqual(md.attributedString().string, h1StringWithBold.expectedOutput)
let h2StringWithItalic = StringTest(input: "Header 2 _With Italic_\n---\nSome following text", expectedOutput: "Header 2 With Italic\nSome following text")
md = SwiftyMarkdown(string: h2StringWithItalic.input)
XCTAssertEqual(md.attributedString().string, h2StringWithItalic.expectedOutput)
let h2StringWithCode = StringTest(input: "Header 2 `With Code`\n---\nSome following text", expectedOutput: "Header 2 With Code\nSome following text")
md = SwiftyMarkdown(string: h2StringWithCode.input)
XCTAssertEqual(md.attributedString().string, h2StringWithCode.expectedOutput)
}
/*
The reason for this test is because the list of items dropped every other item in bullet lists marked with "-"
The faulty result was: "A cool title\n \n- Här har vi svenska ÅÄÖåäö tecken\n \nA Link"
As you can see, "- Point number one" and "- Point number two" are mysteriously missing.
It incorrectly identified rows as `Alt-H2`
*/
func offtestInternationalCharactersInList() {
let expected = "A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n \nA Link"
let input = "# A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n\n[A Link](http://dooer.com)"
let output = SwiftyMarkdown(string: input).attributedString().string
XCTAssertEqual(output, expected)
}
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!", expectedOutput: "Finally some Markdown!")
// let md = SwiftyMarkdown(string: yaml.input)
// XCTAssertEqual(md.attributedString().string, yaml.expectedOutput)
}
}
@@ -0,0 +1,43 @@
//
// SwiftyMarkdownAttributedStringTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
class SwiftyMarkdownPerformanceTests: XCTestCase {
func testThatFilesAreProcessedQuickly() {
guard let url = Bundle(for: SwiftyMarkdownPerformanceTests.self).url(forResource: "test", withExtension: "md") else {
XCTFail("Failed to load test.md in test bundle")
return
}
measure {
let md = SwiftyMarkdown(url: url)
_ = md?.attributedString()
}
}
func testThatStringsAreProcessedQuickly() {
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."
let md = SwiftyMarkdown(string: string)
measure {
_ = md.attributedString(from: string)
}
}
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 {
_ = md.attributedString(from: string)
}
}
}
@@ -1,280 +0,0 @@
//
// SwiftyMarkdownTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
class SwiftyMarkdownTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testThatOctothorpeHeadersAreHandledCorrectly() {
let headerString = "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6"
let headerStringWithBold = "# **Bold Header 1**"
let headerStringWithItalic = "## Header 2 _With Italics_"
var md = SwiftyMarkdown(string: headerString)
XCTAssertEqual(md.attributedString().string, "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6\n")
md = SwiftyMarkdown(string: headerStringWithBold)
XCTAssertEqual(md.attributedString().string, "Bold Header 1\n")
md = SwiftyMarkdown(string: headerStringWithItalic)
XCTAssertEqual(md.attributedString().string, "Header 2 With Italics\n")
}
func testThatUndelinedHeadersAreHandledCorrectly() {
let h1String = "Header 1\n===\nSome following text"
let h2String = "Header 2\n---\nSome following text"
let h1StringWithBold = "Header 1 **With Bold**\n===\nSome following text"
let h2StringWithItalic = "Header 2 _With Italic_\n---\nSome following text"
let h2StringWithCode = "Header 2 `With Code`\n---\nSome following text"
var md = SwiftyMarkdown(string: h1String)
XCTAssertEqual(md.attributedString().string, "Header 1\nSome following text\n")
md = SwiftyMarkdown(string: h2String)
XCTAssertEqual(md.attributedString().string, "Header 2\nSome following text\n")
md = SwiftyMarkdown(string: h1StringWithBold)
XCTAssertEqual(md.attributedString().string, "Header 1 With Bold\nSome following text\n")
md = SwiftyMarkdown(string: h2StringWithItalic)
XCTAssertEqual(md.attributedString().string, "Header 2 With Italic\nSome following text\n")
md = SwiftyMarkdown(string: h2StringWithCode)
XCTAssertEqual(md.attributedString().string, "Header 2 With Code\nSome following text\n")
}
func testThatRegularTraitsAreParsedCorrectly() {
let boldAtStartOfString = "**A bold string**"
let boldWithinString = "A string with a **bold** word"
let codeAtStartOfString = "`Code (should be indented)`"
let codeWithinString = "A string with `code` (should not be indented)"
let italicAtStartOfString = "*An italicised string*"
let italicWithinString = "A string with *italicised* text"
let multipleBoldWords = "__A bold string__ with a **mix** **of** bold __styles__"
let multipleCodeWords = "`A code string` with multiple `code` `instances`"
let multipleItalicWords = "_An italic string_ with a *mix* _of_ italic *styles*"
let longMixedString = "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety."
var md = SwiftyMarkdown(string: boldAtStartOfString)
XCTAssertEqual(md.attributedString().string, "A bold string\n")
md = SwiftyMarkdown(string: boldWithinString)
XCTAssertEqual(md.attributedString().string, "A string with a bold word\n")
md = SwiftyMarkdown(string: codeAtStartOfString)
XCTAssertEqual(md.attributedString().string, "\tCode (should be indented)\n")
md = SwiftyMarkdown(string: codeWithinString)
XCTAssertEqual(md.attributedString().string, "A string with code (should not be indented)\n")
md = SwiftyMarkdown(string: italicAtStartOfString)
XCTAssertEqual(md.attributedString().string, "An italicised string\n")
md = SwiftyMarkdown(string: italicWithinString)
XCTAssertEqual(md.attributedString().string, "A string with italicised text\n")
md = SwiftyMarkdown(string: multipleBoldWords)
XCTAssertEqual(md.attributedString().string, "A bold string with a mix of bold styles\n")
md = SwiftyMarkdown(string: multipleCodeWords)
XCTAssertEqual(md.attributedString().string, "\tA code string with multiple code instances\n")
md = SwiftyMarkdown(string: multipleItalicWords)
XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles\n")
md = SwiftyMarkdown(string: longMixedString)
XCTAssertEqual(md.attributedString().string, "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.\n")
}
func testThatMarkdownMistakesAreHandledAppropriately() {
let mismatchedBoldCharactersAtStart = "**This should be bold*"
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
XCTAssertEqual(md.attributedString().string, "This should be bold\n")
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
XCTAssertEqual(md.attributedString().string, "A string that should be italic\n")
}
func testThatEscapedCharactersAreEscapedCorrectly() {
let escapedBoldAtStart = "\\*\\*A normal string\\*\\*"
let escapedBoldWithin = "A string with \\*\\*escaped\\*\\* asterisks"
let escapedItalicAtStart = "\\_A normal string\\_"
let escapedItalicWithin = "A string with \\_escaped\\_ underscores"
let escapedBackticksAtStart = "\\`A normal string\\`"
let escapedBacktickWithin = "A string with \\`escaped\\` backticks"
let oneEscapedAsteriskOneNormalAtStart = "\\**A normal string\\**"
let oneEscapedAsteriskOneNormalWithin = "A string with \\**escaped\\** asterisks"
let oneEscapedAsteriskTwoNormalAtStart = "\\***A normal string*\\**"
let oneEscapedAsteriskTwoNormalWithin = "A string with *\\**escaped**\\* asterisks"
var md = SwiftyMarkdown(string: escapedBoldAtStart)
XCTAssertEqual(md.attributedString().string, "**A normal string**\n")
md = SwiftyMarkdown(string: escapedBoldWithin)
XCTAssertEqual(md.attributedString().string, "A string with **escaped** asterisks\n")
md = SwiftyMarkdown(string: escapedItalicAtStart)
XCTAssertEqual(md.attributedString().string, "_A normal string_\n")
md = SwiftyMarkdown(string: escapedItalicWithin)
XCTAssertEqual(md.attributedString().string, "A string with _escaped_ underscores\n")
md = SwiftyMarkdown(string: escapedBackticksAtStart)
XCTAssertEqual(md.attributedString().string, "`A normal string`\n")
md = SwiftyMarkdown(string: escapedBacktickWithin)
XCTAssertEqual(md.attributedString().string, "A string with `escaped` backticks\n")
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalAtStart)
XCTAssertEqual(md.attributedString().string, "*A normal string*\n")
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalWithin)
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks\n")
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalAtStart)
XCTAssertEqual(md.attributedString().string, "*A normal string*\n")
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalWithin)
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks\n")
}
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
let asteriskSpace = "An asterisk followed by a space: * "
let backtickSpace = "A backtick followed by a space: ` "
let underscoreSpace = "An underscore followed by a space: _ "
let asteriskFullStop = "Two asterisks followed by a full stop: **."
let backtickFullStop = "Two backticks followed by a full stop: ``."
let underscoreFullStop = "Two underscores followed by a full stop: __."
let asteriskComma = "An asterisk followed by a full stop: *, *"
let backtickComma = "A backtick followed by a space: `, `"
let underscoreComma = "An underscore followed by a space: _, _"
let asteriskWithBold = "A **bold** word followed by an asterisk * "
let backtickWithCode = "A `code` word followed by a backtick ` "
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
var md = SwiftyMarkdown(string: asteriskSpace)
XCTAssertEqual(md.attributedString().string, asteriskSpace + "\n")
md = SwiftyMarkdown(string: backtickSpace)
XCTAssertEqual(md.attributedString().string, backtickSpace + "\n")
md = SwiftyMarkdown(string: underscoreSpace)
XCTAssertEqual(md.attributedString().string, underscoreSpace + "\n")
md = SwiftyMarkdown(string: asteriskFullStop)
XCTAssertEqual(md.attributedString().string, asteriskFullStop + "\n")
md = SwiftyMarkdown(string: backtickFullStop)
XCTAssertEqual(md.attributedString().string, backtickFullStop + "\n")
md = SwiftyMarkdown(string: underscoreFullStop)
XCTAssertEqual(md.attributedString().string, underscoreFullStop + "\n")
md = SwiftyMarkdown(string: asteriskComma)
XCTAssertEqual(md.attributedString().string, asteriskComma + "\n")
md = SwiftyMarkdown(string: backtickComma)
XCTAssertEqual(md.attributedString().string, backtickComma + "\n")
md = SwiftyMarkdown(string: underscoreComma)
XCTAssertEqual(md.attributedString().string, underscoreComma + "\n")
md = SwiftyMarkdown(string: asteriskWithBold)
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk * \n")
md = SwiftyMarkdown(string: backtickWithCode)
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick ` \n")
md = SwiftyMarkdown(string: underscoreWithItalic)
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _ \n")
}
func testForLinks() {
let linkAtStart = "[Link at start](http://voyagetravelapps.com/)"
let linkWithin = "A [Link](http://voyagetravelapps.com/)"
let headerLink = "## [Header link](http://voyagetravelapps.com/)"
let multipleLinks = "[Link 1](http://voyagetravelapps.com/), [Link 2](http://voyagetravelapps.com/)"
let mailtoAndTwitterLinks = "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)"
let syntaxErrorSquareBracketAtStart = "[Link with missing square(http://voyagetravelapps.com/)"
let syntaxErrorSquareBracketWithin = "A [Link(http://voyagetravelapps.com/)"
let syntaxErrorParenthesisAtStart = "[Link with missing parenthesis](http://voyagetravelapps.com/"
let syntaxErrorParenthesisWithin = "A [Link](http://voyagetravelapps.com/"
var md = SwiftyMarkdown(string: linkAtStart)
XCTAssertEqual(md.attributedString().string, "Link at start\n")
md = SwiftyMarkdown(string: linkWithin)
XCTAssertEqual(md.attributedString().string, "A Link\n")
md = SwiftyMarkdown(string: headerLink)
XCTAssertEqual(md.attributedString().string, "Header link\n")
md = SwiftyMarkdown(string: multipleLinks)
XCTAssertEqual(md.attributedString().string, "Link 1, Link 2\n")
md = SwiftyMarkdown(string: syntaxErrorSquareBracketAtStart)
XCTAssertEqual(md.attributedString().string, "Link with missing square\n")
md = SwiftyMarkdown(string: syntaxErrorSquareBracketWithin)
XCTAssertEqual(md.attributedString().string, "A Link\n")
md = SwiftyMarkdown(string: syntaxErrorParenthesisAtStart)
XCTAssertEqual(md.attributedString().string, "Link with missing parenthesis\n")
md = SwiftyMarkdown(string: syntaxErrorParenthesisWithin)
XCTAssertEqual(md.attributedString().string, "A Link\n")
md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
XCTAssertEqual(md.attributedString().string, "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp\n")
// let mailtoAndTwitterLinks = "Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)"
// let md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
// XCTAssertEqual(md.attributedString().string, "Twitter @VoyageTravelApp\n")
}
}
@@ -0,0 +1,24 @@
//
// XCTest+SwiftyMarkdown.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
extension XCTestCase {
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)
}
}
+14
View File
@@ -0,0 +1,14 @@
---
layout: page
title: "Trail Wallet FAQ"
date: 2015-04-22 10:59
comments: true
sharing: true
liking: false
footer: true
sidebar: false
---
# Good Day To You, Walleteer!
We are Erin and Simon from [Never Ending Voyage][1] and we want to thank you for trying out our app. We have been travelling non-stop for seven years and part of how we support ourselves is through Trail Wallet.
+21
View File
@@ -0,0 +1,21 @@
# 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.
+4 -69
View File
@@ -16,73 +16,8 @@
# This is the minimum version number required.
# Update this, if you use features of a newer version
fastlane_version "1.36.1"
$scheme = "SwiftyMarkdown"
$spec = "SwiftyMarkdown.podspec"
$project = './SwiftyMarkdown.xcodeproj'
default_platform :ios
platform :ios do
spec = "SwiftyMarkdown.podspec"
before_all do
ensure_git_branch
ensure_git_status_clean
increment_build_number
commit_version_bump(xcodeproj: './SwiftyMarkdown.xcodeproj')
end
desc "Bumps the patch version, increments it, and pushes it to both the remote and spec repos"
lane :patch do
# snapshot
update(type: "patch")
end
desc "Bumps the minor version, increments it, and pushes it to both the remote and spec repos"
lane :minor do
# snapshot
update(type: "minor")
end
lane :initial do
version = version_get_podspec
bump(version: version)
end
private_lane :update do |options|
type = options[:type]
version = version_bump_podspec(path: spec, bump_type: type)
increment_version_number( bump_type: type, version_number: version )
git_commit(path: spec, message: "Updating podspec")
bump(version: version)
end
private_lane :bump do |options|
version = options[:version]
add_git_tag( tag: version)
push_to_git_remote
pod_push(path: spec)
end
# You can define as many lanes as you want
after_all do |lane|
# This block is called, only if the executed lane was successful
notification(subtitle: "SwiftyMarkdown fastlane finished", message: "Completed '#{lane}' successfully")
end
error do |lane, exception|
notification(subtitle: "Error: SwiftyMarkdown fastlane", message: "'#{exception.message}'")
end
end
# More information about multiple platforms in fastlane: https://github.com/KrauseFx/fastlane/blob/master/docs/Platforms.md
# All available actions: https://github.com/KrauseFx/fastlane/blob/master/docs/Actions.md
import "../../Fastlane/FastfilePods"
+52 -8
View File
@@ -1,29 +1,73 @@
fastlane documentation
================
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
sudo gem install fastlane
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
# Available Actions
## iOS
### ios patch
```
fastlane ios patch
```
Bumps the patch version, increments it, and pushes it to both the remote and spec repos
This does the following:
- Runs the unit tests
- Ensures Cocoapods compatibility
- Bumps the patch version
### ios minor
```
fastlane ios minor
```
Bumps the minor version, increments it, and pushes it to both the remote and spec repos
### ios initial
This does the following:
- Runs the unit tests
- Ensures Cocoapods compatibility
- Bumps the minor version
### ios major
```
fastlane ios initial
fastlane ios major
```
This does the following:
- Runs the unit tests
- Ensures Cocoapods compatibility
- Bumps the major version
### ios test
```
fastlane ios test
```
### 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 to run [fastlane](https://fastlane.tools).
More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane).
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).