Compare commits

...

149 Commits

Author SHA1 Message Date
Simon Fairbairn dde451ab4e Merge branch 'release/1.2.4' 2022-05-19 08:36:00 +01:00
Simon Fairbairn ae97f0fbea Version Bump 2022-05-19 08:34:47 +01:00
Simon Fairbairn 040d7896fe Updating podspec 2022-05-19 08:34:43 +01:00
Simon Fairbairn 403285e46f Automatic fastlane Readme update 2022-05-19 08:32:25 +01:00
Simon Fairbairn af94dd615e Updates gemfile 2022-05-19 08:29:23 +01:00
Simon Fairbairn 99ded8446b Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2021-04-10 11:30:39 +12:00
Simon Fairbairn 2bbf1213a6 Merge pull request #110 from bensLine/master
Fix bold font size get's overridden by font descriptor
2021-04-10 11:28:53 +12:00
bensLine aa748f2cdd Prefer configured font size over bold/italic descriptor size 2021-03-22 15:22:11 +01:00
Simon Fairbairn 851b547dcd Merge branch 'release/1.2.3' into develop 2020-09-14 09:22:48 +12:00
Simon Fairbairn 5b0a1e7633 Merge branch 'release/1.2.3' 2020-09-14 09:22:43 +12:00
Simon Fairbairn 13955aac92 Updates fastlane readme 2020-09-14 09:21:58 +12:00
Simon Fairbairn 0b40036eda Version Bump 2020-09-14 09:20:57 +12:00
Simon Fairbairn 2e3d59324a Updating podspec 2020-09-14 09:20:54 +12:00
Simon Fairbairn 9d1457a96a Updating Fastfile 2020-09-14 09:19:10 +12:00
Simon Fairbairn eb9742b518 Updating Fastfile 2020-09-14 09:18:56 +12:00
Simon Fairbairn 569daaf779 Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2020-09-14 09:04:03 +12:00
Simon Fairbairn 649d7690e1 Merge pull request #94 from csauvage/hotfix/fix-carthage-build
Modify plist file (attempt to fix #93) Thanks for this!
2020-09-14 09:03:27 +12:00
Clément SAUVAGE 2b8df10a0d Modify plist 2020-09-02 11:42:59 +02:00
Simon Fairbairn c9fcecafc3 Merge branch 'release/1.2.2' into develop 2020-06-19 11:02:44 +12:00
Simon Fairbairn 6c64c16475 Merge branch 'release/1.2.2' 2020-06-19 11:02:41 +12:00
Simon Fairbairn d87627c957 Version Bump 2020-06-19 11:02:30 +12:00
Simon Fairbairn 1ecc196398 Updating podspec 2020-06-19 11:02:28 +12:00
Simon Fairbairn 6103068a26 Fixes issue with macOS support 2020-06-19 11:01:23 +12:00
Simon Fairbairn 429d557c9c Fixes issue with macOS 2020-06-19 11:00:38 +12:00
Simon Fairbairn 2e69439003 Updates Bundle 2020-06-19 10:58:12 +12:00
Simon Fairbairn a93ccedac5 Updating readme 2020-06-19 10:57:11 +12:00
Simon Fairbairn bc81245371 Merge pull request #87 from ramonilho/feature/underline-link-style
Added LinkStyles to customize underline properties
2020-06-19 10:56:39 +12:00
Simon Fairbairn ecebb6d8b8 Merge pull request #89 from bitsfabrik/master
fixed issue where underline style is not going to be reseted
2020-06-19 10:51:40 +12:00
Philip Messlehner 1df35418f2 added support for linespacing and paragraphspacing 2020-06-17 22:06:00 +02:00
Philip Messlehner 5e8c7d03ff fixed issue where underline style is not going to be resetted 2020-06-17 21:36:53 +02:00
Ramon Honório 6ab69d3d82 added underline color 2020-05-26 12:14:01 -03:00
Ramon Honório 9cf9d6bfbe added underline style into link 2020-05-26 12:05:26 -03:00
Simon Fairbairn f4529f5ff9 Merge branch 'release/1.2.1' 2020-04-15 14:14:16 +12:00
Simon Fairbairn 4592cdd4fa Version Bump 2020-04-15 14:13:39 +12:00
Simon Fairbairn 17a2b6b5ff Updating podspec 2020-04-15 14:13:37 +12:00
Simon Fairbairn 20d2827573 Merge branch 'master' of https://github.com/SimonFairbairn/SwiftyMarkdown into develop 2020-04-15 14:11:45 +12:00
Simon Fairbairn 98f6beced3 Merge pull request #81 from julo15/fix-swift-build-error
Fix build break: "Cannot capture lastElement before it is declared"
2020-04-15 14:11:16 +12:00
Julian Lo fab59935e2 Fix build break: "Cannot capture lastElement before it is declared" 2020-04-14 11:28:48 -07:00
Simon Fairbairn 54e1f56cad Merge branch 'release/1.2.0' into develop 2020-04-11 16:59:39 +12:00
Simon Fairbairn abc0768701 Merge branch 'release/1.2.0' 2020-04-11 16:59:35 +12:00
Simon Fairbairn 95c1e59344 Version Bump 2020-04-11 16:59:21 +12:00
Simon Fairbairn 4581712c5e Updating podspec 2020-04-11 16:59:19 +12:00
Simon Fairbairn b0e9e36c6b Removes unneeded 2020-04-11 16:57:40 +12:00
Simon Fairbairn 61490b1e13 Cleans up codebase and class names 2020-04-11 16:08:02 +12:00
Simon Fairbairn bcbf3bd38c Merge branch 'feature/referencedLists' into develop 2020-04-11 16:02:31 +12:00
Simon Fairbairn 79467395b5 Updates readme 2020-04-11 16:02:15 +12:00
Simon Fairbairn 4e2f5c234e Fixes more weird character combinations 2020-04-11 15:04:33 +12:00
Simon Fairbairn 898a1ca2ed Fixes #75. Fixes #78. Fixes #79. 2020-04-11 14:51:04 +12:00
Simon Fairbairn ba5d8ce9c6 Marker before third attempt at parser 2020-04-08 14:10:26 +12:00
Simon Fairbairn 68336ab630 Adds new non-repeating system 2020-04-06 14:12:58 +12:00
Simon Fairbairn 368f0da628 Prior to implementing new link system 2020-04-06 10:58:49 +12:00
Simon Fairbairn 72ff31b6ff Adds additional tests 2020-02-04 11:52:04 +13:00
Simon Fairbairn 924d06bcfe Pulls token scanning out of tokeniser and into separate scanning class and adds performance log class 2020-02-04 09:18:21 +13:00
Simon Fairbairn a1aa1a0eac Massively increases performance of complex strings 2020-02-03 17:24:22 +13:00
Simon Fairbairn 71a64dee21 Passes all tests again 2020-02-03 16:34:08 +13:00
Simon Fairbairn 71eb29ada0 Completely overhauls tokenising engine 2020-02-03 14:18:15 +13:00
Simon Fairbairn 711850056d Refactors tests to make customisable rules possible 2020-02-01 15:43:26 +13:00
Simon Fairbairn d2cbd0ce96 Updates readme 2020-02-01 12:50:02 +13:00
Simon Fairbairn 61c6701518 Updates Readme 2020-02-01 12:45:24 +13:00
Simon Fairbairn 8518dfb406 Merge branch 'release/1.1.0' into develop 2020-02-01 12:39:27 +13:00
Simon Fairbairn a06d690000 Merge branch 'release/1.1.0' 2020-02-01 12:39:23 +13:00
Simon Fairbairn 09d4166025 Version Bump 2020-02-01 12:38:18 +13:00
Simon Fairbairn 1f162f124e Updating podspec 2020-02-01 12:38:16 +13:00
Simon Fairbairn 79f35fa4aa Correctly names the source file location in the podspec #details 2020-02-01 12:37:24 +13:00
Simon Fairbairn 095b2d3a6c Updates podspec to reflect new source location 2020-02-01 12:35:58 +13:00
Simon Fairbairn 8a77770f4d Merge branch 'feature/indentedLists' into develop 2020-02-01 12:34:03 +13:00
Simon Fairbairn 6389a8cbe5 Adds ordered lists and ability to mix list types, and fixes #71 2020-02-01 12:33:52 +13:00
Simon Fairbairn e467e992e2 Adds ordered lists 2020-02-01 12:33:27 +13:00
Simon Fairbairn f68e64505b Updates readme with descriptions of the new features 2020-02-01 09:38:39 +13:00
Simon Fairbairn d2c2af66fd Fixes #73. Fixes #72. 2020-01-31 20:55:30 +13:00
Simon Fairbairn b7f451993b Adds support for first and second order indented unordered lists 2020-01-31 16:10:29 +13:00
Simon Fairbairn b095feaca5 Fixes some formatting issues and adds more example text 2020-01-31 15:53:34 +13:00
Simon Fairbairn f963bfec5b Merge branch 'release/1.0.2' into develop 2020-01-30 12:38:28 +13:00
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
82 changed files with 9609 additions and 1803 deletions
+1
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
#
-1
View File
@@ -1 +0,0 @@
4.0
@@ -0,0 +1,26 @@
//
// AppDelegate.swift
// SwiftyMarkdownExample macOS
//
// Created by Simon Fairbairn on 01/02/2020.
// Copyright © 2020 Voyage Travel Apps. All rights reserved.
//
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
@@ -0,0 +1,58 @@
{
"images" : [
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,717 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="SwiftyMarkdownExample macOS" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="SwiftyMarkdownExample macOS" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About SwiftyMarkdownExample macOS" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide SwiftyMarkdownExample macOS" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit SwiftyMarkdownExample macOS" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" id="bib-Uj-vzu">
<items>
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="newDocument:" target="Ady-hI-5gd" id="4Si-XN-c54"/>
</connections>
</menuItem>
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
<connections>
<action selector="openDocument:" target="Ady-hI-5gd" id="bVn-NM-KNZ"/>
</connections>
</menuItem>
<menuItem title="Open Recent" id="tXI-mr-wws">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
<items>
<menuItem title="Clear Menu" id="vNY-rz-j42">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="Daa-9d-B3U"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
<connections>
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
<connections>
<action selector="saveDocument:" target="Ady-hI-5gd" id="teZ-XB-qJY"/>
</connections>
</menuItem>
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
<connections>
<action selector="saveDocumentAs:" target="Ady-hI-5gd" id="mDf-zr-I0C"/>
</connections>
</menuItem>
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
<connections>
<action selector="revertDocumentToSaved:" target="Ady-hI-5gd" id="iJ3-Pv-kwq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/>
</connections>
</menuItem>
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
<connections>
<action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Format" id="jxT-CU-nIS">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
<items>
<menuItem title="Font" id="Gi5-1S-RQB">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
<items>
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
<connections>
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
</connections>
</menuItem>
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
</connections>
</menuItem>
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
<connections>
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
</connections>
</menuItem>
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
<connections>
<action selector="underline:" target="Ady-hI-5gd" id="FYS-2b-JAY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
</connections>
</menuItem>
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
<connections>
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
<menuItem title="Kern" id="jBQ-r6-VK2">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
<items>
<menuItem title="Use Default" id="GUa-eO-cwY">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardKerning:" target="Ady-hI-5gd" id="6dk-9l-Ckg"/>
</connections>
</menuItem>
<menuItem title="Use None" id="cDB-IK-hbR">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffKerning:" target="Ady-hI-5gd" id="U8a-gz-Maa"/>
</connections>
</menuItem>
<menuItem title="Tighten" id="46P-cB-AYj">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="tightenKerning:" target="Ady-hI-5gd" id="hr7-Nz-8ro"/>
</connections>
</menuItem>
<menuItem title="Loosen" id="ogc-rX-tC1">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="loosenKerning:" target="Ady-hI-5gd" id="8i4-f9-FKE"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ligatures" id="o6e-r0-MWq">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
<items>
<menuItem title="Use Default" id="agt-UL-0e3">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardLigatures:" target="Ady-hI-5gd" id="7uR-wd-Dx6"/>
</connections>
</menuItem>
<menuItem title="Use None" id="J7y-lM-qPV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffLigatures:" target="Ady-hI-5gd" id="iX2-gA-Ilz"/>
</connections>
</menuItem>
<menuItem title="Use All" id="xQD-1f-W4t">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useAllLigatures:" target="Ady-hI-5gd" id="KcB-kA-TuK"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Baseline" id="OaQ-X3-Vso">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
<items>
<menuItem title="Use Default" id="3Om-Ey-2VK">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unscript:" target="Ady-hI-5gd" id="0vZ-95-Ywn"/>
</connections>
</menuItem>
<menuItem title="Superscript" id="Rqc-34-cIF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="superscript:" target="Ady-hI-5gd" id="3qV-fo-wpU"/>
</connections>
</menuItem>
<menuItem title="Subscript" id="I0S-gh-46l">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="subscript:" target="Ady-hI-5gd" id="Q6W-4W-IGz"/>
</connections>
</menuItem>
<menuItem title="Raise" id="2h7-ER-AoG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="raiseBaseline:" target="Ady-hI-5gd" id="4sk-31-7Q9"/>
</connections>
</menuItem>
<menuItem title="Lower" id="1tx-W0-xDw">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowerBaseline:" target="Ady-hI-5gd" id="OF1-bc-KW4"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
<connections>
<action selector="orderFrontColorPanel:" target="Ady-hI-5gd" id="mSX-Xz-DV3"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="copyFont:" target="Ady-hI-5gd" id="GJO-xA-L4q"/>
</connections>
</menuItem>
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteFont:" target="Ady-hI-5gd" id="JfD-CL-leO"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Text" id="Fal-I4-PZk">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Text" id="d9c-me-L2H">
<items>
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
<connections>
<action selector="alignLeft:" target="Ady-hI-5gd" id="zUv-R1-uAa"/>
</connections>
</menuItem>
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
<connections>
<action selector="alignCenter:" target="Ady-hI-5gd" id="spX-mk-kcS"/>
</connections>
</menuItem>
<menuItem title="Justify" id="J5U-5w-g23">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="alignJustified:" target="Ady-hI-5gd" id="ljL-7U-jND"/>
</connections>
</menuItem>
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
<connections>
<action selector="alignRight:" target="Ady-hI-5gd" id="r48-bG-YeY"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
<menuItem title="Writing Direction" id="H1b-Si-o9J">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
<items>
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="YGs-j5-SAR">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionNatural:" target="Ady-hI-5gd" id="qtV-5e-UBP"/>
</connections>
</menuItem>
<menuItem id="Lbh-J2-qVU">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="S0X-9S-QSf"/>
</connections>
</menuItem>
<menuItem id="jFq-tB-4Kx">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="5fk-qB-AqJ"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="Nop-cj-93Q">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionNatural:" target="Ady-hI-5gd" id="lPI-Se-ZHp"/>
</connections>
</menuItem>
<menuItem id="BgM-ve-c93">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="caW-Bv-w94"/>
</connections>
</menuItem>
<menuItem id="RB4-Sm-HuC">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="EXD-6r-ZUu"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
<menuItem title="Show Ruler" id="vLm-3I-IUL">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleRuler:" target="Ady-hI-5gd" id="FOx-HJ-KwY"/>
</connections>
</menuItem>
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="copyRuler:" target="Ady-hI-5gd" id="71i-fW-3W2"/>
</connections>
</menuItem>
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="pasteRuler:" target="Ady-hI-5gd" id="cSh-wd-qM2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/>
</connections>
</menuItem>
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
</connections>
</menuItem>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="SwiftyMarkdownExample macOS Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
</scene>
</scenes>
</document>
@@ -3,9 +3,11 @@
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
@@ -13,12 +15,22 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>0.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>19</string>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 Voyage Travel Apps. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,28 @@
//
// ViewController.swift
// SwiftyMarkdownExample macOS
//
// Created by Simon Fairbairn on 01/02/2020.
// Copyright © 2020 Voyage Travel Apps. All rights reserved.
//
import Cocoa
import SwiftyMarkdown
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
@@ -3,11 +3,15 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
F421DD991C8AF4E900B86D66 /* example.md in Resources */ = {isa = PBXBuildFile; fileRef = F421DD951C8AF34F00B86D66 /* example.md */; };
F4B4A44C23E4E17400550249 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B4A44B23E4E17400550249 /* AppDelegate.swift */; };
F4B4A44E23E4E17400550249 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B4A44D23E4E17400550249 /* ViewController.swift */; };
F4B4A45023E4E17400550249 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4B4A44F23E4E17400550249 /* Assets.xcassets */; };
F4B4A45323E4E17400550249 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4B4A45123E4E17400550249 /* Main.storyboard */; };
F4B4A46023E4E3DC00550249 /* SwiftyMarkdown in Frameworks */ = {isa = PBXBuildFile; productRef = F4B4A45F23E4E3DC00550249 /* SwiftyMarkdown */; };
F4CE98AC1C8AEF7D00D735C1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98AB1C8AEF7D00D735C1 /* AppDelegate.swift */; };
F4CE98AE1C8AEF7D00D735C1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98AD1C8AEF7D00D735C1 /* ViewController.swift */; };
F4CE98B11C8AEF7D00D735C1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98AF1C8AEF7D00D735C1 /* Main.storyboard */; };
@@ -15,8 +19,7 @@
F4CE98B61C8AEF7D00D735C1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */; };
F4CE98C11C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98C01C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift */; };
F4CE98CC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98CB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift */; };
F4CE98E31C8AEFEC00D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */; };
F4CE98E51C8AEFFB00D735C1 /* SwiftyMarkdown.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
F4EAB653244179FE00206782 /* example.md in Resources */ = {isa = PBXBuildFile; fileRef = F4576C2E2437F67B0013E2B6 /* example.md */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -34,37 +37,40 @@
remoteGlobalIDString = F4CE98A71C8AEF7D00D735C1;
remoteInfo = SwiftyMarkdownExample;
};
F4CE98DF1C8AEFE200D735C1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F4CE98DA1C8AEFE200D735C1 /* SwiftyMarkdown.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F4CE98811C8A921300D735C1;
remoteInfo = SwiftyMarkdown;
};
F4CE98E11C8AEFE200D735C1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F4CE98DA1C8AEFE200D735C1 /* SwiftyMarkdown.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F4CE988B1C8A921300D735C1;
remoteInfo = SwiftyMarkdownTests;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
F4B4A45C23E4E18800550249 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98E41C8AEFF000D735C1 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
F4CE98E51C8AEFFB00D735C1 /* SwiftyMarkdown.framework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
F421DD951C8AF34F00B86D66 /* example.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = "<group>"; };
F4576C2E2437F67B0013E2B6 /* example.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = "<group>"; };
F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftyMarkdownExample macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F4B4A44B23E4E17400550249 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F4B4A44D23E4E17400550249 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
F4B4A44F23E4E17400550249 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F4B4A45223E4E17400550249 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
F4B4A45423E4E17400550249 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4B4A45523E4E17400550249 /* SwiftyMarkdownExample_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftyMarkdownExample_macOS.entitlements; sourceTree = "<group>"; };
F4B4A46123E4E5DD00550249 /* SwiftyMarkdown */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftyMarkdown; path = ../../../Developer/SwiftyMarkdown; sourceTree = "<group>"; };
F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyMarkdownExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98AB1C8AEF7D00D735C1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F4CE98AD1C8AEF7D00D735C1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -78,15 +84,21 @@
F4CE98C71C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98CB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownExampleUITests.swift; sourceTree = "<group>"; };
F4CE98CD1C8AEF7D00D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4CE98DA1C8AEFE200D735C1 /* SwiftyMarkdown.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftyMarkdown.xcodeproj; path = ../SwiftyMarkdown.xcodeproj; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
F4B4A44623E4E17400550249 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98A51C8AEF7D00D735C1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F4CE98E31C8AEFEC00D735C1 /* SwiftyMarkdown.framework in Frameworks */,
F4B4A46023E4E3DC00550249 /* SwiftyMarkdown in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -107,14 +119,36 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
F4B4A44A23E4E17400550249 /* SwiftyMarkdownExample macOS */ = {
isa = PBXGroup;
children = (
F4B4A44B23E4E17400550249 /* AppDelegate.swift */,
F4B4A44D23E4E17400550249 /* ViewController.swift */,
F4B4A44F23E4E17400550249 /* Assets.xcassets */,
F4B4A45123E4E17400550249 /* Main.storyboard */,
F4B4A45423E4E17400550249 /* Info.plist */,
F4B4A45523E4E17400550249 /* SwiftyMarkdownExample_macOS.entitlements */,
);
path = "SwiftyMarkdownExample macOS";
sourceTree = "<group>";
};
F4B4A45923E4E18800550249 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
F4CE989F1C8AEF7D00D735C1 = {
isa = PBXGroup;
children = (
F4CE98DA1C8AEFE200D735C1 /* SwiftyMarkdown.xcodeproj */,
F4B4A46123E4E5DD00550249 /* SwiftyMarkdown */,
F4CE98AA1C8AEF7D00D735C1 /* SwiftyMarkdownExample */,
F4CE98BF1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */,
F4CE98CA1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */,
F4B4A44A23E4E17400550249 /* SwiftyMarkdownExample macOS */,
F4CE98A91C8AEF7D00D735C1 /* Products */,
F4B4A45923E4E18800550249 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -124,6 +158,7 @@
F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */,
F4CE98BC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.xctest */,
F4CE98C71C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.xctest */,
F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */,
);
name = Products;
sourceTree = "<group>";
@@ -137,7 +172,7 @@
F4CE98B21C8AEF7D00D735C1 /* Assets.xcassets */,
F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */,
F4CE98B71C8AEF7D00D735C1 /* Info.plist */,
F421DD951C8AF34F00B86D66 /* example.md */,
F4576C2E2437F67B0013E2B6 /* example.md */,
);
path = SwiftyMarkdownExample;
sourceTree = "<group>";
@@ -160,18 +195,27 @@
path = SwiftyMarkdownExampleUITests;
sourceTree = "<group>";
};
F4CE98DB1C8AEFE200D735C1 /* Products */ = {
isa = PBXGroup;
children = (
F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */,
F4CE98E21C8AEFE200D735C1 /* SwiftyMarkdownTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
F4B4A44823E4E17400550249 /* SwiftyMarkdownExample macOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = F4B4A45623E4E17400550249 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExample macOS" */;
buildPhases = (
F4B4A44523E4E17400550249 /* Sources */,
F4B4A44623E4E17400550249 /* Frameworks */,
F4B4A44723E4E17400550249 /* Resources */,
F4B4A45C23E4E18800550249 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "SwiftyMarkdownExample macOS";
productName = "SwiftyMarkdownExample macOS";
productReference = F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */;
productType = "com.apple.product-type.application";
};
F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = F4CE98D01C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExample" */;
@@ -186,6 +230,9 @@
dependencies = (
);
name = SwiftyMarkdownExample;
packageProductDependencies = (
F4B4A45F23E4E3DC00550249 /* SwiftyMarkdown */,
);
productName = SwiftyMarkdownExample;
productReference = F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */;
productType = "com.apple.product-type.application";
@@ -232,77 +279,72 @@
F4CE98A01C8AEF7D00D735C1 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 0900;
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4B4A44823E4E17400550249 = {
CreatedOnToolsVersion = 11.3.1;
DevelopmentTeam = 52T262DA8V;
ProvisioningStyle = Automatic;
};
F4CE98A71C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0900;
LastSwiftMigration = 1120;
};
F4CE98BB1C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0900;
LastSwiftMigration = 1120;
TestTargetID = F4CE98A71C8AEF7D00D735C1;
};
F4CE98C61C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0900;
LastSwiftMigration = 1120;
TestTargetID = F4CE98A71C8AEF7D00D735C1;
};
};
};
buildConfigurationList = F4CE98A31C8AEF7D00D735C1 /* Build configuration list for PBXProject "SwiftyMarkdownExample" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = F4CE989F1C8AEF7D00D735C1;
packageReferences = (
F4B4A45E23E4E3DC00550249 /* XCRemoteSwiftPackageReference "SwiftyMarkdown" */,
);
productRefGroup = F4CE98A91C8AEF7D00D735C1 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = F4CE98DB1C8AEFE200D735C1 /* Products */;
ProjectRef = F4CE98DA1C8AEFE200D735C1 /* SwiftyMarkdown.xcodeproj */;
},
);
projectRoot = "";
targets = (
F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */,
F4CE98BB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */,
F4CE98C61C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */,
F4B4A44823E4E17400550249 /* SwiftyMarkdownExample macOS */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = SwiftyMarkdown.framework;
remoteRef = F4CE98DF1C8AEFE200D735C1 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
F4CE98E21C8AEFE200D735C1 /* SwiftyMarkdownTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = SwiftyMarkdownTests.xctest;
remoteRef = F4CE98E11C8AEFE200D735C1 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
F4B4A44723E4E17400550249 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4B4A45023E4E17400550249 /* Assets.xcassets in Resources */,
F4B4A45323E4E17400550249 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98A61C8AEF7D00D735C1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4CE98B61C8AEF7D00D735C1 /* LaunchScreen.storyboard in Resources */,
F4EAB653244179FE00206782 /* example.md in Resources */,
F4CE98B31C8AEF7D00D735C1 /* Assets.xcassets in Resources */,
F421DD991C8AF4E900B86D66 /* example.md in Resources */,
F4CE98B11C8AEF7D00D735C1 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -324,6 +366,15 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F4B4A44523E4E17400550249 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4B4A44E23E4E17400550249 /* ViewController.swift in Sources */,
F4B4A44C23E4E17400550249 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98A41C8AEF7D00D735C1 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -365,6 +416,14 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
F4B4A45123E4E17400550249 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
F4B4A45223E4E17400550249 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
F4CE98AF1C8AEF7D00D735C1 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
@@ -384,10 +443,75 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
F4B4A45723E4E17400550249 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 52T262DA8V;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "SwiftyMarkdownExample macOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.voyagetravelapps.SwiftyMarkdownExample-macOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F4B4A45823E4E17400550249 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 52T262DA8V;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "SwiftyMarkdownExample macOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.voyagetravelapps.SwiftyMarkdownExample-macOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 5.0;
};
name = Release;
};
F4CE98CE1C8AEF7D00D735C1 /* Debug */ = {
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;
@@ -396,12 +520,14 @@
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;
@@ -428,7 +554,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;
@@ -441,6 +567,7 @@
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;
@@ -449,12 +576,14 @@
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;
@@ -475,10 +604,11 @@
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";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -489,11 +619,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -502,11 +635,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -515,11 +651,15 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
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 = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
};
name = Debug;
@@ -529,11 +669,15 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
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 = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
};
name = Release;
@@ -542,11 +686,15 @@
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
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 = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = SwiftyMarkdownExample;
USES_XCTRUNNER = YES;
};
@@ -556,11 +704,15 @@
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
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 = On;
SWIFT_VERSION = 4.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = SwiftyMarkdownExample;
USES_XCTRUNNER = YES;
};
@@ -569,6 +721,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F4B4A45623E4E17400550249 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExample macOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F4B4A45723E4E17400550249 /* Debug */,
F4B4A45823E4E17400550249 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F4CE98A31C8AEF7D00D735C1 /* Build configuration list for PBXProject "SwiftyMarkdownExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -606,6 +767,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
F4B4A45E23E4E3DC00550249 /* XCRemoteSwiftPackageReference "SwiftyMarkdown" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SimonFairbairn/SwiftyMarkdown.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.2;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
F4B4A45F23E4E3DC00550249 /* SwiftyMarkdown */ = {
isa = XCSwiftPackageProductDependency;
package = F4B4A45E23E4E3DC00550249 /* XCRemoteSwiftPackageReference "SwiftyMarkdown" */;
productName = SwiftyMarkdown;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F4CE98A01C8AEF7D00D735C1 /* Project object */;
}
@@ -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,16 @@
{
"object": {
"pins": [
{
"package": "SwiftyMarkdown",
"repositoryURL": "https://github.com/SimonFairbairn/SwiftyMarkdown.git",
"state": {
"branch": null,
"revision": "36f0c4d9c772a57f72941e7b51f0f04fb57fd79b",
"version": "1.0.2"
}
}
]
},
"version": 1
}
@@ -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

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" 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="15706"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftyMarkdownExample" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<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="16" y="0.0" width="343" height="583"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<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" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="qZP-CU-74n" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="1yU-8N-26a"/>
<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,6 +11,8 @@ import SwiftyMarkdown
class ViewController: UIViewController {
@IBOutlet weak var textField : UITextField!
@IBOutlet weak var textView : UITextView!
override func viewDidLoad() {
@@ -18,23 +20,39 @@ class ViewController: UIViewController {
// This is to help debugging.
reloadText(nil)
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 self.textView.text != "" {
self.textView.attributedText = SwiftyMarkdown(string: "Yo I'm a *single* line **string**. How do I look?").attributedString()
return
}
if let url = Bundle.main.url(forResource: "example", withExtension: "md"), let md = SwiftyMarkdown(url: url) {
md.h2.fontName = "AvenirNextCondensed-Bold"
md.h2.color = UIColor.red
md.h2.color = UIColor.blue
md.h2.alignment = .center
md.code.fontName = "CourierNewPSMT"
if #available(iOS 13.0, *) {
md.strikethrough.color = .tertiaryLabel
} else {
md.strikethrough.color = .lightGray
}
md.blockquotes.fontStyle = .italic
md.underlineLinks = true
self.textView.attributedText = md.attributedString()
} else {
+50
View File
@@ -0,0 +1,50 @@
# 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 with custom fonts.
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`, ~~strikethrough~~, 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!
**Lists**
- It Supports
- Unordered
- Lists
- Indented item with a longer string to make sure indentation is consistent
- Second level indent with a longer string to make sure indentation is consistent
- List item with a longer string to make sure indentation is consistent
1. And
1. Ordered
1. Lists
1. Indented item
1. Second level indent
1. (Use `1.` as the list item identifier)
1. List item
1. List item
- Mix
- List styles
1. List item with a longer string to make sure indentation is consistent
1. List item
1. List item
1. List item
1. List item
+232 -137
View File
@@ -1,182 +1,277 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (2.3.5)
activesupport (4.2.8)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
babosa (1.0.2)
claide (1.0.1)
cocoapods (1.2.1.beta.1)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.1, < 2.0)
cocoapods-core (= 1.2.1.beta.1)
cocoapods-deintegrate (>= 1.0.1, < 2.0)
cocoapods-downloader (>= 1.1.3, < 2.0)
CFPropertyList (3.0.5)
rexml
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.588.0)
aws-sdk-core (3.131.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.1.2, < 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.0.1)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.5.7)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (~> 0.2.5)
xcodeproj (>= 1.4.1, < 2.0)
cocoapods-core (1.2.1.beta.1)
activesupport (>= 4.0.2, < 5)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.1)
cocoapods-downloader (1.1.3)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.0.0)
cocoapods-trunk (1.1.2)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (= 0.7.8)
cocoapods-try (1.1.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.4)
highline (~> 1.7.2)
domain_name (0.5.20170223)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.10)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.2.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
escape (0.0.4)
excon (0.55.0)
faraday (0.11.0)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.0)
fastlane (2.24.0)
activesupport (< 5)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 2.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.0, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.9)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.9)
fastimage (>= 1.6)
gh_inspector (>= 1.0.1, < 2.0.0)
google-api-client (~> 0.9.2)
highline (>= 1.7.2, < 2.0.0)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
mini_magick (~> 4.5.1)
multi_json
multi_xml (~> 0.5)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 1.1.0, < 2.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
slack-notifier (>= 1.3, < 2.0.0)
terminal-notifier (>= 1.6.2, < 2.0.0)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (~> 0.5.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 0.20, < 2.0.0)
xcpretty (>= 0.2.4, < 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fourflusher (2.0.1)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.0.3)
google-api-client (0.9.28)
addressable (~> 2.3)
googleauth (~> 0.5)
httpclient (~> 2.7)
hurley (~> 0.1)
memoist (~> 0.11)
mime-types (>= 1.6)
representable (~> 2.3.0)
retriable (~> 2.0)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
logging (~> 2.0)
memoist (~> 0.12)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.21.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.5.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.14.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.3)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
highline (1.7.8)
http-cookie (1.0.3)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
hurley (0.2)
i18n (0.8.1)
json (2.0.3)
jwt (1.5.6)
little-plugger (1.1.4)
logging (2.2.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
memoist (0.15.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_magick (4.5.1)
minitest (5.10.1)
molinillo (0.5.7)
multi_json (1.12.1)
multi_xml (0.6.0)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.15.0)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.2.3)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.7.8)
os (0.9.6)
plist (3.2.0)
public_suffix (2.0.5)
representable (2.3.0)
uber (~> 0.0.7)
retriable (2.1.0)
rouge (1.11.1)
ruby-macho (0.2.6)
rubyzip (1.2.1)
naturally (2.2.1)
netrc (0.11.0)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.7.3)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
slack-notifier (1.5.1)
terminal-notifier (1.7.1)
terminal-table (1.7.3)
unicode-display_width (~> 1.1.1)
thread_safe (0.3.6)
tty-screen (0.5.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
uber (0.0.15)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.3)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.4.3)
CFPropertyList (~> 2.3.3)
activesupport (>= 3)
claide (>= 1.0.1, < 2.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.3)
xcpretty (0.2.6)
rouge (~> 1.8)
xcpretty-travis-formatter (0.0.4)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -186,4 +281,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
1.14.5
2.3.8
+19
View File
@@ -0,0 +1,19 @@
// swift-tools-version:5.1
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"),
.testTarget(name: "SwiftyMarkdownTests", dependencies: ["SwiftyMarkdown"])
]
)
@@ -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,285 @@
//: [Previous](@previous)
import Foundation
extension String {
func repeating( _ max : Int ) -> String {
var output = self
for _ in 1..<max {
output += self
}
return output
}
}
enum TagState {
case none
case open
case intermediate
case closed
}
struct TagString {
var state : TagState = .none
var preOpenString = ""
var openTagString = ""
var intermediateString = ""
var intermediateTagString = ""
var metadataString = ""
var closedTagString = ""
var postClosedString = ""
let rule : Rule
init( with rule : Rule ) {
self.rule = rule
}
mutating func append( _ string : String? ) {
guard let existentString = string else {
return
}
switch self.state {
case .none:
self.preOpenString += existentString
case .open:
self.intermediateString += existentString
case .intermediate:
self.metadataString += existentString
case .closed:
self.postClosedString += existentString
}
}
mutating func append( contentsOf tokenGroup: [TokenGroup] ) {
print(tokenGroup)
for token in tokenGroup {
switch token.state {
case .none:
self.append(token.string)
case .open:
if self.state != .none {
self.preOpenString += token.string
} else {
self.openTagString += token.string
}
case .intermediate:
if self.state != .open {
self.intermediateString += token.string
} else {
self.intermediateTagString += token.string
}
case .closed:
if self.rule.intermediateTag != nil && self.state != .intermediate {
self.metadataString += token.string
} else {
self.closedTagString += token.string
}
}
self.state = token.state
}
}
mutating func tokens() -> [Token] {
print(self)
var tokens : [Token] = []
if !self.preOpenString.isEmpty {
tokens.append(Token(type: .string, inputString: self.preOpenString))
}
if !self.openTagString.isEmpty {
tokens.append(Token(type: .openTag, inputString: self.openTagString))
}
if !self.intermediateString.isEmpty {
var token = Token(type: .string, inputString: self.intermediateString)
token.metadataString = self.metadataString
tokens.append(token)
}
if !self.intermediateTagString.isEmpty {
tokens.append(Token(type: .intermediateTag, inputString: self.intermediateTagString))
}
if !self.metadataString.isEmpty {
tokens.append(Token(type: .metadata, inputString: self.metadataString))
}
if !self.closedTagString.isEmpty {
tokens.append(Token(type: .closeTag, inputString: self.closedTagString))
}
self.preOpenString = ""
self.openTagString = ""
self.intermediateString = ""
self.intermediateTagString = ""
self.metadataString = ""
self.closedTagString = ""
self.postClosedString = ""
self.state = .none
return tokens
}
}
struct TokenGroup {
enum TokenGroupType {
case string
case tag
case escape
}
let string : String
let isEscaped : Bool
let type : TokenGroupType
var state : TagState = .none
}
func getTokenGroups( for string : inout String, with rule : Rule, shouldEmpty : Bool = false ) -> [TokenGroup] {
if string.isEmpty {
return []
}
let maxCount = rule.openTag.count * rule.maxTags
var groups : [TokenGroup] = []
let maxTag = rule.openTag.repeating(rule.maxTags)
if maxTag.contains(string) {
if string.count == maxCount || shouldEmpty {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .open
groups.append(token)
string.removeAll()
}
} else if string == rule.intermediateTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .intermediate
groups.append(token)
string.removeAll()
} else if string == rule.closingTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .closed
groups.append(token)
string.removeAll()
}
if shouldEmpty && !string.isEmpty {
let token = TokenGroup(string: string, isEscaped: false, type: .tag)
groups.append(token)
string.removeAll()
}
return groups
}
func scan( _ string : String, with rule : Rule) -> [Token] {
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
var tokens : [Token] = []
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
if let existentEscape = rule.escapeCharacter {
set.insert(charactersIn: String(existentEscape))
}
var openTag = rule.openTag.repeating(rule.maxTags)
var tagString = TagString(with: rule)
var openTagFound : TagState = .none
var regularCharacters = ""
var tagGroupCount = 0
while !scanner.isAtEnd {
tagGroupCount += 1
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let start = scanner.scanUpToCharacters(from: set) {
tagString.append(start)
}
} else {
var string : NSString?
scanner.scanUpToCharacters(from: set, into: &string)
if let existentString = string as String? {
tagString.append(existentString)
}
}
// The end of the string
let maybeFoundChars = scanner.scanCharacters(from: set )
guard let foundTag = maybeFoundChars else {
continue
}
if foundTag == rule.openTag && foundTag.count < rule.minTags {
tagString.append(foundTag)
continue
}
//:--
print(foundTag)
var tokenGroups : [TokenGroup] = []
var escapeCharacter : Character? = nil
var cumulatedString = ""
for char in foundTag {
if let existentEscapeCharacter = escapeCharacter {
// If any of the tags feature the current character
let escape = String(existentEscapeCharacter)
let nextTagCharacter = String(char)
if rule.openTag.contains(nextTagCharacter) || rule.intermediateTag?.contains(nextTagCharacter) ?? false || rule.closingTag?.contains(nextTagCharacter) ?? false {
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: true, type: .tag))
escapeCharacter = nil
} else if nextTagCharacter == escape {
// Doesn't apply to this rule
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: false, type: .escape))
}
continue
}
if let existentEscape = rule.escapeCharacter {
if char == existentEscape {
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
escapeCharacter = char
continue
}
}
cumulatedString.append(char)
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule))
}
if let remainingEscape = escapeCharacter {
tokenGroups.append(TokenGroup(string: String(remainingEscape), isEscaped: false, type: .escape))
}
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
tagString.append(contentsOf: tokenGroups)
if tagString.state == .closed {
tokens.append(contentsOf: tagString.tokens())
}
}
tokens.append(contentsOf: tagString.tokens())
return tokens
}
//: [Next](@next)
var string = "[]([[\\[Some Link]\\]](\\(\\(\\url) [Regular link](url)"
//string = "Text before [Regular link](url) Text after"
var output = "[]([[Some Link]] Regular link"
var tokens = scan(string, with: LinkRule())
print( tokens.filter( { $0.type == .string }).map({ $0.outputString }).joined())
//print( tokens )
//string = "**\\*\\Bold\\*\\***"
//output = "*\\Bold**"
//tokens = scan(string, with: AsteriskRule())
//print( tokens )
@@ -0,0 +1,32 @@
import Foundation
public protocol Rule {
var escapeCharacter : Character? { get }
var openTag : String { get }
var intermediateTag : String? { get }
var closingTag : String? { get }
var maxTags : Int { get }
var minTags : Int { get }
}
public struct LinkRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "["
public let intermediateTag : String? = "]("
public let closingTag : String? = ")"
public let maxTags : Int = 1
public let minTags : Int = 1
public init() { }
}
public struct AsteriskRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "*"
public let intermediateTag : String? = nil
public let closingTag : String? = nil
public let maxTags : Int = 3
public let minTags : Int = 1
public init() { }
}
@@ -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,497 @@
//
// 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 group : Int = 0
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
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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'/>
<page name='SKLabelNode'/>
<page name='Groups'/>
</pages>
</playground>
+361 -13
View File
@@ -1,55 +1,403 @@
# 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.
- [What's New](#fully-rebuilt-for-2020)
- [Installation](#installation)
- [How to Use](#how-to-use-swiftymarkdown)
- [Screenshot](#screenshot)
- [Front Matter](#front-matter)
- [Appendix](#appendix)
## Fully Rebuilt For 2020!
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:
### CocoaPods:
`pod 'SwiftyMarkdown'`
## Usage
### SPM:
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
## How To Use SwiftyMarkdown
Read Markdown from a text string...
Text string
```swift
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
md.attributedString()
```
URL
...or from a URL.
```swift
if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
md.attributedString()
}
```
## Supported Features
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
```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__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5
###### Header 6
`code`
##### Header 5 #####
###### Header 6 ######
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
[Referenced Links][1]
![Referenced Images][2]
[1]: http://voyagetravelapps.com/
[2]: <Name of asset in bundle>
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
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
Set the attributes of every paragraph and character style type using straightforward dot syntax:
```swift
md.body.fontName = "AvenirNextCondensed-Medium"
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
md.bullet = "🍏"
```
md.italic.color = UIColor.blueColor()
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 `Example/SwiftyMarkdown.xcodeproj` file to get started.
## Front Matter
SwiftyMarkdown recognises YAML front matter and will populate the `frontMatterAttributes` property with the key-value pairs that it fines.
## 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
strikethrough.fontName : String
strikethrough.fontSize : CGFloat
strikethrough.color : UI/NSColor
strikethrough.fontStyle : FontStyle
underlineLinks : Bool
bullet : String
```
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look. For example, perhaps you want blockquotes to default to having the italic style:
```swift
md.blockquotes.fontStyle = .italic
```
Or, 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(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingTags: true, balancedTags: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
These Character Rules are defined by SwiftyMarkdown:
public struct CharacterRule : CustomStringConvertible {
public let primaryTag : CharacterRuleTag
public let tags : [CharacterRuleTag]
public let escapeCharacters : [Character]
public let styles : [Int : CharacterStyling]
public let minTags : Int
public let maxTags : Int
public var metadataLookup : Bool = false
public var definesBoundary = false
public var shouldCancelRemainingRules = false
public var balancedTags = false
}
1. `primaryTag`: Each rule must have at least one tag and it can be one of `repeating`, `open`, `close`, `metadataOpen`, or `metadataClose`. `repeating` tags are tags that have identical open and close characters (and often have more than 1 style depending on how many are in a group). For example, the `*` tag used in Markdown.
2. `tags`: An array of other tags that the rule can look for. This is where you would put the `close` tag for a custom rule, for example.
3. `escapeCharacters`: The characters that appear prior to any of the tag characters that tell the scanner to ignore the tag.
4. `styles`: The styles that should be applied to every character between the opening and closing tags.
5. `minTags`: The minimum number of repeating characters to be considered a successful match. For example, setting the `primaryTag` to `*` and the `minTag` to 2 would mean that `**foo**` would be a successful match wheras `*bar*` would not.
6. `maxTags`: The maximum number of repeating characters to be considered a successful match.
7. `metadataLookup`: Used for Markdown reference links. Tells the scanner to try to look up the metadata from this dictionary, rather than from the inline result.
8. `definesBoundary`: In order for open and close tags to be successful, the `boundaryCount` for a given location in the string needs to be the same. Setting this property to `true` means that this rule will increase the `boundaryCount` for every character between its opening and closing tags. For example, the `[` rule defines a boundary. After it is applied, the string `*foo[bar*]` becomes `*foobar*` with a boundaryCount `00001111`. Applying the `*` rule results in the output `*foobar*` because the opening `*` tag and the closing `*` tag now have different `boundaryCount` values. It's basically a way to fix the `**[should not be bold**](url)` problem in Markdown.
9. `shouldCancelRemainingTags`: A successful match will mark every character between the opening and closing tags as complete, thereby preventing any further rules from being applied to those characters.
10. `balancedTags`: This flag requires that the opening and closing tags be of exactly equal length. E.g. If this is set to true, `**foo*` would result in `**foo*`. If it was false, the output would be `*foo`.
#### Rule Subsets
If you want to only support a small subset of Markdown, it's now easy to do.
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(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
#### 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(primaryTag: CharacterRuleTag(tag: "%", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.elf])
]
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()
```
+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.
+129
View File
@@ -0,0 +1,129 @@
# SwiftyMarkdown 1.0
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
## Fully Rebuilt For 2020!
SwiftyMarkdown now features a more robust and reliable rules-based line processing and tokenisation engine. It has added support for images stored in the bundle (`![Image](<Name In bundle>)`), codeblocks, blockquotes, and unordered lists!
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be underlined by setting underlineLinks to `true`.
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
## Installation
### CocoaPods:
`pod 'SwiftyMarkdown'`
### SPM:
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
1. Neat!
# SwiftyMarkdown 1.0
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
## Fully Rebuilt For 2020!
SwiftyMarkdown now features a more robust and reliable rules-based line processing and tokenisation engine. It has added support for images stored in the bundle (`![Image](<Name In bundle>)`), codeblocks, blockquotes, and unordered lists!
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be underlined by setting underlineLinks to `true`.
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
## Installation
### CocoaPods:
`pod 'SwiftyMarkdown'`
### SPM:
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
## Header 2
or
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
1. Neat!
+108
View File
@@ -0,0 +1,108 @@
//
// CharacterRule.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
public enum SpaceAllowed {
case no
case bothSides
case oneSide
case leadingSide
case trailingSide
}
public enum Cancel {
case none
case allRemaining
case currentSet
}
public enum CharacterRuleTagType {
case open
case close
case metadataOpen
case metadataClose
case repeating
}
public struct CharacterRuleTag {
let tag : String
let type : CharacterRuleTagType
public init( tag : String, type : CharacterRuleTagType ) {
self.tag = tag
self.type = type
}
}
public struct CharacterRule : CustomStringConvertible {
public let primaryTag : CharacterRuleTag
public let tags : [CharacterRuleTag]
public let escapeCharacters : [Character]
public let styles : [Int : CharacterStyling]
public let minTags : Int
public let maxTags : Int
public var metadataLookup : Bool = false
public var isRepeatingTag : Bool {
return self.primaryTag.type == .repeating
}
public var definesBoundary = false
public var shouldCancelRemainingRules = false
public var balancedTags = false
public var description: String {
return "Character Rule with Open tag: \(self.primaryTag.tag) and current styles : \(self.styles) "
}
public func tag( for type : CharacterRuleTagType ) -> CharacterRuleTag? {
return self.tags.filter({ $0.type == type }).first ?? nil
}
public init(primaryTag: CharacterRuleTag, otherTags: [CharacterRuleTag], escapeCharacters : [Character] = ["\\"], styles: [Int : CharacterStyling] = [:], minTags : Int = 1, maxTags : Int = 1, metadataLookup : Bool = false, definesBoundary : Bool = false, shouldCancelRemainingRules : Bool = false, balancedTags : Bool = false) {
self.primaryTag = primaryTag
self.tags = otherTags
self.escapeCharacters = escapeCharacters
self.styles = styles
self.metadataLookup = metadataLookup
self.definesBoundary = definesBoundary
self.shouldCancelRemainingRules = shouldCancelRemainingRules
self.minTags = maxTags < minTags ? maxTags : minTags
self.maxTags = minTags > maxTags ? minTags : maxTags
self.balancedTags = balancedTags
}
}
enum ElementType {
case tag
case escape
case string
case space
case newline
case metadata
}
struct Element {
let character : Character
var type : ElementType
var boundaryCount : Int = 0
var isComplete : Bool = false
var styles : [CharacterStyling] = []
var metadata : [String] = []
}
extension CharacterSet {
func containsUnicodeScalars(of character: Character) -> Bool {
return character.unicodeScalars.allSatisfy(contains(_:))
}
}
@@ -0,0 +1,44 @@
//
// PerfomanceLog.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
import os.log
class PerformanceLog {
var timer : TimeInterval = 0
let enablePerfomanceLog : Bool
let log : OSLog
let identifier : String
init( with environmentVariableName : String, identifier : String, log : OSLog ) {
self.log = log
self.enablePerfomanceLog = (ProcessInfo.processInfo.environment[environmentVariableName] != nil)
self.identifier = identifier
}
func start() {
guard enablePerfomanceLog else { return }
self.timer = Date().timeIntervalSinceReferenceDate
os_log("--- TIMER %{public}@ began", log: self.log, type: .info, self.identifier)
}
func tag( with string : String) {
guard enablePerfomanceLog else { return }
if timer == 0 {
self.start()
}
os_log("TIMER %{public}@: %f %@", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer, string)
}
func end() {
guard enablePerfomanceLog else { return }
self.timer = Date().timeIntervalSinceReferenceDate
os_log("--- TIMER %{public}@ finished. Total time: %f", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer)
self.timer = 0
}
}
@@ -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
}
}
@@ -0,0 +1,249 @@
//
// SwiftyLineProcessor.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 = "SwiftyLineProcessor"
static let swiftyLineProcessorPerformance = OSLog(subsystem: subsystem, category: "Swifty Line Processor Performance")
}
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 FrontMatterRule {
let openTag : String
let closeTag : String
let keyValueSeparator : Character
}
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 {
public var processEmptyStrings : LineStyling?
public internal(set) var frontMatterAttributes : [String : String] = [:]
var closeToken : String? = nil
let defaultType : LineStyling
let lineRules : [LineRule]
let frontMatterRules : [FrontMatterRule]
let perfomanceLog = PerformanceLog(with: "SwiftyLineProcessorPerformanceLogging", identifier: "Line Processor", log: OSLog.swiftyLineProcessorPerformance)
public init( rules : [LineRule], defaultRule: LineStyling, frontMatterRules : [FrontMatterRule] = []) {
self.lineRules = rules
self.defaultType = defaultRule
self.frontMatterRules = frontMatterRules
}
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
}
if !text.contains(element.token) {
continue
}
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)
}
func processFrontMatter( _ strings : [String] ) -> [String] {
guard let firstString = strings.first?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return strings
}
var rulesToApply : FrontMatterRule? = nil
for matter in self.frontMatterRules {
if firstString == matter.openTag {
rulesToApply = matter
break
}
}
guard let existentRules = rulesToApply else {
return strings
}
var outputString = strings
// Remove the first line, which is the front matter opening tag
let _ = outputString.removeFirst()
var closeFound = false
while !closeFound {
let nextString = outputString.removeFirst()
if nextString == existentRules.closeTag {
closeFound = true
continue
}
var keyValue = nextString.components(separatedBy: "\(existentRules.keyValueSeparator)")
if keyValue.count < 2 {
continue
}
let key = keyValue.removeFirst()
let value = keyValue.joined()
self.frontMatterAttributes[key] = value
}
while outputString.first?.isEmpty ?? false {
outputString.removeFirst()
}
return outputString
}
public func process( _ string : String ) -> [SwiftyLine] {
var foundAttributes : [SwiftyLine] = []
self.perfomanceLog.start()
var lines = string.components(separatedBy: CharacterSet.newlines)
lines = self.processFrontMatter(lines)
self.perfomanceLog.tag(with: "(Front matter completed)")
for heading in lines {
if processEmptyStrings == nil && heading.isEmpty {
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)
self.perfomanceLog.tag(with: "(line completed: \(heading)")
}
return foundAttributes
}
}
@@ -0,0 +1,174 @@
//
// 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
case .strikethrough:
fontName = strikethrough.fontName ?? fontName
fontSize = strikethrough.fontSize
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: fontSize ?? 0)
}
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: fontSize ?? 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, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
return body.color
case .referencedLink:
return link.color
}
}
}
#endif
@@ -0,0 +1,146 @@
//
// 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, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
return body.color
case .yaml:
return body.color
case .referencedLink:
return body.color
}
}
}
#endif
+621
View File
@@ -0,0 +1,621 @@
//
// SwiftyMarkdown.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import os.log
#if os(macOS)
import AppKit
#else
import UIKit
#endif
extension OSLog {
private static var subsystem = "SwiftyMarkdown"
static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
}
public enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
case referencedLink
case referencedImage
case strikethrough
public func isEqualTo(_ other: CharacterStyling) -> Bool {
guard let other = other as? CharacterStyle else {
return false
}
return other == self
}
}
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
case unorderedListIndentFirstOrder
case unorderedListIndentSecondOrder
case orderedList
case orderedListIndentFirstOrder
case orderedListIndentSecondOrder
case referencedLink
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 }
var lineSpacing: CGFloat { get set }
var paragraphSpacing: CGFloat { 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?
#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
}
@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
public var lineSpacing : CGFloat = 0.0
public var paragraphSpacing : CGFloat = 0.0
}
@objc open class LinkStyles : BasicStyles {
public var underlineStyle: NSUnderlineStyle = .single
#if os(macOS)
public lazy var underlineColor = self.color
#else
public lazy var underlineColor = self.color
#endif
}
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
@objc open class SwiftyMarkdown: NSObject {
static public var frontMatterRules = [
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
]
static public var lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading),
LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
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.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(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.image], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "(", type: .metadataOpen),
CharacterRuleTag(tag: ")", type: .metadataClose)
], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "(", type: .metadataOpen),
CharacterRuleTag(tag: ")", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingRules: true, balancedTags: true),
CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
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 = LinkStyles()
/// 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()
open var strikethrough = BasicStyles()
public var bullet : String = ""
public var underlineLinks : Bool = false
public var frontMatterAttributes : [String : String] {
get {
return self.lineProcessor.frontMatterAttributes
}
}
var currentType : MarkdownLineStyle = .body
var string : String
var orderedListCount = 0
var orderedListIndentFirstOrderCount = 0
var orderedListIndentSecondOrderCount = 0
var previouslyFoundTokens : [Token] = []
var applyAttachments = true
let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance)
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
- returns: An initialized SwiftyMarkdown object
*/
public init(string : String ) {
self.string = string
super.init()
self.setup()
}
/**
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()
self.setup()
}
func setup() {
#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
strikethrough.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
strikethrough.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
strikethrough.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
strikethrough.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(from markdownString : String? = nil) -> NSAttributedString {
self.previouslyFoundTokens.removeAll()
self.perfomanceLog.start()
if let existentMarkdownString = markdownString {
self.string = existentMarkdownString
}
let attributedString = NSMutableAttributedString(string: "")
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") })
let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) })
var keyValuePairs : [String : String] = [:]
for line in references {
let strings = line.line.components(separatedBy: "]:")
guard strings.count >= 2 else {
continue
}
var key : String = strings[0]
if !key.isEmpty {
let newstart = key.index(key.startIndex, offsetBy: 1)
let range : Range<String.Index> = newstart..<key.endIndex
key = String(key[range]).trimmingCharacters(in: .whitespacesAndNewlines)
}
keyValuePairs[key] = strings[1].trimmingCharacters(in: .whitespacesAndNewlines)
}
self.perfomanceLog.tag(with: "(line processing complete)")
self.tokeniser.metadataLookup = keyValuePairs
for (idx, line) in referencesRemoved.enumerated() {
if idx > 0 {
attributedString.append(NSAttributedString(string: "\n"))
}
let finalTokens = self.tokeniser.process(line.line)
self.previouslyFoundTokens.append(contentsOf: finalTokens)
self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)")
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
}
self.perfomanceLog.end()
return attributedString
}
}
extension SwiftyMarkdown {
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
var finalTokens = tokens
let finalAttributedString = NSMutableAttributedString()
var attributes : [NSAttributedString.Key : AnyObject] = [:]
guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else {
preconditionFailure("The passed line style is not a valid Markdown Line Style")
}
var listItem = self.bullet
switch markdownLineStyle {
case .orderedList:
self.orderedListCount += 1
self.orderedListIndentFirstOrderCount = 0
self.orderedListIndentSecondOrderCount = 0
listItem = "\(self.orderedListCount)."
case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder:
self.orderedListIndentFirstOrderCount += 1
self.orderedListIndentSecondOrderCount = 0
if markdownLineStyle == .orderedListIndentFirstOrder {
listItem = "\(self.orderedListIndentFirstOrderCount)."
}
case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder:
self.orderedListIndentSecondOrderCount += 1
if markdownLineStyle == .orderedListIndentSecondOrder {
listItem = "\(self.orderedListIndentSecondOrderCount)."
}
default:
self.orderedListCount = 0
self.orderedListIndentFirstOrderCount = 0
self.orderedListIndentSecondOrderCount = 0
}
let lineProperties : LineProperties
switch 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
paragraphStyle.headIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
let interval : CGFloat = 30
var addition = interval
var indent = ""
switch line.lineStyle as! MarkdownLineStyle {
case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder:
addition = interval * 2
indent = "\t"
case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder:
addition = interval * 3
indent = "\t\t"
default:
break
}
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])]
paragraphStyle.defaultTabInterval = interval
paragraphStyle.headIndent = addition
attributes[.paragraphStyle] = paragraphStyle
finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0)
case .yaml:
lineProperties = body
case .previousH1:
lineProperties = body
case .previousH2:
lineProperties = body
case .body:
lineProperties = body
case .referencedLink:
lineProperties = body
}
let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
if lineProperties.alignment != .left {
paragraphStyle.alignment = lineProperties.alignment
}
paragraphStyle.lineSpacing = lineProperties.lineSpacing
paragraphStyle.paragraphSpacing = lineProperties.paragraphSpacing
attributes[.paragraphStyle] = paragraphStyle
for token in finalTokens {
attributes[.font] = self.font(for: line)
attributes[.link] = nil
attributes[.strikethroughStyle] = nil
attributes[.foregroundColor] = self.color(for: line)
attributes[.underlineStyle] = nil
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 let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = token.metadataStrings[linkIdx] as AnyObject
if underlineLinks {
attributes[.underlineStyle] = self.link.underlineStyle.rawValue as AnyObject
attributes[.underlineColor] = self.link.underlineColor
}
}
if styles.contains(.strikethrough) {
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
attributes[.foregroundColor] = self.strikethrough.color
}
#if !os(watchOS)
if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count {
if !self.applyAttachments {
continue
}
#if !os(macOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx])
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#elseif !os(watchOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx])
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)
}
return finalAttributedString
}
}
+564
View File
@@ -0,0 +1,564 @@
//
// SwiftyScanner.swift
//
//
// Created by Simon Fairbairn on 04/04/2020.
//
//
// SwiftyScanner.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyScanner"
static let swiftyScanner = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner")
static let swiftyScannerPerformance = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner Peformance")
}
enum RepeatingTagType {
case open
case either
case close
case neither
}
struct TagGroup {
let groupID = UUID().uuidString
var tagRanges : [ClosedRange<Int>]
var tagType : RepeatingTagType = .open
var count = 1
}
class SwiftyScanner {
var elements : [Element]
let rule : CharacterRule
let metadata : [String : String]
var pointer : Int = 0
var spaceAndNewLine = CharacterSet.whitespacesAndNewlines
var tagGroups : [TagGroup] = []
var isMetadataOpen = false
var enableLog = (ProcessInfo.processInfo.environment["SwiftyScannerScanner"] != nil)
let currentPerfomanceLog = PerformanceLog(with: "SwiftyScannerScannerPerformanceLogging", identifier: "Scanner", log: OSLog.swiftyScannerPerformance)
let log = PerformanceLog(with: "SwiftyScannerScanner", identifier: "Scanner", log: OSLog.swiftyScanner)
enum Position {
case forward(Int)
case backward(Int)
}
init( withElements elements : [Element], rule : CharacterRule, metadata : [String : String]) {
self.elements = elements
self.rule = rule
self.currentPerfomanceLog.start()
self.metadata = metadata
}
func elementsBetweenCurrentPosition( and newPosition : Position ) -> [Element]? {
let newIdx : Int
var isForward = true
switch newPosition {
case .backward(let positions):
isForward = false
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
let range : ClosedRange<Int> = ( isForward ) ? self.pointer...newIdx : newIdx...self.pointer
return Array(self.elements[range])
}
func element( for position : Position ) -> Element? {
let newIdx : Int
switch position {
case .backward(let positions):
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
return self.elements[newIdx]
}
func positionIsEqualTo( character : Character, direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return validElement.character == character
}
func positionContains( characters : [Character], direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return characters.contains(validElement.character)
}
func isEscaped() -> Bool {
let isEscaped = self.positionContains(characters: self.rule.escapeCharacters, direction: .backward(1))
if isEscaped {
self.elements[self.pointer - 1].type = .escape
}
return isEscaped
}
func range( for tag : String? ) -> ClosedRange<Int>? {
guard let tag = tag else {
return nil
}
guard let openChar = tag.first else {
return nil
}
if self.pointer == self.elements.count {
return nil
}
if self.elements[self.pointer].character != openChar {
return nil
}
if isEscaped() {
return nil
}
let range : ClosedRange<Int>
if tag.count > 1 {
guard let elements = self.elementsBetweenCurrentPosition(and: .forward(tag.count - 1) ) else {
return nil
}
// If it's already a tag, then it should be ignored
if elements.filter({ $0.type != .string }).count > 0 {
return nil
}
if elements.map( { String($0.character) }).joined() != tag {
return nil
}
let endIdx = (self.pointer + tag.count - 1)
for i in self.pointer...endIdx {
self.elements[i].type = .tag
}
range = self.pointer...endIdx
self.pointer += tag.count
} else {
// If it's already a tag, then it should be ignored
if self.elements[self.pointer].type != .string {
return nil
}
self.elements[self.pointer].type = .tag
range = self.pointer...self.pointer
self.pointer += 1
}
return range
}
func resetTagGroup( withID id : String ) {
if let idx = self.tagGroups.firstIndex(where: { $0.groupID == id }) {
for range in self.tagGroups[idx].tagRanges {
self.resetTag(in: range)
}
self.tagGroups.remove(at: idx)
}
self.isMetadataOpen = false
}
func resetTag( in range : ClosedRange<Int>) {
for idx in range {
self.elements[idx].type = .string
}
}
func resetLastTag( for range : inout [ClosedRange<Int>]) {
guard let last = range.last else {
return
}
for idx in last {
self.elements[idx].type = .string
}
}
func closeTag( _ tag : String, withGroupID id : String ) {
guard let tagIdx = self.tagGroups.firstIndex(where: { $0.groupID == id }) else {
return
}
var metadataString = ""
if self.isMetadataOpen {
let metadataCloseRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let metadataOpenRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if metadataOpenRange.upperBound + 1 == (metadataCloseRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
for idx in (metadataOpenRange.upperBound)...(metadataCloseRange.lowerBound) {
self.elements[idx].type = .metadata
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
}
let key = self.elements[metadataOpenRange.upperBound + 1..<metadataCloseRange.lowerBound].map( { String( $0.character )}).joined()
if self.rule.metadataLookup {
metadataString = self.metadata[key] ?? ""
} else {
metadataString = key
}
}
}
let closeRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let openRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if self.rule.balancedTags && closeRange.count != openRange.count {
self.tagGroups[tagIdx].tagRanges.append(openRange)
self.tagGroups[tagIdx].tagRanges.append(closeRange)
return
}
var shouldRemove = true
var styles : [CharacterStyling] = []
if openRange.upperBound + 1 == (closeRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
var remainingTags = min(openRange.upperBound - openRange.lowerBound, closeRange.upperBound - closeRange.lowerBound) + 1
while remainingTags > 0 {
if remainingTags >= self.rule.maxTags {
remainingTags -= self.rule.maxTags
if let style = self.rule.styles[ self.rule.maxTags ] {
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
if let style = self.rule.styles[remainingTags] {
remainingTags -= remainingTags
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
for idx in (openRange.upperBound)...(closeRange.lowerBound) {
self.elements[idx].styles.append(contentsOf: styles)
self.elements[idx].metadata.append(metadataString)
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
if self.rule.shouldCancelRemainingRules {
self.elements[idx].boundaryCount = 1000
}
}
if self.rule.isRepeatingTag {
let difference = ( openRange.upperBound - openRange.lowerBound ) - (closeRange.upperBound - closeRange.lowerBound)
switch difference {
case 1...:
shouldRemove = false
self.tagGroups[tagIdx].count = difference
self.tagGroups[tagIdx].tagRanges.append( openRange.upperBound - (abs(difference) - 1)...openRange.upperBound )
case ...(-1):
for idx in closeRange.upperBound - (abs(difference) - 1)...closeRange.upperBound {
self.elements[idx].type = .string
}
default:
break
}
}
}
if shouldRemove {
self.tagGroups.removeAll(where: { $0.groupID == id })
}
self.isMetadataOpen = false
}
func emptyRanges( _ ranges : inout [ClosedRange<Int>] ) {
while !ranges.isEmpty {
self.resetLastTag(for: &ranges)
ranges.removeLast()
}
}
func scanNonRepeatingTags() {
var groupID = ""
let closeTag = self.rule.tag(for: .close)?.tag
let metadataOpen = self.rule.tag(for: .metadataOpen)?.tag
let metadataClose = self.rule.tag(for: .metadataClose)?.tag
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if let range = self.range(for: metadataClose) {
if self.isMetadataOpen {
guard let groupIdx = self.tagGroups.firstIndex(where: { $0.groupID == groupID }) else {
self.pointer += 1
continue
}
guard !self.tagGroups.isEmpty else {
self.resetTagGroup(withID: groupID)
continue
}
guard self.isMetadataOpen else {
self.resetTagGroup(withID: groupID)
continue
}
if self.enableLog {
os_log("Closing metadata tag found. Closing tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.tagGroups[groupIdx].tagRanges.append(range)
self.closeTag(closeTag!, withGroupID: groupID)
self.isMetadataOpen = false
continue
} else {
self.resetTag(in: range)
self.pointer -= metadataClose!.count
}
}
if let openRange = self.range(for: self.rule.primaryTag.tag) {
if self.isMetadataOpen {
self.resetTagGroup(withID: groupID)
}
let tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
if self.enableLog {
os_log("New open tag found. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
if self.rule.isRepeatingTag {
}
self.tagGroups.append(tagGroup)
continue
}
if let range = self.range(for: closeTag) {
guard !self.tagGroups.isEmpty else {
if self.enableLog {
os_log("No open tags exist, resetting this close tag", log: OSLog.swiftyScanner, type:.info)
}
self.resetTag(in: range)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
groupID = self.tagGroups[self.tagGroups.count - 1].groupID
if self.enableLog {
os_log("New close tag found. Appending to group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
guard metadataOpen != nil else {
if self.enableLog {
os_log("No metadata tags exist, closing valid tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.closeTag(closeTag!, withGroupID: groupID)
continue
}
guard self.pointer != self.elements.count else {
continue
}
guard let range = self.range(for: metadataOpen) else {
if self.enableLog {
os_log("No metadata tag found, resetting group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTagGroup(withID: groupID)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
self.isMetadataOpen = true
continue
}
if let range = self.range(for: metadataOpen) {
if self.enableLog {
os_log("Multiple open metadata tags found!", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTag(in: range)
self.resetTagGroup(withID: groupID)
self.isMetadataOpen = false
continue
}
self.pointer += 1
}
}
func scanRepeatingTags() {
var groupID = ""
let escapeCharacters = "" //self.rule.escapeCharacters.map( { String( $0 ) }).joined()
let unionSet = spaceAndNewLine.union(CharacterSet(charactersIn: escapeCharacters))
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if var openRange = self.range(for: self.rule.primaryTag.tag) {
if self.elements[openRange].first?.boundaryCount == 1000 {
self.resetTag(in: openRange)
continue
}
var count = 1
var tagType : RepeatingTagType = .open
if let prevElement = self.element(for: .backward(self.rule.primaryTag.tag.count + 1)) {
if !unionSet.containsUnicodeScalars(of: prevElement.character) {
tagType = .either
}
} else {
tagType = .open
}
while let nextRange = self.range(for: self.rule.primaryTag.tag) {
count += 1
openRange = openRange.lowerBound...nextRange.upperBound
}
if self.rule.minTags > 1 {
if (openRange.upperBound - openRange.lowerBound) + 1 < self.rule.minTags {
self.resetTag(in: openRange)
os_log("Tag does not meet minimum length", log: .swiftyScanner, type: .info)
continue
}
}
var validTagGroup = true
if let nextElement = self.element(for: .forward(0)) {
if unionSet.containsUnicodeScalars(of: nextElement.character) {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
} else {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
if !validTagGroup {
if self.enableLog {
os_log("Tag has whitespace on both sides", log: .swiftyScanner, type: .info)
}
self.resetTag(in: openRange)
continue
}
if let idx = tagGroups.firstIndex(where: { $0.groupID == groupID }) {
if tagType == .either {
if tagGroups[idx].count == count {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
if let last = self.tagGroups.last {
groupID = last.groupID
}
continue
}
} else {
if let prevRange = tagGroups[idx].tagRanges.first {
if self.elements[prevRange].first?.boundaryCount == self.elements[openRange].first?.boundaryCount {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
}
}
continue
}
}
var tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
tagGroup.tagType = tagType
tagGroup.count = count
if self.enableLog {
os_log("New open tag found with characters %@. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info, self.elements[openRange].map( { String($0.character) }).joined(), groupID)
}
self.tagGroups.append(tagGroup)
continue
}
self.pointer += 1
}
}
func scan() -> [Element] {
guard self.elements.filter({ $0.type == .string }).map({ String($0.character) }).joined().contains(self.rule.primaryTag.tag) else {
return self.elements
}
self.currentPerfomanceLog.tag(with: "Beginning \(self.rule.primaryTag.tag)")
if self.enableLog {
os_log("RULE: %@", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
if self.rule.isRepeatingTag {
self.scanRepeatingTags()
} else {
self.scanNonRepeatingTags()
}
for tagGroup in self.tagGroups {
self.resetTagGroup(withID: tagGroup.groupID)
}
if self.enableLog {
for element in self.elements {
print(element)
}
}
return self.elements
}
}
@@ -0,0 +1,146 @@
//
// 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")
static let performance = OSLog(subsystem: subsystem, category: "Peformance")
}
public class SwiftyTokeniser {
let rules : [CharacterRule]
var replacements : [String : [Token]] = [:]
var enableLog = (ProcessInfo.processInfo.environment["SwiftyTokeniserLogging"] != nil)
let totalPerfomanceLog = PerformanceLog(with: "SwiftyTokeniserPerformanceLogging", identifier: "Tokeniser Total Run Time", log: OSLog.performance)
let currentPerfomanceLog = PerformanceLog(with: "SwiftyTokeniserPerformanceLogging", identifier: "Tokeniser Current", log: OSLog.performance)
public var metadataLookup : [String : String] = [:]
let newlines = CharacterSet.newlines
let spaces = CharacterSet.whitespaces
public init( with rules : [CharacterRule] ) {
self.rules = rules
self.totalPerfomanceLog.start()
}
deinit {
self.totalPerfomanceLog.end()
}
/// 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] {
let currentTokens = [Token(type: .string, inputString: inputString)]
guard rules.count > 0 else {
return currentTokens
}
var mutableRules = self.rules
if inputString.isEmpty {
return [Token(type: .string, inputString: "", characterStyles: [])]
}
self.currentPerfomanceLog.start()
var elementArray : [Element] = []
for char in inputString {
if newlines.containsUnicodeScalars(of: char) {
let element = Element(character: char, type: .newline)
elementArray.append(element)
continue
}
if spaces.containsUnicodeScalars(of: char) {
let element = Element(character: char, type: .space)
elementArray.append(element)
continue
}
let element = Element(character: char, type: .string)
elementArray.append(element)
}
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
if enableLog {
os_log("------------------------------", log: .tokenising, type: .info)
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
}
self.currentPerfomanceLog.tag(with: "(start rule %@)")
let scanner = SwiftyScanner(withElements: elementArray, rule: nextRule, metadata: self.metadataLookup)
elementArray = scanner.scan()
}
var output : [Token] = []
var lastElement = elementArray.first!
func empty( _ string : inout String, into tokens : inout [Token] ) {
guard !string.isEmpty else {
return
}
var token = Token(type: .string, inputString: string)
token.metadataStrings.append(contentsOf: lastElement.metadata)
token.characterStyles = lastElement.styles
string.removeAll()
tokens.append(token)
}
var accumulatedString = ""
for element in elementArray {
guard element.type != .escape else {
continue
}
guard element.type == .string || element.type == .space || element.type == .newline else {
empty(&accumulatedString, into: &output)
continue
}
if lastElement.styles as? [CharacterStyle] != element.styles as? [CharacterStyle] {
empty(&accumulatedString, into: &output)
}
accumulatedString.append(element.character)
lastElement = element
}
empty(&accumulatedString, into: &output)
self.currentPerfomanceLog.tag(with: "(finished all rules)")
if enableLog {
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
os_log("==================================", log: .tokenising, type: .info)
}
return output
}
}
extension String {
func repeating( _ max : Int ) -> String {
var output = self
for _ in 1..<max {
output += self
}
return output
}
}
+81
View File
@@ -0,0 +1,81 @@
//
// Token.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
// Tag definition
public protocol CharacterStyling {
func isEqualTo( _ other : CharacterStyling ) -> Bool
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case string
case escape
case replacement
}
public struct Token {
public let id = UUID().uuidString
public let type : TokenType
public let inputString : String
public var metadataStrings : [String] = []
public internal(set) var group : Int = 0
public internal(set) var characterStyles : [CharacterStyling] = []
public internal(set) var count : Int = 0
public internal(set) var shouldSkip : Bool = false
public internal(set) var tokenIndex : Int = -1
public internal(set) var isProcessed : Bool = false
public internal(set) var isMetadata : Bool = false
public var children : [Token] = []
public var outputString : String {
get {
switch self.type {
case .repeatingTag:
if count <= 0 {
return ""
} else {
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
return String(inputString[range])
}
case .openTag, .closeTag, .intermediateTag:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .escape, .string:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .replacement:
return self.inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
if type == .repeatingTag {
self.count = inputString.count
}
}
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
newToken.metadataStrings = self.metadataStrings
newToken.isMetadata = self.isMetadata
newToken.isProcessed = self.isProcessed
return newToken
}
}
extension Sequence where Iterator.Element == Token {
var oslogDisplay: String {
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
}
}
-322
View File
@@ -1,322 +0,0 @@
//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 600))
PlaygroundPage.current.liveView = containerView
let label = UITextView(frame: containerView.frame)
containerView.addSubview(label)
var foundCharacters : String = ""
var matchedCharacters : String = "\\Some string ''\\"
if let hasRange = matchedCharacters.range(of: "\\") {
let newRange = hasRange.lowerBound..<hasRange.upperBound
foundCharacters = foundCharacters + matchedCharacters[newRange]
matchedCharacters.removeSubrange(newRange)
}
//
//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()
//}
//
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios' last-migration='0900'>
<timeline fileName='timeline.xctimeline'/>
</playground>
+8 -4
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SwiftyMarkdown"
s.version = "0.5.0"
s.version = "1.2.4"
s.summary = "Converts Markdown to NSAttributed String"
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
s.license = 'MIT'
@@ -8,10 +8,14 @@ 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.tvos.deployment_target = "9.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.source_files = 'Sources/SwiftyMarkdown/**/*'
s.swift_version = "5.0"
end
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
@@ -15,11 +15,11 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.5.0</string>
<string>1.2.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>19</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
+380 -264
View File
@@ -6,119 +6,207 @@
objectVersion = 46;
objects = {
/* Begin PBXAggregateTarget section */
"SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget" /* SwiftyMarkdownPackageTests */ = {
isa = PBXAggregateTarget;
buildConfigurationList = OBJ_54 /* Build configuration list for PBXAggregateTarget "SwiftyMarkdownPackageTests" */;
buildPhases = (
);
dependencies = (
OBJ_57 /* PBXTargetDependency */,
);
name = SwiftyMarkdownPackageTests;
productName = SwiftyMarkdownPackageTests;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
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 */; };
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 */; };
F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */; };
F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CD23E8A88400EA665D /* Token.swift */; };
F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */; };
F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */; };
OBJ_40 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* String+SwiftyMarkdown.swift */; };
OBJ_41 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* SwiftyLineProcessor.swift */; };
OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* SwiftyMarkdown+iOS.swift */; };
OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* SwiftyMarkdown+macOS.swift */; };
OBJ_44 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* SwiftyMarkdown.swift */; };
OBJ_45 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* SwiftyTokeniser.swift */; };
OBJ_52 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
OBJ_63 /* SwiftyMarkdownAttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */; };
OBJ_64 /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* SwiftyMarkdownCharacterTests.swift */; };
OBJ_65 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* SwiftyMarkdownLineTests.swift */; };
OBJ_66 /* SwiftyMarkdownLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* SwiftyMarkdownLinkTests.swift */; };
OBJ_67 /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */; };
OBJ_68 /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* XCTest+SwiftyMarkdown.swift */; };
OBJ_70 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */ = {
F4ACB6CC23E8A5C600EA665D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F4CE98781C8A921300D735C1 /* Project object */;
containerPortal = OBJ_1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = F4CE98801C8A921300D735C1;
remoteGlobalIDString = "SwiftyMarkdown::SwiftyMarkdownTests";
remoteInfo = SwiftyMarkdownTests;
};
F4B37A0723E507C900833479 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = OBJ_1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = "SwiftyMarkdown::SwiftyMarkdown";
remoteInfo = SwiftyMarkdown;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
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>"; };
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>"; };
F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyScanner.swift; sourceTree = "<group>"; };
F4ACB6CD23E8A88400EA665D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRule.swift; sourceTree = "<group>"; };
F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerfomanceLog.swift; sourceTree = "<group>"; };
OBJ_10 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
OBJ_11 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
OBJ_12 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
OBJ_13 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
OBJ_14 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = "<group>"; };
OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownAttributedStringTests.swift; sourceTree = "<group>"; };
OBJ_18 /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = "<group>"; };
OBJ_19 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = "<group>"; };
OBJ_20 /* SwiftyMarkdownLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLinkTests.swift; sourceTree = "<group>"; };
OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = "<group>"; };
OBJ_22 /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
OBJ_26 /* Playground */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Playground; sourceTree = SOURCE_ROOT; };
OBJ_27 /* Example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Example; sourceTree = SOURCE_ROOT; };
OBJ_28 /* Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources; sourceTree = SOURCE_ROOT; };
OBJ_29 /* fastlane */ = {isa = PBXFileReference; lastKnownFileType = folder; path = fastlane; sourceTree = SOURCE_ROOT; };
OBJ_30 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
OBJ_31 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
OBJ_32 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = "<group>"; };
OBJ_33 /* Gemfile.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile.lock; sourceTree = "<group>"; };
OBJ_34 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
OBJ_9 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
"SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"SwiftyMarkdown::SwiftyMarkdownTests::Product" /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
F4CE987D1C8A921300D735C1 /* Frameworks */ = {
OBJ_46 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98881C8A921300D735C1 /* Frameworks */ = {
OBJ_69 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 0;
files = (
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */,
OBJ_70 /* SwiftyMarkdown.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
F4CE98771C8A921300D735C1 = {
OBJ_15 /* Tests */ = {
isa = PBXGroup;
children = (
F4CE98831C8A921300D735C1 /* SwiftyMarkdown */,
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */,
F4CE98821C8A921300D735C1 /* Products */,
F4CE98E61C8AF01300D735C1 /* LICENSE */,
F4CE98E71C8AF01300D735C1 /* README.md */,
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */,
OBJ_16 /* SwiftyMarkdownTests */,
);
sourceTree = "<group>";
name = Tests;
sourceTree = SOURCE_ROOT;
};
F4CE98821C8A921300D735C1 /* Products */ = {
OBJ_16 /* SwiftyMarkdownTests */ = {
isa = PBXGroup;
children = (
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */,
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */,
OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */,
OBJ_18 /* SwiftyMarkdownCharacterTests.swift */,
OBJ_19 /* SwiftyMarkdownLineTests.swift */,
OBJ_20 /* SwiftyMarkdownLinkTests.swift */,
OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */,
OBJ_22 /* XCTest+SwiftyMarkdown.swift */,
);
name = SwiftyMarkdownTests;
path = Tests/SwiftyMarkdownTests;
sourceTree = SOURCE_ROOT;
};
OBJ_23 /* Products */ = {
isa = PBXGroup;
children = (
"SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */,
"SwiftyMarkdown::SwiftyMarkdownTests::Product" /* SwiftyMarkdownTests.xctest */,
);
name = Products;
sourceTree = "<group>";
sourceTree = BUILT_PRODUCTS_DIR;
};
F4CE98831C8A921300D735C1 /* SwiftyMarkdown */ = {
OBJ_5 = {
isa = PBXGroup;
children = (
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */,
F4CE98861C8A921300D735C1 /* Info.plist */,
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */,
OBJ_6 /* Package.swift */,
OBJ_7 /* Sources */,
OBJ_15 /* Tests */,
OBJ_23 /* Products */,
OBJ_26 /* Playground */,
OBJ_27 /* Example */,
OBJ_28 /* Resources */,
OBJ_29 /* fastlane */,
OBJ_30 /* LICENSE */,
OBJ_31 /* README.md */,
OBJ_32 /* Gemfile */,
OBJ_33 /* Gemfile.lock */,
OBJ_34 /* SwiftyMarkdown.podspec */,
);
path = SwiftyMarkdown;
sourceTree = "<group>";
};
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */ = {
OBJ_7 /* Sources */ = {
isa = PBXGroup;
children = (
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */,
F4CE98921C8A921300D735C1 /* Info.plist */,
OBJ_8 /* SwiftyMarkdown */,
);
path = SwiftyMarkdownTests;
sourceTree = "<group>";
name = Sources;
sourceTree = SOURCE_ROOT;
};
OBJ_8 /* SwiftyMarkdown */ = {
isa = PBXGroup;
children = (
OBJ_9 /* String+SwiftyMarkdown.swift */,
OBJ_10 /* SwiftyLineProcessor.swift */,
OBJ_11 /* SwiftyMarkdown+iOS.swift */,
OBJ_12 /* SwiftyMarkdown+macOS.swift */,
OBJ_13 /* SwiftyMarkdown.swift */,
OBJ_14 /* SwiftyTokeniser.swift */,
F4ACB6CD23E8A88400EA665D /* Token.swift */,
F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */,
F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */,
F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */,
);
name = SwiftyMarkdown;
path = Sources/SwiftyMarkdown;
sourceTree = SOURCE_ROOT;
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
F4CE987E1C8A921300D735C1 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
F4CE98801C8A921300D735C1 /* SwiftyMarkdown */ = {
"SwiftyMarkdown::SwiftPMPackageDescription" /* SwiftyMarkdownPackageDescription */ = {
isa = PBXNativeTarget;
buildConfigurationList = F4CE98951C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */;
buildConfigurationList = OBJ_48 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownPackageDescription" */;
buildPhases = (
F4CE987C1C8A921300D735C1 /* Sources */,
F4CE987D1C8A921300D735C1 /* Frameworks */,
F4CE987E1C8A921300D735C1 /* Headers */,
F4CE987F1C8A921300D735C1 /* Resources */,
OBJ_51 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = SwiftyMarkdownPackageDescription;
productName = SwiftyMarkdownPackageDescription;
productType = "com.apple.product-type.framework";
};
"SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_36 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */;
buildPhases = (
OBJ_39 /* Sources */,
OBJ_46 /* Frameworks */,
);
buildRules = (
);
@@ -126,320 +214,348 @@
);
name = SwiftyMarkdown;
productName = SwiftyMarkdown;
productReference = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */;
productReference = "SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */;
productType = "com.apple.product-type.framework";
};
F4CE988A1C8A921300D735C1 /* SwiftyMarkdownTests */ = {
"SwiftyMarkdown::SwiftyMarkdownTests" /* SwiftyMarkdownTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = F4CE98981C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */;
buildConfigurationList = OBJ_59 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */;
buildPhases = (
F4CE98871C8A921300D735C1 /* Sources */,
F4CE98881C8A921300D735C1 /* Frameworks */,
F4CE98891C8A921300D735C1 /* Resources */,
OBJ_62 /* Sources */,
OBJ_69 /* Frameworks */,
);
buildRules = (
);
dependencies = (
F4CE988E1C8A921300D735C1 /* PBXTargetDependency */,
OBJ_71 /* PBXTargetDependency */,
);
name = SwiftyMarkdownTests;
productName = SwiftyMarkdownTests;
productReference = F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */;
productReference = "SwiftyMarkdown::SwiftyMarkdownTests::Product" /* SwiftyMarkdownTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F4CE98781C8A921300D735C1 /* Project object */ = {
OBJ_1 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 0900;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4CE98801C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 0900;
};
F4CE988A1C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
};
};
LastSwiftMigration = 9999;
LastUpgradeCheck = 9999;
};
buildConfigurationList = F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = F4CE98771C8A921300D735C1;
productRefGroup = F4CE98821C8A921300D735C1 /* Products */;
mainGroup = OBJ_5;
productRefGroup = OBJ_23 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
F4CE98801C8A921300D735C1 /* SwiftyMarkdown */,
F4CE988A1C8A921300D735C1 /* SwiftyMarkdownTests */,
"SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */,
"SwiftyMarkdown::SwiftPMPackageDescription" /* SwiftyMarkdownPackageDescription */,
"SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget" /* SwiftyMarkdownPackageTests */,
"SwiftyMarkdown::SwiftyMarkdownTests" /* SwiftyMarkdownTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
F4CE987F1C8A921300D735C1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */,
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98891C8A921300D735C1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F4CE987C1C8A921300D735C1 /* Sources */ = {
OBJ_39 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 0;
files = (
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */,
OBJ_40 /* String+SwiftyMarkdown.swift in Sources */,
OBJ_41 /* SwiftyLineProcessor.swift in Sources */,
F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */,
F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */,
OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */,
OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */,
OBJ_44 /* SwiftyMarkdown.swift in Sources */,
OBJ_45 /* SwiftyTokeniser.swift in Sources */,
F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */,
F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4CE98871C8A921300D735C1 /* Sources */ = {
OBJ_51 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 0;
files = (
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */,
OBJ_52 /* Package.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_62 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_63 /* SwiftyMarkdownAttributedStringTests.swift in Sources */,
OBJ_64 /* SwiftyMarkdownCharacterTests.swift in Sources */,
OBJ_65 /* SwiftyMarkdownLineTests.swift in Sources */,
OBJ_66 /* SwiftyMarkdownLinkTests.swift in Sources */,
OBJ_67 /* SwiftyMarkdownPerformanceTests.swift in Sources */,
OBJ_68 /* XCTest+SwiftyMarkdown.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
F4CE988E1C8A921300D735C1 /* PBXTargetDependency */ = {
OBJ_57 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F4CE98801C8A921300D735C1 /* SwiftyMarkdown */;
targetProxy = F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */;
target = "SwiftyMarkdown::SwiftyMarkdownTests" /* SwiftyMarkdownTests */;
targetProxy = F4ACB6CC23E8A5C600EA665D /* PBXContainerItemProxy */;
};
OBJ_71 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = "SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */;
targetProxy = F4B37A0723E507C900833479 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
F4CE98931C8A921300D735C1 /* Debug */ = {
OBJ_3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
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_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_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";
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_NS_ASSERTIONS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
"SWIFT_PACKAGE=1",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
MTL_ENABLE_DEBUG_INFO = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
USE_HEADERMAP = NO;
};
name = Debug;
};
F4CE98941C8A921300D735C1 /* Release */ = {
OBJ_37 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CURRENT_PROJECT_VERSION = 1;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
MACOSX_DEPLOYMENT_TARGET = 10.12;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = SwiftyMarkdown;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = SwiftyMarkdown;
TVOS_DEPLOYMENT_TARGET = 11.0;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Debug;
};
OBJ_38 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CURRENT_PROJECT_VERSION = 1;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
MACOSX_DEPLOYMENT_TARGET = 10.12;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = SwiftyMarkdown;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = SwiftyMarkdown;
TVOS_DEPLOYMENT_TARGET = 11.0;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Release;
};
OBJ_4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
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_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_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 = 19;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_OPTIMIZATION_LEVEL = s;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"SWIFT_PACKAGE=1",
);
MACOSX_DEPLOYMENT_TARGET = 10.10;
OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
USE_HEADERMAP = NO;
};
name = Release;
};
F4CE98961C8A921300D735C1 /* Debug */ = {
OBJ_49 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BITCODE_GENERATION_MODE = bitcode;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
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;
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;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F4CE98971C8A921300D735C1 /* Release */ = {
OBJ_50 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BITCODE_GENERATION_MODE = bitcode;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
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;
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;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1";
SWIFT_VERSION = 5.0;
};
name = Release;
};
F4CE98991C8A921300D735C1 /* Debug */ = {
OBJ_55 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
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_VERSION = 3.0;
};
name = Debug;
};
F4CE989A1C8A921300D735C1 /* Release */ = {
OBJ_56 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
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_VERSION = 3.0;
};
name = Release;
};
OBJ_60 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = SwiftyMarkdownTests;
TVOS_DEPLOYMENT_TARGET = 11.0;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Debug;
};
OBJ_61 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = SwiftyMarkdownTests;
TVOS_DEPLOYMENT_TARGET = 11.0;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */ = {
OBJ_2 /* Build configuration list for PBXProject "SwiftyMarkdown" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F4CE98931C8A921300D735C1 /* Debug */,
F4CE98941C8A921300D735C1 /* Release */,
OBJ_3 /* Debug */,
OBJ_4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F4CE98951C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */ = {
OBJ_36 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F4CE98961C8A921300D735C1 /* Debug */,
F4CE98971C8A921300D735C1 /* Release */,
OBJ_37 /* Debug */,
OBJ_38 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F4CE98981C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */ = {
OBJ_48 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownPackageDescription" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F4CE98991C8A921300D735C1 /* Debug */,
F4CE989A1C8A921300D735C1 /* Release */,
OBJ_49 /* Debug */,
OBJ_50 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_54 /* Build configuration list for PBXAggregateTarget "SwiftyMarkdownPackageTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_55 /* Debug */,
OBJ_56 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_59 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_60 /* Debug */,
OBJ_61 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = F4CE98781C8A921300D735C1 /* Project object */;
rootObject = OBJ_1 /* Project object */;
}
@@ -4,4 +4,4 @@
<FileRef
location = "self:">
</FileRef>
</Workspace>
</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,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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>SwiftyMarkdownPerformanceTests</key>
<dict>
<key>testThatFilesAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,44 @@
<?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.212</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
<key>maxPercentRelativeStandardDeviation</key>
<real>10</real>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.016</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.016</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,57 @@
<?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>88991ED5-B954-422F-B610-BDC9A4AEC008</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>400</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>8-Core Intel Core i9</string>
<key>cpuSpeedInMHz</key>
<integer>2400</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>16</integer>
<key>modelCode</key>
<string>MacBookPro16,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>x86_64h</string>
</dict>
<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
<dict>
<key>localComputer</key>
<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>
</dict>
</dict>
</dict>
</plist>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "9999"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -14,7 +14,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BlueprintIdentifier = "SwiftyMarkdown::SwiftyMarkdown"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
@@ -26,54 +26,67 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE988A1C8A921300D735C1"
BlueprintIdentifier = "SwiftyMarkdown::SwiftyMarkdownTests"
BuildableName = "SwiftyMarkdownTests.xctest"
BlueprintName = "SwiftyMarkdownTests"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
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>
<AdditionalOptions>
</AdditionalOptions>
<EnvironmentVariables>
<EnvironmentVariable
key = "SwiftyTokeniserLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerScanner"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerScannerPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyLineProcessorPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyScannerPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyTokeniserPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SwiftyMarkdownPerformanceLogging"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -81,15 +94,6 @@
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">
-19
View File
@@ -1,19 +0,0 @@
//
// SwiftyMarkdown.h
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for SwiftyMarkdown.
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
//! Project version string for SwiftyMarkdown.
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>
-482
View File
@@ -1,482 +0,0 @@
//
// SwiftyMarkdown.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import UIKit
@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? = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).fontName
public var color = UIColor.black
public var fontSize : CGFloat = 0.0
}
enum LineType : Int {
case h1, h2, h3, h4, h5, h6, body
}
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.
@objc open class SwiftyMarkdown: NSObject {
/// 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 : LineType = .body
let string : String
let instructionSet = 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 lines = self.string.components(separatedBy: CharacterSet.newlines)
var lineCount = 0
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
var skipLine = false
for theLine in lines {
lineCount += 1
if skipLine {
skipLine = false
continue
}
var line = theLine == "" ? " " : theLine
for heading in headings {
if let range = line.range(of: heading) , range.lowerBound == line.startIndex {
let startHeadingString = line.replacingCharacters(in: range, with: "")
// Remove ending
let endHeadingString = heading.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
line = startHeadingString.replacingOccurrences(of: endHeadingString, with: "").trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
currentType = LineType(rawValue: headings.index(of: heading)!)!
// We found a heading so break out of the inner loop
break
}
}
// Look for underlined headings
if lineCount < lines.count {
let nextLine = lines[lineCount]
let hasNonWhiteSpaceCharacters = (line.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.inverted) != nil)
if hasNonWhiteSpaceCharacters, let range = nextLine.range(of: "=") , range.lowerBound == nextLine.startIndex {
// Make H1
currentType = .h1
// We need to skip the next line
skipLine = true
}
if hasNonWhiteSpaceCharacters, let nextRange = nextLine.range(of: "-") , nextRange.lowerBound == nextLine.startIndex {
if let range = line.range(of: "-"), range.lowerBound == line.startIndex {
// This is a bullet list, not an `Alt-H2`, don't skip
} else {
// 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 = Scanner(string: line)
// We want to be aware of spaces
scanner.charactersToBeSkipped = nil
while !scanner.isAtEnd {
var string : NSString?
// Get all the characters up to the ones we are interested in
if scanner.scanUpToCharacters(from: instructionSet, into: &string) {
if let hasString = string as String? {
let bodyString = attributedStringFromString(hasString, withStyle: .none)
attributedString.append(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.whitespace()
set.formUnion(with: CharacterSet.punctuationCharacters)
if scanner.scanUpToCharacters(from: set as CharacterSet, into: nil) {
scanner.scanLocation = location
attributedString.append(self.attributedStringFromScanner(scanner))
} else if matchedCharacters == "[" {
scanner.scanLocation = location
attributedString.append(self.attributedStringFromScanner(scanner))
} else {
let charAtts = attributedStringFromString(matchedCharacters, withStyle: .none)
attributedString.append(charAtts)
}
}
} else {
attributedString.append(self.attributedStringFromScanner(scanner, atStartOfLine: true))
}
}
}
// Append a new line character to the end of the processed line
if lineCount < lines.count {
attributedString.append(NSAttributedString(string: "\n"))
}
currentType = .body
}
return attributedString
}
func attributedStringFromScanner( _ scanner : Scanner, atStartOfLine start : Bool = false) -> NSAttributedString {
var followingString : NSString?
let results = self.tagFromScanner(scanner)
var style = LineStyle.styleFromString(results.foundCharacters)
var attributes = [NSAttributedStringKey : AnyObject]()
if style == .link {
var linkText : NSString?
var linkURL : NSString?
let linkCharacters = CharacterSet(charactersIn: "]()")
scanner.scanUpToCharacters(from: linkCharacters, into: &linkText)
scanner.scanCharacters(from: linkCharacters, into: nil)
scanner.scanUpToCharacters(from: linkCharacters, into: &linkURL)
scanner.scanCharacters(from: linkCharacters, into: nil)
if let hasLink = linkText, let hasURL = linkURL {
followingString = hasLink
attributes[NSAttributedStringKey.link] = hasURL
} else {
style = .none
}
} else {
scanner.scanUpToCharacters(from: instructionSet, into: &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.append(attString)
}
let suffix = self.tagFromScanner(scanner)
attributedString.append(attributedStringFromString(suffix.escapedCharacters, withStyle: style))
return attributedString
}
func tagFromScanner( _ scanner : Scanner ) -> (foundCharacters : String, escapedCharacters : String) {
var matchedCharacters : String = ""
var tempCharacters : NSString?
// Scan the ones we are interested in
while scanner.scanCharacters(from: instructionSet, into: &tempCharacters) {
if let chars = tempCharacters as String? {
matchedCharacters = matchedCharacters + chars
}
}
var foundCharacters : String = ""
while matchedCharacters.contains("\\") {
if let hasRange = matchedCharacters.range(of: "\\") {
if matchedCharacters.characters.count > 1 {
let newRange = hasRange.lowerBound..<matchedCharacters.index(hasRange.upperBound, offsetBy: 1)
foundCharacters = foundCharacters + matchedCharacters[newRange].replacingOccurrences(of: "\\", with: "")
matchedCharacters.removeSubrange(newRange)
} else {
foundCharacters = matchedCharacters
break
}
}
}
return (matchedCharacters, foundCharacters)
}
// Make H1
func attributedStringFromString(_ string : String, withStyle style : LineStyle, attributes : [NSAttributedStringKey : AnyObject] = [:] ) -> NSAttributedString {
let textStyle : UIFontTextStyle
var fontName : String?
var attributes = attributes
var fontSize : CGFloat?
// What type are we and is there a font name set?
switch currentType {
case .h1:
fontName = h1.fontName
fontSize = h1.fontSize
if #available(iOS 9, *) {
textStyle = UIFontTextStyle.title1
} else {
textStyle = UIFontTextStyle.headline
}
attributes[NSAttributedStringKey.foregroundColor] = h1.color
case .h2:
fontName = h2.fontName
fontSize = h2.fontSize
if #available(iOS 9, *) {
textStyle = UIFontTextStyle.title2
} else {
textStyle = UIFontTextStyle.headline
}
attributes[NSAttributedStringKey.foregroundColor] = h2.color
case .h3:
fontName = h3.fontName
fontSize = h3.fontSize
if #available(iOS 9, *) {
textStyle = UIFontTextStyle.title2
} else {
textStyle = UIFontTextStyle.subheadline
}
attributes[NSAttributedStringKey.foregroundColor] = h3.color
case .h4:
fontName = h4.fontName
fontSize = h4.fontSize
textStyle = UIFontTextStyle.headline
attributes[NSAttributedStringKey.foregroundColor] = h4.color
case .h5:
fontName = h5.fontName
fontSize = h5.fontSize
textStyle = UIFontTextStyle.subheadline
attributes[NSAttributedStringKey.foregroundColor] = h5.color
case .h6:
fontName = h6.fontName
fontSize = h6.fontSize
textStyle = UIFontTextStyle.footnote
attributes[NSAttributedStringKey.foregroundColor] = h6.color
default:
fontName = body.fontName
fontSize = body.fontSize
textStyle = UIFontTextStyle.body
attributes[NSAttributedStringKey.foregroundColor] = body.color
break
}
// Check for code
if style == .code {
fontName = code.fontName
fontSize = code.fontSize
attributes[NSAttributedStringKey.foregroundColor] = code.color
}
if style == .link {
fontName = link.fontName
fontSize = link.fontSize
attributes[NSAttributedStringKey.foregroundColor] = link.color
}
// Fallback to body
if let _ = fontName {
} else {
fontName = body.fontName
}
fontSize = fontSize == 0.0 ? nil : fontSize
let font = UIFont.preferredFont(forTextStyle: textStyle)
let styleDescriptor = font.fontDescriptor
let styleSize = fontSize ?? styleDescriptor.fontAttributes[UIFontDescriptor.AttributeName.size] as? CGFloat ?? CGFloat(14)
var finalFont : UIFont
if let finalFontName = fontName, let font = UIFont(name: finalFontName, size: styleSize) {
finalFont = font
} else {
finalFont = UIFont.preferredFont(forTextStyle: textStyle)
}
let finalFontDescriptor = finalFont.fontDescriptor
if style == .italic {
if let italicDescriptor = finalFontDescriptor.withSymbolicTraits(.traitItalic) {
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
}
}
if style == .bold {
if let boldDescriptor = finalFontDescriptor.withSymbolicTraits(.traitBold) {
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
}
}
attributes[NSAttributedStringKey.font] = finalFont
return NSAttributedString(string: string, attributes: attributes)
}
}
@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12118" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12086"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftyMarkdownExample" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<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="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="16" y="20" width="343" height="601"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<dataDetectorType key="dataDetectorTypes" link="YES"/>
</textView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JK2-Lo-xRa">
<rect key="frame" x="164" y="629" width="47" height="30"/>
<state key="normal" title="Reload"/>
<connections>
<action selector="reloadText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="fGe-1g-NTo"/>
</connections>
</button>
</subviews>
<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="JK2-Lo-xRa" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="3Zx-lw-7hH"/>
<constraint firstAttribute="trailingMargin" secondItem="qZP-CU-74n" secondAttribute="trailing" id="F5p-iG-zTB"/>
<constraint firstItem="JK2-Lo-xRa" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" constant="8" id="Rod-d1-6Yb"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="JK2-Lo-xRa" secondAttribute="bottom" constant="8" id="Yc0-Xe-YPV"/>
<constraint firstItem="qZP-CU-74n" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="srf-u0-j0n"/>
</constraints>
</view>
<connections>
<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>
@@ -1,22 +0,0 @@
# 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.
## Features
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/)
Supports Alternative Headings
===
@@ -1,297 +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")
md = SwiftyMarkdown(string: headerStringWithBold)
XCTAssertEqual(md.attributedString().string, "Bold Header 1")
md = SwiftyMarkdown(string: headerStringWithItalic)
XCTAssertEqual(md.attributedString().string, "Header 2 With Italics")
}
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")
md = SwiftyMarkdown(string: h2String)
XCTAssertEqual(md.attributedString().string, "Header 2\nSome following text")
md = SwiftyMarkdown(string: h1StringWithBold)
XCTAssertEqual(md.attributedString().string, "Header 1 With Bold\nSome following text")
md = SwiftyMarkdown(string: h2StringWithItalic)
XCTAssertEqual(md.attributedString().string, "Header 2 With Italic\nSome following text")
md = SwiftyMarkdown(string: h2StringWithCode)
XCTAssertEqual(md.attributedString().string, "Header 2 With Code\nSome following text")
}
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")
md = SwiftyMarkdown(string: boldWithinString)
XCTAssertEqual(md.attributedString().string, "A string with a bold word")
md = SwiftyMarkdown(string: codeAtStartOfString)
XCTAssertEqual(md.attributedString().string, "\tCode (should be indented)")
md = SwiftyMarkdown(string: codeWithinString)
XCTAssertEqual(md.attributedString().string, "A string with code (should not be indented)")
md = SwiftyMarkdown(string: italicAtStartOfString)
XCTAssertEqual(md.attributedString().string, "An italicised string")
md = SwiftyMarkdown(string: italicWithinString)
XCTAssertEqual(md.attributedString().string, "A string with italicised text")
md = SwiftyMarkdown(string: multipleBoldWords)
XCTAssertEqual(md.attributedString().string, "A bold string with a mix of bold styles")
md = SwiftyMarkdown(string: multipleCodeWords)
XCTAssertEqual(md.attributedString().string, "\tA code string with multiple code instances")
md = SwiftyMarkdown(string: multipleItalicWords)
XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles")
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.")
}
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")
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
XCTAssertEqual(md.attributedString().string, "A string that should be italic")
}
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**")
md = SwiftyMarkdown(string: escapedBoldWithin)
XCTAssertEqual(md.attributedString().string, "A string with **escaped** asterisks")
md = SwiftyMarkdown(string: escapedItalicAtStart)
XCTAssertEqual(md.attributedString().string, "_A normal string_")
md = SwiftyMarkdown(string: escapedItalicWithin)
XCTAssertEqual(md.attributedString().string, "A string with _escaped_ underscores")
md = SwiftyMarkdown(string: escapedBackticksAtStart)
XCTAssertEqual(md.attributedString().string, "`A normal string`")
md = SwiftyMarkdown(string: escapedBacktickWithin)
XCTAssertEqual(md.attributedString().string, "A string with `escaped` backticks")
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalAtStart)
XCTAssertEqual(md.attributedString().string, "*A normal string*")
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalWithin)
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks")
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalAtStart)
XCTAssertEqual(md.attributedString().string, "*A normal string*")
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalWithin)
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks")
}
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() {
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")
md = SwiftyMarkdown(string: linkWithin)
XCTAssertEqual(md.attributedString().string, "A Link")
md = SwiftyMarkdown(string: headerLink)
XCTAssertEqual(md.attributedString().string, "Header link")
md = SwiftyMarkdown(string: multipleLinks)
XCTAssertEqual(md.attributedString().string, "Link 1, Link 2")
md = SwiftyMarkdown(string: syntaxErrorSquareBracketAtStart)
XCTAssertEqual(md.attributedString().string, "Link with missing square")
md = SwiftyMarkdown(string: syntaxErrorSquareBracketWithin)
XCTAssertEqual(md.attributedString().string, "A Link")
md = SwiftyMarkdown(string: syntaxErrorParenthesisAtStart)
XCTAssertEqual(md.attributedString().string, "Link with missing parenthesis")
md = SwiftyMarkdown(string: syntaxErrorParenthesisWithin)
XCTAssertEqual(md.attributedString().string, "A Link")
md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
XCTAssertEqual(md.attributedString().string, "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp")
// let mailtoAndTwitterLinks = "Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)"
// let md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
// XCTAssertEqual(md.attributedString().string, "Twitter @VoyageTravelApp")
}
/*
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 testInternationalCharactersInList() {
let extected = "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, extected)
}
}
+7
View File
@@ -0,0 +1,7 @@
import XCTest
import AppLibrarianTests
var tests = [XCTestCaseEntry]()
tests += AppLibrarianTests.allTests()
XCTMain(tests)
@@ -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,806 @@
//
// SwiftyMarkdownCharacterTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
@testable import SwiftyMarkdown
import XCTest
class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
func off_testIsolatedCase() {
challenge = TokenTest(input: "*\\***\\****b*\\***\\****\\", output: "***b***\\", tokens : [
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "*b**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "\\", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
return
challenge = TokenTest(input: """
An asterisk: *
Line break
""", output: """
An asterisk: *
Line break
""", tokens: [
Token(type: .string, inputString: "An asterisk: *", characterStyles: []),
Token(type: .string, inputString: "Line break", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
return
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [referenced link][link]\n[notLink]: https://www.neverendingvoyage.com/", output: "A [referenced link][link]", tokens: [
Token(type: .string, inputString: "A [referenced link][link]", characterStyles: [])
])
results = self.attempt(challenge, rules: [.links, .images, .referencedLinks])
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 0)
}
func testThatBoldTraitsAreRecognised() {
challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\\\*\\*A normal \\\\ string\\*\\*", output: "\\**A normal \\\\ string**", tokens: [
Token(type: .string, inputString: "\\**A normal \\\\ string**", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\**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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with 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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatCodeTraitsAreRecognised() {
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with `code` (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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "`A 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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [
Token(type: .string, inputString: "`A normal string`", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A lonely backtick: `", output: "A lonely backtick: `", tokens: [
Token(type: .string, inputString: "A lonely backtick: `", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "Two backticks followed by a full stop ``.", output: "Two backticks followed by a full stop ``.", tokens: [
Token(type: .string, inputString: "Two backticks followed by a full stop ``.", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatItalicTraitsAreParsedCorrectly() {
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with *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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_ 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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [
Token(type: .string, inputString: "_A normal string_", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: """
An asterisk: *
Line break
""", output: """
An asterisk: *
Line break
""", tokens: [
Token(type: .string, inputString: "An asterisk: *", characterStyles: []),
Token(type: .string, inputString: "Line break", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatStrikethroughTraitsAreRecognised() {
challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [
Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]),
Token(type: .string, inputString: "A crossed-out string", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
challenge = TokenTest(input: "A **Bold** string and a ~~removed~~crossed-out string", output: "A Bold string and a removedcrossed-out string", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " string and a ", characterStyles: []),
Token(type: .string, inputString: "removed", characterStyles: [CharacterStyle.strikethrough]),
Token(type: .string, inputString: "crossed-out string", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [
Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
}
func testThatMixedTraitsAreRecognised() {
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_, **followed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, followed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]),
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testForExtremeEscapeCombinations() {
challenge = TokenTest(input: "\\****b\\****", output: "*b*", tokens : [
Token(type: .string, inputString: "*", characterStyles: []),
Token(type: .string, inputString: "b*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "**\\**b*\\***", output: "*b*", tokens : [
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "b", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
// challenge = TokenTest(input: "Before *\\***\\****A bold string*\\***\\****\\ After", output: "Before ***A bold string***\\ After", tokens : [
// Token(type: .string, inputString: "Before ", characterStyles: []),
// Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
// Token(type: .string, inputString: "**", characterStyles: [CharacterStyle.bold]),
// Token(type: .string, inputString: "A bold string**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
// Token(type: .string, inputString: "\\ After", characterStyles: [])
// ])
// results = self.attempt(challenge)
// if results.stringTokens.count == challenge.tokens.count {
// for (idx, token) in results.stringTokens.enumerated() {
// XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
// XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
// }
// } else {
// XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
// }
// XCTAssertEqual(results.foundStyles, results.expectedStyles)
// XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatExtraCharactersAreHandles() {
challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ****bold**** word", output: "A string with a bold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with 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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with ```code`", output: "A string with ```code`", tokens : [
Token(type: .string, inputString: "A string with ```code`", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with ```code```", output: "A string with code", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
// 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 offtestAdvancedEscaping() {
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: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with 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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
let asteriskFullStop = "Two asterisks followed by a full stop: **."
let asteriskWithBold = "A **bold** word followed by an asterisk * "
let underscoreFullStop = "Two underscores followed by a full stop: __."
let asteriskComma = "An asterisk followed by a full stop: *, *"
let backtickSpace = "A backtick followed by a space: `"
let underscoreSpace = "An underscore followed by a space: _"
let backtickComma = "A backtick followed by a space: `, `"
let underscoreComma = "An underscore followed by a space: _, _"
let backtickWithCode = "A `code` word followed by a backtick ` "
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
var md = SwiftyMarkdown(string: backtickSpace)
SwiftyMarkdown.characterRules = self.defaultRules
XCTAssertEqual(md.attributedString().string, backtickSpace)
md = SwiftyMarkdown(string: underscoreSpace)
XCTAssertEqual(md.attributedString().string, underscoreSpace)
md = SwiftyMarkdown(string: asteriskFullStop)
XCTAssertEqual(md.attributedString().string, asteriskFullStop)
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 testReportedCrashingStrings() {
challenge = TokenTest(input: "[**\\!bang**](https://duckduckgo.com/bang)", output: "\\!bang", tokens: [
Token(type: .string, inputString: "\\!bang", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
}
@@ -0,0 +1,201 @@
//
// 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)
}
func testThatUnorderedListsAreHandledCorrectly() {
let dashBullets = StringTest(input: "An Unordered List\n- Item 1\n\t- Indented\n- Item 2", expectedOutput: "An Unordered List\n-\tItem 1\n\t-\tIndented\n-\tItem 2")
var md = SwiftyMarkdown(string: dashBullets.input)
md.bullet = "-"
XCTAssertEqual(md.attributedString().string, dashBullets.expectedOutput)
let starBullets = StringTest(input: "An Unordered List\n* Item 1\n\t* Indented\n* Item 2", expectedOutput: "An Unordered List\n-\tItem 1\n\t-\tIndented\n-\tItem 2")
md = SwiftyMarkdown(string: starBullets.input)
md.bullet = "-"
XCTAssertEqual(md.attributedString().string, starBullets.expectedOutput)
}
func testThatOrderedListsAreHandled() {
let dashBullets = StringTest(input: "An Ordered List\n1. Item 1\n\t1. Indented\n1. Item 2", expectedOutput: "An Ordered List\n1.\tItem 1\n\t1.\tIndented\n2.\tItem 2")
var md = SwiftyMarkdown(string: dashBullets.input)
XCTAssertEqual(md.attributedString().string, dashBullets.expectedOutput)
let moreComplicatedList = StringTest(input: """
A long ordered list:
1. Item 1
1. Item 2
1. First Indent 1
1. First Indent 2
1. Second Indent 1
1. First Indent 3
1. Second Indent 2
1. Item 3
A break
1. Item 1
1. Item 2
""", expectedOutput: """
A long ordered list:
1. Item 1
2. Item 2
1. First Indent 1
2. First Indent 2
1. Second Indent 1
3. First Indent 3
1. Second Indent 2
3. Item 3
A break
1. Item 1
2. Item 2
""")
md = SwiftyMarkdown(string: moreComplicatedList.input)
XCTAssertEqual(md.attributedString().string, moreComplicatedList.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 testThatYAMLMetadataIsRemoved() {
let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!\n\nWith A Heading\n---", expectedOutput: "Finally some Markdown!\n\nWith A Heading")
let md = SwiftyMarkdown(string: yaml.input)
XCTAssertEqual(md.attributedString().string, yaml.expectedOutput)
XCTAssertEqual(md.frontMatterAttributes.count, 8)
}
}
@@ -0,0 +1,734 @@
//
// SwiftyMarkdownCharacterTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
@testable import SwiftyMarkdown
import XCTest
class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
func testSingleLinkPositions() {
challenge = TokenTest(input: "[a](b)", output: "a", tokens: [
Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataStrings.first, "b")
} else {
XCTFail("Failed to find an open link tag")
}
challenge = TokenTest(input: "[Link at](http://voyagetravelapps.com/) start", output: "Link at start", tokens: [
Token(type: .string, inputString: "Link at", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " start")
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Failed to find an open link tag")
}
challenge = TokenTest(input: "A [link at end](http://voyagetravelapps.com/)", output: "A link at end", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "link at end", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [link in the](http://voyagetravelapps.com/) middle", output: "A link in the middle", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "link in the", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " middle", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testEscapedLinks() {
challenge = TokenTest(input: "\\[a](b)", output: "[a](b)", tokens: [
Token(type: .string, inputString: "[a](b)", characterStyles: [])
])
results = self.attempt(challenge, rules: [.images, .referencedLinks, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "![a](b)", output: "!a", tokens: [
Token(type: .string, inputString: "!", characterStyles: []),
Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testMultipleLinkPositions() {
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/)[Link 2](https://www.neverendingvoyage.com/)", output: "Link 1Link 2", tokens: [
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "String at start [Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "String at start Link 1, Link 2", tokens: [
Token(type: .string, inputString: "String at start ", characterStyles: []),
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "String at start [Link 1](http://voyagetravelapps.com/)[Link 2](https://www.neverendingvoyage.com/)", output: "String at start Link 1Link 2", tokens: [
Token(type: .string, inputString: "String at start ", characterStyles: []),
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
func testForAlternativeURLs() {
challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [
Token(type: .string, inputString: "Email us at ", characterStyles: []),
Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " Twitter ", characterStyles: []),
Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 2 {
XCTAssertEqual(links[0].metadataStrings.first, "mailto:simon@voyagetravelapps.com")
XCTAssertEqual(links[1].metadataStrings.first, "twitter://user?screen_name=VoyageTravelApp")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
func testForLinksMixedWithTokenCharacters() {
challenge = TokenTest(input: "Link ([Surrounded by parentheses](https://www.neverendingvoyage.com/))", output: "Link (Surrounded by parentheses)", tokens: [
Token(type: .string, inputString: "Link (", characterStyles: []),
Token(type: .string, inputString: "Surrounded by parentheses", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ")", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 1 {
XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
challenge = TokenTest(input: "[[Surrounded by square brackets](https://www.neverendingvoyage.com/)]", output: "[Surrounded by square brackets]", tokens: [
Token(type: .string, inputString: "[", characterStyles: []),
Token(type: .string, inputString: "Surrounded by square brackets", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: "]", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
if links.count == 1 {
XCTAssertEqual(links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect number of links found. Expecting 2, found \(links.count)")
}
}
func testMalformedLinks() {
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "[Link with missing parenthesis](http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "A [Link](http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[A link](((url)", output: "[A link](((url)", tokens: [
Token(type: .string, inputString: "[A link](((url)", characterStyles: [])
])
results = self.attempt(challenge, rules: [.images, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 0)
challenge = TokenTest(input: "[[a](((b)](c)", output: "[a](((b)", tokens: [
Token(type: .string, inputString: "[a](((b)", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.images, .links])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "c")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[Link with [second opening](http://voyagetravelapps.com/)", output: "[Link with second opening", tokens: [
Token(type: .string, inputString: "[Link with ", characterStyles: []),
Token(type: .string, inputString: "second opening", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "A [Link(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testMalformedLinksWithValidLinks() {
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a [valid link](http://voyagetravelapps.com/)", output: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a valid link", tokens: [
Token(type: .string, inputString: "[Link with missing parenthesis](http://voyagetravelapps.com/ followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/ followed by a [valid link](http://voyagetravelapps.com/)", output: "A [Link](http://voyagetravelapps.com/ followed by a valid link", tokens: [
Token(type: .string, inputString: "A [Link](http://voyagetravelapps.com/ followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/) followed by a [valid link](http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/) followed by a valid link", tokens: [
Token(type: .string, inputString: "[Link with missing square(http://voyagetravelapps.com/) followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/) followed by a [valid link](http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/) followed by a valid link", tokens: [
Token(type: .string, inputString: "A [Link(http://voyagetravelapps.com/) followed by a ", characterStyles: []),
Token(type: .string, inputString: "valid link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testLinksWithOtherStyles() {
challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A Bold ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [ CharacterStyle.link, CharacterStyle.bold])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.links.count, 1)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " ", characterStyles: []),
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)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testForImages() {
challenge = TokenTest(input: "An ![Image](imageName)", output: "An ", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.images.count)")
}
challenge = TokenTest(input: "An [![Image](imageName)](https://www.neverendingvoyage.com/)", output: "An ", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image, CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.attributedString.string, challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.images.count)")
}
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.last, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForReferencedImages() {
challenge = TokenTest(input: "A ![referenced image][image]\n[image]: imageName", output: "A ", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced image", characterStyles: [CharacterStyle.image])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.images.count == 1 {
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForReferencedLinks() {
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "A [referenced link][link]\n [link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
challenge = TokenTest(input: "An *\\*italic\\** [referenced link][a]\n[a]: link", output: "An *italic* referenced link", tokens: [
Token(type: .string, inputString: "An ", characterStyles: []),
Token(type: .string, inputString: "*italic*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge, rules: [.asterisks, .links, .referencedLinks])
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 1 {
XCTAssertEqual(results.links[0].metadataStrings.first, "link")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
func testForMixedLinkStyles() {
challenge = TokenTest(input: "A [referenced link][link] and a [regular link](http://voyagetravelapps.com/)\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link and a regular link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " and a ", characterStyles: []),
Token(type: .string, inputString: "regular link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
if results.stringTokens.count == challenge.tokens.count {
for (idx, token) in results.stringTokens.enumerated() {
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
}
} else {
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
}
XCTAssertEqual(results.foundStyles, results.expectedStyles)
if results.links.count == 2 {
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
XCTAssertEqual(results.links[1].metadataStrings.first, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
}
}
}
@@ -0,0 +1,44 @@
//
// 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() {
let url = self.resourceURL(for: "test.md")
measure {
guard let md = SwiftyMarkdown(url: url) else {
XCTFail("Failed to load file")
return
}
_ = 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)
}
}
}
@@ -0,0 +1,96 @@
//
// 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
struct ChallengeReturn {
let tokens : [Token]
let stringTokens : [Token]
let links : [Token]
let images : [Token]
let attributedString : NSAttributedString
let foundStyles : [[CharacterStyle]]
let expectedStyles : [[CharacterStyle]]
}
enum Rule {
case asterisks
case backticks
case underscores
case images
case links
case referencedLinks
case referencedImages
case tildes
func asCharacterRule() -> CharacterRule {
switch self {
case .images:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && !$0.metadataLookup }).first!
case .links:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && !$0.metadataLookup }).first!
case .backticks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "`" }).first!
case .tildes:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "~" }).first!
case .asterisks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "*" }).first!
case .underscores:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "_" }).first!
case .referencedLinks:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && $0.metadataLookup }).first!
case .referencedImages:
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && $0.metadataLookup }).first!
}
}
}
class SwiftyMarkdownCharacterTests : XCTestCase {
let defaultRules = SwiftyMarkdown.characterRules
var challenge : TokenTest!
var results : ChallengeReturn!
func attempt( _ challenge : TokenTest, rules : [Rule]? = nil ) -> ChallengeReturn {
if let validRules = rules {
SwiftyMarkdown.characterRules = validRules.map({ $0.asCharacterRule() })
} else {
SwiftyMarkdown.characterRules = self.defaultRules
}
let md = SwiftyMarkdown(string: challenge.input)
md.applyAttachments = false
let attributedString = md.attributedString()
let tokens : [Token] = md.previouslyFoundTokens
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let linkTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
let imageTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
return ChallengeReturn(tokens: tokens, stringTokens: stringTokens, links : linkTokens, images: imageTokens, attributedString: attributedString, foundStyles: existentTokenStyles, expectedStyles : expectedStyles)
}
}
extension XCTestCase {
func resourceURL(for filename : String ) -> URL {
let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
return thisDirectory.appendingPathComponent("Resources", isDirectory: true).appendingPathComponent(filename)
}
}
+1 -1
View File
@@ -20,4 +20,4 @@ $scheme = "SwiftyMarkdown"
$spec = "SwiftyMarkdown.podspec"
$project = './SwiftyMarkdown.xcodeproj'
import "../../Fastlane/FastfilePods"
import "/Users/simon/Developer/Fastlane/FastfilePods"
+32 -42
View File
@@ -1,38 +1,26 @@
fastlane documentation
================
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
```sh
xcode-select --install
```
## Choose your installation method:
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
<table width="100%" >
<tr>
<th width="33%"><a href="http://brew.sh">Homebrew</a></td>
<th width="33%">Installer Script</td>
<th width="33%">Rubygems</td>
</tr>
<tr>
<td width="33%" align="center">macOS</td>
<td width="33%" align="center">macOS</td>
<td width="33%" align="center">macOS or Linux with Ruby 2.0.0 or above</td>
</tr>
<tr>
<td width="33%"><code>brew cask install fastlane</code></td>
<td width="33%"><a href="https://download.fastlane.tools">Download the zip file</a>. Then double click on the <code>install</code> script (or run it in a terminal window).</td>
<td width="33%"><code>sudo gem install fastlane -NV</code></td>
</tr>
</table>
# Available Actions
## iOS
### ios patch
```sh
[bundle exec] fastlane ios patch
```
fastlane ios patch
```
This does the following:
@@ -43,13 +31,12 @@ This does the following:
- Bumps the patch version
- Pushes it to the remote repository
- Updates the spec repository
### ios minor
```sh
[bundle exec] fastlane ios minor
```
fastlane ios minor
```
This does the following:
@@ -60,13 +47,12 @@ This does the following:
- Bumps the minor version
- Pushes it to the remote repository
- Updates the spec repository
### ios major
```sh
[bundle exec] fastlane ios major
```
fastlane ios major
```
This does the following:
@@ -77,22 +63,26 @@ This does the following:
- Bumps the major version
- Pushes it to the remote repository
- Updates the spec repository
### ios test
```sh
[bundle exec] fastlane ios test
```
fastlane ios test
```
### ios submit_pod
```
fastlane ios submit_pod
```sh
[bundle exec] fastlane ios submit_pod
```
Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to run tests, bump versions, and commit changes.
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).