Compare commits

...

73 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
77 changed files with 5337 additions and 2092 deletions
@@ -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,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>1.0.2</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,10 +279,15 @@
F4CE98A01C8AEF7D00D735C1 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 1120;
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4B4A44823E4E17400550249 = {
CreatedOnToolsVersion = 11.3.1;
DevelopmentTeam = 52T262DA8V;
ProvisioningStyle = Automatic;
};
F4CE98A71C8AEF7D00D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
@@ -261,48 +313,38 @@
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,6 +443,70 @@
/* 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 = {
@@ -484,7 +607,8 @@
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;
};
@@ -495,7 +619,10 @@
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 = Default;
@@ -508,7 +635,10 @@
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 = Default;
@@ -521,7 +651,11 @@
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 = Default;
@@ -535,7 +669,11 @@
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 = Default;
@@ -548,7 +686,11 @@
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 = Default;
@@ -562,7 +704,11 @@
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 = Default;
@@ -575,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 = (
@@ -612,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,16 @@
{
"object": {
"pins": [
{
"package": "SwiftyMarkdown",
"repositoryURL": "https://github.com/SimonFairbairn/SwiftyMarkdown.git",
"state": {
"branch": null,
"revision": "36f0c4d9c772a57f72941e7b51f0f04fb57fd79b",
"version": "1.0.2"
}
}
]
},
"version": 1
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<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="15704"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -21,6 +21,7 @@
<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"/>
@@ -55,7 +56,7 @@
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<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"/>
@@ -37,11 +37,18 @@ class ViewController: UIViewController {
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
@@ -14,22 +14,37 @@ Customise fonts and colors easily in a Swift-like way:
md.h2.color = UIColor.redColor()
md.h2.alignment = .center
It supports the standard Markdown syntax, like *italics*, _underline italics_, **bold**, `backticks for code` and headings.
It 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!
**List**
**Lists**
- And
- It Supports
- Unordered
- Lists
- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
- 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
+174 -109
View File
@@ -1,106 +1,152 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
activesupport (4.2.11.1)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
CFPropertyList (3.0.5)
rexml
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.1)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
cocoapods (1.8.4)
activesupport (>= 4.0.2, < 5)
aws-eventstream (1.2.0)
aws-partitions (1.588.0)
aws-sdk-core (3.131.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.8.4)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.6.6)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.11.1, < 2.0)
cocoapods-core (1.8.4)
activesupport (>= 4.0.2, < 6)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.1.0)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
concurrent-ruby (1.1.5)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.10)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
dotenv (2.7.6)
emoji_regex (3.2.3)
escape (0.0.4)
excon (0.72.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.140.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (~> 2.1.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
@@ -109,104 +155,123 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-api-client (0.36.4)
google-apis-androidpublisher_v3 (0.21.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.5.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.14.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
googleauth (1.1.3)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
highline (1.7.10)
http-cookie (1.0.3)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (0.9.5)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.3.0)
jwt (2.1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
minitest (5.14.0)
molinillo (0.6.6)
multi_json (1.14.1)
multi_xml (0.6.0)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.15.0)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.0)
naturally (2.2.1)
netrc (0.11.0)
os (1.0.1)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (1.4.0)
rubyzip (1.3.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.7)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.7.0)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
tzinfo (1.2.6)
thread_safe (~> 0.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.14.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -216,4 +281,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.1.4
2.3.8
+3 -2
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.1
import PackageDescription
let package = Package(
@@ -13,6 +13,7 @@ let package = Package(
.library(name: "SwiftyMarkdown", targets: ["SwiftyMarkdown"]),
],
targets: [
.target(name: "SwiftyMarkdown", path: "SwiftyMarkdown"),
.target(name: "SwiftyMarkdown"),
.testTarget(name: "SwiftyMarkdownTests", dependencies: ["SwiftyMarkdown"])
]
)
@@ -0,0 +1,285 @@
//: [Previous](@previous)
import Foundation
extension String {
func repeating( _ max : Int ) -> String {
var output = self
for _ in 1..<max {
output += self
}
return output
}
}
enum TagState {
case none
case open
case intermediate
case closed
}
struct TagString {
var state : TagState = .none
var preOpenString = ""
var openTagString = ""
var intermediateString = ""
var intermediateTagString = ""
var metadataString = ""
var closedTagString = ""
var postClosedString = ""
let rule : Rule
init( with rule : Rule ) {
self.rule = rule
}
mutating func append( _ string : String? ) {
guard let existentString = string else {
return
}
switch self.state {
case .none:
self.preOpenString += existentString
case .open:
self.intermediateString += existentString
case .intermediate:
self.metadataString += existentString
case .closed:
self.postClosedString += existentString
}
}
mutating func append( contentsOf tokenGroup: [TokenGroup] ) {
print(tokenGroup)
for token in tokenGroup {
switch token.state {
case .none:
self.append(token.string)
case .open:
if self.state != .none {
self.preOpenString += token.string
} else {
self.openTagString += token.string
}
case .intermediate:
if self.state != .open {
self.intermediateString += token.string
} else {
self.intermediateTagString += token.string
}
case .closed:
if self.rule.intermediateTag != nil && self.state != .intermediate {
self.metadataString += token.string
} else {
self.closedTagString += token.string
}
}
self.state = token.state
}
}
mutating func tokens() -> [Token] {
print(self)
var tokens : [Token] = []
if !self.preOpenString.isEmpty {
tokens.append(Token(type: .string, inputString: self.preOpenString))
}
if !self.openTagString.isEmpty {
tokens.append(Token(type: .openTag, inputString: self.openTagString))
}
if !self.intermediateString.isEmpty {
var token = Token(type: .string, inputString: self.intermediateString)
token.metadataString = self.metadataString
tokens.append(token)
}
if !self.intermediateTagString.isEmpty {
tokens.append(Token(type: .intermediateTag, inputString: self.intermediateTagString))
}
if !self.metadataString.isEmpty {
tokens.append(Token(type: .metadata, inputString: self.metadataString))
}
if !self.closedTagString.isEmpty {
tokens.append(Token(type: .closeTag, inputString: self.closedTagString))
}
self.preOpenString = ""
self.openTagString = ""
self.intermediateString = ""
self.intermediateTagString = ""
self.metadataString = ""
self.closedTagString = ""
self.postClosedString = ""
self.state = .none
return tokens
}
}
struct TokenGroup {
enum TokenGroupType {
case string
case tag
case escape
}
let string : String
let isEscaped : Bool
let type : TokenGroupType
var state : TagState = .none
}
func getTokenGroups( for string : inout String, with rule : Rule, shouldEmpty : Bool = false ) -> [TokenGroup] {
if string.isEmpty {
return []
}
let maxCount = rule.openTag.count * rule.maxTags
var groups : [TokenGroup] = []
let maxTag = rule.openTag.repeating(rule.maxTags)
if maxTag.contains(string) {
if string.count == maxCount || shouldEmpty {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .open
groups.append(token)
string.removeAll()
}
} else if string == rule.intermediateTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .intermediate
groups.append(token)
string.removeAll()
} else if string == rule.closingTag {
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
token.state = .closed
groups.append(token)
string.removeAll()
}
if shouldEmpty && !string.isEmpty {
let token = TokenGroup(string: string, isEscaped: false, type: .tag)
groups.append(token)
string.removeAll()
}
return groups
}
func scan( _ string : String, with rule : Rule) -> [Token] {
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
var tokens : [Token] = []
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
if let existentEscape = rule.escapeCharacter {
set.insert(charactersIn: String(existentEscape))
}
var openTag = rule.openTag.repeating(rule.maxTags)
var tagString = TagString(with: rule)
var openTagFound : TagState = .none
var regularCharacters = ""
var tagGroupCount = 0
while !scanner.isAtEnd {
tagGroupCount += 1
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let start = scanner.scanUpToCharacters(from: set) {
tagString.append(start)
}
} else {
var string : NSString?
scanner.scanUpToCharacters(from: set, into: &string)
if let existentString = string as String? {
tagString.append(existentString)
}
}
// The end of the string
let maybeFoundChars = scanner.scanCharacters(from: set )
guard let foundTag = maybeFoundChars else {
continue
}
if foundTag == rule.openTag && foundTag.count < rule.minTags {
tagString.append(foundTag)
continue
}
//:--
print(foundTag)
var tokenGroups : [TokenGroup] = []
var escapeCharacter : Character? = nil
var cumulatedString = ""
for char in foundTag {
if let existentEscapeCharacter = escapeCharacter {
// If any of the tags feature the current character
let escape = String(existentEscapeCharacter)
let nextTagCharacter = String(char)
if rule.openTag.contains(nextTagCharacter) || rule.intermediateTag?.contains(nextTagCharacter) ?? false || rule.closingTag?.contains(nextTagCharacter) ?? false {
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: true, type: .tag))
escapeCharacter = nil
} else if nextTagCharacter == escape {
// Doesn't apply to this rule
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: false, type: .escape))
}
continue
}
if let existentEscape = rule.escapeCharacter {
if char == existentEscape {
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
escapeCharacter = char
continue
}
}
cumulatedString.append(char)
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule))
}
if let remainingEscape = escapeCharacter {
tokenGroups.append(TokenGroup(string: String(remainingEscape), isEscaped: false, type: .escape))
}
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
tagString.append(contentsOf: tokenGroups)
if tagString.state == .closed {
tokens.append(contentsOf: tagString.tokens())
}
}
tokens.append(contentsOf: tagString.tokens())
return tokens
}
//: [Next](@next)
var string = "[]([[\\[Some Link]\\]](\\(\\(\\url) [Regular link](url)"
//string = "Text before [Regular link](url) Text after"
var output = "[]([[Some Link]] Regular link"
var tokens = scan(string, with: LinkRule())
print( tokens.filter( { $0.type == .string }).map({ $0.outputString }).joined())
//print( tokens )
//string = "**\\*\\Bold\\*\\***"
//output = "*\\Bold**"
//tokens = scan(string, with: AsteriskRule())
//print( tokens )
@@ -0,0 +1,32 @@
import Foundation
public protocol Rule {
var escapeCharacter : Character? { get }
var openTag : String { get }
var intermediateTag : String? { get }
var closingTag : String? { get }
var maxTags : Int { get }
var minTags : Int { get }
}
public struct LinkRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "["
public let intermediateTag : String? = "]("
public let closingTag : String? = ")"
public let maxTags : Int = 1
public let minTags : Int = 1
public init() { }
}
public struct AsteriskRule : Rule {
public let escapeCharacter : Character? = "\\"
public let openTag : String = "*"
public let intermediateTag : String? = nil
public let closingTag : String? = nil
public let maxTags : Int = 3
public let minTags : Int = 1
public init() { }
}
@@ -74,6 +74,7 @@ public struct Token {
public let inputString : String
public var metadataString : String? = nil
public var characterStyles : [CharacterStyling] = []
public var group : Int = 0
public var count : Int = 0
public var shouldSkip : Bool = false
public var outputString : String {
@@ -5,5 +5,7 @@
<page name='Line Processing'/>
<page name='Tokenising'/>
<page name='Attributed String'/>
<page name='SKLabelNode'/>
<page name='Groups'/>
</pages>
</playground>
+85 -18
View File
@@ -6,6 +6,7 @@ SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s us
- [Installation](#installation)
- [How to Use](#how-to-use-swiftymarkdown)
- [Screenshot](#screenshot)
- [Front Matter](#front-matter)
- [Appendix](#appendix)
## Fully Rebuilt For 2020!
@@ -64,37 +65,53 @@ label.attributedText = md.attributedString()
*italics* or _italics_
**bold** or __bold__
~~Linethrough~~Strikethroughs.
`code`
# Header 1
or
Header 1
====
Header 1
====
## Header 2
or
Header 2
---
Header 2
---
### Header 3
#### Header 4
##### Header 5 #####
###### Header 6 ######
`code`
Indented code blocks (spaces or tabs)
[Links](http://voyagetravelapps.com/)
![Images](<Name of asset in bundle>)
[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:
@@ -102,7 +119,7 @@ Compound rules also work, for example:
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).
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
## Customisation
@@ -119,6 +136,8 @@ md.h1.alignmnent = .center
md.italic.color = UIColor.blueColor()
md.underlineLinks = true
md.bullet = "🍏"
```
On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.
@@ -127,7 +146,11 @@ On iOS, Specified font sizes will be adjusted relative to the the user's dynamic
![Screenshot](https://cl.ly/779e6964257a/swiftymarkdown-2020.png)
There's an example project included in the repository. Open the `.xcworkspace` file to get started.
There's an example project included in the repository. Open the `Example/SwiftyMarkdown.xcodeproj` file to get started.
## Front Matter
SwiftyMarkdown recognises YAML front matter and will populate the `frontMatterAttributes` property with the key-value pairs that it fines.
## Appendix
@@ -202,12 +225,22 @@ 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.
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look. For example, perhaps you want blockquotes to default to having the italic style:
If you like a bit of chaos:
```swift
md.blockquotes.fontStyle = .italic
```
Or, if you like a bit of chaos:
```swift
md.bold.fontStyle = .italic
@@ -265,13 +298,46 @@ enum CharacterStyle : CharacterStyling {
}
static public var characterRules = [
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingTags: true, balancedTags: true),
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
These Character Rules are defined by SwiftyMarkdown:
public struct CharacterRule : CustomStringConvertible {
public let primaryTag : CharacterRuleTag
public let tags : [CharacterRuleTag]
public let escapeCharacters : [Character]
public let styles : [Int : CharacterStyling]
public let minTags : Int
public let maxTags : Int
public var metadataLookup : Bool = false
public var definesBoundary = false
public var shouldCancelRemainingRules = false
public var balancedTags = false
}
1. `primaryTag`: Each rule must have at least one tag and it can be one of `repeating`, `open`, `close`, `metadataOpen`, or `metadataClose`. `repeating` tags are tags that have identical open and close characters (and often have more than 1 style depending on how many are in a group). For example, the `*` tag used in Markdown.
2. `tags`: An array of other tags that the rule can look for. This is where you would put the `close` tag for a custom rule, for example.
3. `escapeCharacters`: The characters that appear prior to any of the tag characters that tell the scanner to ignore the tag.
4. `styles`: The styles that should be applied to every character between the opening and closing tags.
5. `minTags`: The minimum number of repeating characters to be considered a successful match. For example, setting the `primaryTag` to `*` and the `minTag` to 2 would mean that `**foo**` would be a successful match wheras `*bar*` would not.
6. `maxTags`: The maximum number of repeating characters to be considered a successful match.
7. `metadataLookup`: Used for Markdown reference links. Tells the scanner to try to look up the metadata from this dictionary, rather than from the inline result.
8. `definesBoundary`: In order for open and close tags to be successful, the `boundaryCount` for a given location in the string needs to be the same. Setting this property to `true` means that this rule will increase the `boundaryCount` for every character between its opening and closing tags. For example, the `[` rule defines a boundary. After it is applied, the string `*foo[bar*]` becomes `*foobar*` with a boundaryCount `00001111`. Applying the `*` rule results in the output `*foobar*` because the opening `*` tag and the closing `*` tag now have different `boundaryCount` values. It's basically a way to fix the `**[should not be bold**](url)` problem in Markdown.
9. `shouldCancelRemainingTags`: A successful match will mark every character between the opening and closing tags as complete, thereby preventing any further rules from being applied to those characters.
10. `balancedTags`: This flag requires that the opening and closing tags be of exactly equal length. E.g. If this is set to true, `**foo*` would result in `**foo*`. If it was false, the output would be `*foo`.
#### Rule Subsets
If you want to only support a small subset of Markdown, it's now easy to do.
@@ -281,8 +347,8 @@ This example would only process strings with `*` and `_` characters, ignoring li
SwiftyMarkdown.lineRules = []
SwiftyMarkdown.characterRules = [
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
]
```
@@ -303,7 +369,7 @@ enum Characters : CharacterStyling {
}
let characterRules = [
CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
CharacterRule(primaryTag: CharacterRuleTag(tag: "%", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.elf])
]
let processor = SwiftyTokeniser( with : characterRules )
@@ -333,4 +399,5 @@ let label = SKLabelNode()
label.preferredMaxLayoutWidth = 500
label.numberOfLines = 0
label.attributedText = smd.attributedString()
```
```
+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
}
}
@@ -7,6 +7,12 @@
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyLineProcessor"
static let swiftyLineProcessorPerformance = OSLog(subsystem: subsystem, category: "Swifty Line Processor Performance")
}
public protocol LineStyling {
var shouldTokeniseLine : Bool { get }
@@ -41,6 +47,12 @@ public enum ChangeApplication {
case untilClose
}
public struct FrontMatterRule {
let openTag : String
let closeTag : String
let keyValueSeparator : Character
}
public struct LineRule {
let token : String
let removeFrom : Remove
@@ -59,14 +71,21 @@ public struct LineRule {
public class SwiftyLineProcessor {
public var processEmptyStrings : LineStyling?
public internal(set) var frontMatterAttributes : [String : String] = [:]
var closeToken : String? = nil
let defaultType : LineStyling
public var processEmptyStrings : LineStyling?
let lineRules : [LineRule]
public init( rules : [LineRule], defaultRule: 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 {
@@ -106,6 +125,10 @@ public class SwiftyLineProcessor {
return nil
}
if !text.contains(element.token) {
continue
}
switch element.removeFrom {
case .leading:
output = findLeadingLineElement(element, in: output)
@@ -146,15 +169,63 @@ public class SwiftyLineProcessor {
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] = []
for heading in string.components(separatedBy: CharacterSet.newlines) {
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
}
@@ -167,6 +238,8 @@ public class SwiftyLineProcessor {
continue
}
foundAttributes.append(input)
self.perfomanceLog.tag(with: "(line completed: \(heading)")
}
return foundAttributes
}
@@ -99,6 +99,9 @@ extension SwiftyMarkdown {
fontName = italic.fontName ?? fontName
fontSize = italic.fontSize
globalItalic = true
case .strikethrough:
fontName = strikethrough.fontName ?? fontName
fontSize = strikethrough.fontSize
default:
break
}
@@ -127,10 +130,10 @@ extension SwiftyMarkdown {
}
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
font = UIFont(descriptor: italicDescriptor, size: 0)
font = UIFont(descriptor: italicDescriptor, size: fontSize ?? 0)
}
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
font = UIFont(descriptor: boldDescriptor, size: 0)
font = UIFont(descriptor: boldDescriptor, size: fontSize ?? 0)
}
return font
@@ -160,8 +163,10 @@ extension SwiftyMarkdown {
return code.color
case .blockquote:
return blockquotes.color
case .unorderedList:
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
return body.color
case .referencedLink:
return link.color
}
}
@@ -133,10 +133,12 @@ extension SwiftyMarkdown {
return code.color
case .blockquote:
return blockquotes.color
case .unorderedList:
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
return body.color
case .yaml:
return body.color
case .referencedLink:
return body.color
}
}
@@ -5,22 +5,30 @@
// Created by Simon Fairbairn on 05/03/2016.
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
//
import os.log
#if os(macOS)
import AppKit
#else
import UIKit
#endif
enum CharacterStyle : CharacterStyling {
extension OSLog {
private static var subsystem = "SwiftyMarkdown"
static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
}
public enum CharacterStyle : CharacterStyling {
case none
case bold
case italic
case code
case link
case image
case referencedLink
case referencedImage
case strikethrough
func isEqualTo(_ other: CharacterStyling) -> Bool {
public func isEqualTo(_ other: CharacterStyling) -> Bool {
guard let other = other as? CharacterStyle else {
return false
}
@@ -51,6 +59,13 @@ enum MarkdownLineStyle : LineStyling {
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:
@@ -89,6 +104,8 @@ enum MarkdownLineStyle : LineStyling {
@objc public protocol LineProperties {
var alignment : NSTextAlignment { get set }
var lineSpacing: CGFloat { get set }
var paragraphSpacing: CGFloat { get set }
}
@@ -118,20 +135,41 @@ If that is not set, then the system default will be used.
public var fontSize : CGFloat = 0.0
public var fontStyle : FontStyle = .normal
public var alignment: NSTextAlignment = .left
public var lineSpacing : CGFloat = 0.0
public var paragraphSpacing : CGFloat = 0.0
}
@objc open class LinkStyles : BasicStyles {
public var underlineStyle: NSUnderlineStyle = .single
#if os(macOS)
public lazy var underlineColor = self.color
#else
public lazy var underlineColor = self.color
#endif
}
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
@objc open class SwiftyMarkdown: NSObject {
static public var frontMatterRules = [
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
]
static public var lineRules = [
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
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.unorderedList, removeFrom: .leading),
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
@@ -141,14 +179,33 @@ If that is not set, then the system default will be used.
]
static public var characterRules = [
CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
CharacterRuleTag(tag: "]", type: .close),
CharacterRuleTag(tag: "[", type: .metadataOpen),
CharacterRuleTag(tag: "]", type: .metadataClose)
], styles: [1 : CharacterStyle.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)
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
@@ -176,7 +233,7 @@ If that is not set, then the system default will be used.
open var blockquotes = LineStyles()
/// The styles to apply to any links found in the Markdown
open var link = BasicStyles()
open var link = LinkStyles()
/// The styles to apply to any bold text found in the Markdown
open var bold = BasicStyles()
@@ -187,19 +244,32 @@ If that is not set, then the system default will be used.
/// 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
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
var orderedListCount = 0
var orderedListIndentFirstOrderCount = 0
var orderedListIndentSecondOrderCount = 0
var previouslyFoundTokens : [Token] = []
var applyAttachments = true
let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance)
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
@@ -209,13 +279,7 @@ If that is not set, then the system default will be used.
public init(string : String ) {
self.string = string
super.init()
#if os(macOS)
self.setFontColorForAllStyles(with: .labelColor)
#elseif !os(watchOS)
if #available(iOS 13.0, tvOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
#endif
self.setup()
}
/**
@@ -235,6 +299,10 @@ If that is not set, then the system default will be used.
return nil
}
super.init()
self.setup()
}
func setup() {
#if os(macOS)
self.setFontColorForAllStyles(with: .labelColor)
#elseif !os(watchOS)
@@ -262,6 +330,7 @@ If that is not set, then the system default will be used.
code.fontSize = size
link.fontSize = size
link.fontSize = size
strikethrough.fontSize = size
}
#if os(macOS)
@@ -278,6 +347,7 @@ If that is not set, then the system default will be used.
code.color = color
link.color = color
blockquotes.color = color
strikethrough.color = color
}
#else
open func setFontColorForAllStyles(with color: UIColor) {
@@ -293,6 +363,7 @@ If that is not set, then the system default will be used.
code.color = color
link.color = color
blockquotes.color = color
strikethrough.color = color
}
#endif
@@ -309,31 +380,62 @@ If that is not set, then the system default will be used.
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)
for (idx, line) in foundAttributes.enumerated() {
let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") })
let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) })
var keyValuePairs : [String : String] = [:]
for line in references {
let strings = line.line.components(separatedBy: "]:")
guard strings.count >= 2 else {
continue
}
var key : String = strings[0]
if !key.isEmpty {
let newstart = key.index(key.startIndex, offsetBy: 1)
let range : Range<String.Index> = newstart..<key.endIndex
key = String(key[range]).trimmingCharacters(in: .whitespacesAndNewlines)
}
keyValuePairs[key] = strings[1].trimmingCharacters(in: .whitespacesAndNewlines)
}
self.perfomanceLog.tag(with: "(line processing complete)")
self.tokeniser.metadataLookup = keyValuePairs
for (idx, line) in referencesRemoved.enumerated() {
if idx > 0 {
attributedString.append(NSAttributedString(string: "\n"))
}
let finalTokens = self.tokeniser.process(line.line)
self.previouslyFoundTokens.append(contentsOf: finalTokens)
self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)")
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
}
self.perfomanceLog.end()
return attributedString
}
@@ -347,9 +449,38 @@ extension SwiftyMarkdown {
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 line.lineStyle as! MarkdownLineStyle {
switch markdownLineStyle {
case .h1:
lineProperties = self.h1
case .h2:
@@ -362,7 +493,6 @@ extension SwiftyMarkdown {
lineProperties = self.h5
case .h6:
lineProperties = self.h6
case .codeblock:
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
@@ -374,34 +504,59 @@ extension SwiftyMarkdown {
paragraphStyle.firstLineHeadIndent = 20.0
paragraphStyle.headIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .unorderedList:
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()
let nonOptions = [NSTextTab.OptionKey: Any]()
paragraphStyle.tabStops = [
NSTextTab(textAlignment: .left, location: 20, options: nonOptions)]
paragraphStyle.defaultTabInterval = 20
paragraphStyle.headIndent = 20
attributes[.paragraphStyle] = paragraphStyle
finalTokens.insert(Token(type: .string, inputString: "\(self.bullet)\t"), at: 0)
default:
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
break
}
let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
if lineProperties.alignment != .left {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = lineProperties.alignment
attributes[.paragraphStyle] = paragraphStyle
}
paragraphStyle.lineSpacing = lineProperties.lineSpacing
paragraphStyle.paragraphSpacing = lineProperties.paragraphSpacing
attributes[.paragraphStyle] = paragraphStyle
for token in finalTokens {
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
}
@@ -414,26 +569,36 @@ extension SwiftyMarkdown {
attributes[.foregroundColor] = self.bold.color
}
if styles.contains(.link), let url = token.metadataString {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = url as AnyObject
if underlineLinks {
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
}
if let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = token.metadataStrings[linkIdx] as AnyObject
if underlineLinks {
attributes[.underlineStyle] = self.link.underlineStyle.rawValue as AnyObject
attributes[.underlineColor] = self.link.underlineColor
}
}
if styles.contains(.strikethrough) {
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
attributes[.foregroundColor] = self.strikethrough.color
}
#if !os(watchOS)
if styles.contains(.image), let imageName = token.metadataString {
if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count {
if !self.applyAttachments {
continue
}
#if !os(macOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx])
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#elseif !os(watchOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = NSImage(named: imageName)
image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx])
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#endif
+564
View File
@@ -0,0 +1,564 @@
//
// SwiftyScanner.swift
//
//
// Created by Simon Fairbairn on 04/04/2020.
//
//
// SwiftyScanner.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 04/02/2020.
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyScanner"
static let swiftyScanner = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner")
static let swiftyScannerPerformance = OSLog(subsystem: subsystem, category: "Swifty Scanner Scanner Peformance")
}
enum RepeatingTagType {
case open
case either
case close
case neither
}
struct TagGroup {
let groupID = UUID().uuidString
var tagRanges : [ClosedRange<Int>]
var tagType : RepeatingTagType = .open
var count = 1
}
class SwiftyScanner {
var elements : [Element]
let rule : CharacterRule
let metadata : [String : String]
var pointer : Int = 0
var spaceAndNewLine = CharacterSet.whitespacesAndNewlines
var tagGroups : [TagGroup] = []
var isMetadataOpen = false
var enableLog = (ProcessInfo.processInfo.environment["SwiftyScannerScanner"] != nil)
let currentPerfomanceLog = PerformanceLog(with: "SwiftyScannerScannerPerformanceLogging", identifier: "Scanner", log: OSLog.swiftyScannerPerformance)
let log = PerformanceLog(with: "SwiftyScannerScanner", identifier: "Scanner", log: OSLog.swiftyScanner)
enum Position {
case forward(Int)
case backward(Int)
}
init( withElements elements : [Element], rule : CharacterRule, metadata : [String : String]) {
self.elements = elements
self.rule = rule
self.currentPerfomanceLog.start()
self.metadata = metadata
}
func elementsBetweenCurrentPosition( and newPosition : Position ) -> [Element]? {
let newIdx : Int
var isForward = true
switch newPosition {
case .backward(let positions):
isForward = false
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
let range : ClosedRange<Int> = ( isForward ) ? self.pointer...newIdx : newIdx...self.pointer
return Array(self.elements[range])
}
func element( for position : Position ) -> Element? {
let newIdx : Int
switch position {
case .backward(let positions):
newIdx = pointer - positions
if newIdx < 0 {
return nil
}
case .forward(let positions):
newIdx = pointer + positions
if newIdx >= self.elements.count {
return nil
}
}
return self.elements[newIdx]
}
func positionIsEqualTo( character : Character, direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return validElement.character == character
}
func positionContains( characters : [Character], direction : Position ) -> Bool {
guard let validElement = self.element(for: direction) else {
return false
}
return characters.contains(validElement.character)
}
func isEscaped() -> Bool {
let isEscaped = self.positionContains(characters: self.rule.escapeCharacters, direction: .backward(1))
if isEscaped {
self.elements[self.pointer - 1].type = .escape
}
return isEscaped
}
func range( for tag : String? ) -> ClosedRange<Int>? {
guard let tag = tag else {
return nil
}
guard let openChar = tag.first else {
return nil
}
if self.pointer == self.elements.count {
return nil
}
if self.elements[self.pointer].character != openChar {
return nil
}
if isEscaped() {
return nil
}
let range : ClosedRange<Int>
if tag.count > 1 {
guard let elements = self.elementsBetweenCurrentPosition(and: .forward(tag.count - 1) ) else {
return nil
}
// If it's already a tag, then it should be ignored
if elements.filter({ $0.type != .string }).count > 0 {
return nil
}
if elements.map( { String($0.character) }).joined() != tag {
return nil
}
let endIdx = (self.pointer + tag.count - 1)
for i in self.pointer...endIdx {
self.elements[i].type = .tag
}
range = self.pointer...endIdx
self.pointer += tag.count
} else {
// If it's already a tag, then it should be ignored
if self.elements[self.pointer].type != .string {
return nil
}
self.elements[self.pointer].type = .tag
range = self.pointer...self.pointer
self.pointer += 1
}
return range
}
func resetTagGroup( withID id : String ) {
if let idx = self.tagGroups.firstIndex(where: { $0.groupID == id }) {
for range in self.tagGroups[idx].tagRanges {
self.resetTag(in: range)
}
self.tagGroups.remove(at: idx)
}
self.isMetadataOpen = false
}
func resetTag( in range : ClosedRange<Int>) {
for idx in range {
self.elements[idx].type = .string
}
}
func resetLastTag( for range : inout [ClosedRange<Int>]) {
guard let last = range.last else {
return
}
for idx in last {
self.elements[idx].type = .string
}
}
func closeTag( _ tag : String, withGroupID id : String ) {
guard let tagIdx = self.tagGroups.firstIndex(where: { $0.groupID == id }) else {
return
}
var metadataString = ""
if self.isMetadataOpen {
let metadataCloseRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let metadataOpenRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if metadataOpenRange.upperBound + 1 == (metadataCloseRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
for idx in (metadataOpenRange.upperBound)...(metadataCloseRange.lowerBound) {
self.elements[idx].type = .metadata
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
}
let key = self.elements[metadataOpenRange.upperBound + 1..<metadataCloseRange.lowerBound].map( { String( $0.character )}).joined()
if self.rule.metadataLookup {
metadataString = self.metadata[key] ?? ""
} else {
metadataString = key
}
}
}
let closeRange = self.tagGroups[tagIdx].tagRanges.removeLast()
let openRange = self.tagGroups[tagIdx].tagRanges.removeLast()
if self.rule.balancedTags && closeRange.count != openRange.count {
self.tagGroups[tagIdx].tagRanges.append(openRange)
self.tagGroups[tagIdx].tagRanges.append(closeRange)
return
}
var shouldRemove = true
var styles : [CharacterStyling] = []
if openRange.upperBound + 1 == (closeRange.lowerBound) {
if self.enableLog {
os_log("Nothing between the tags", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
} else {
var remainingTags = min(openRange.upperBound - openRange.lowerBound, closeRange.upperBound - closeRange.lowerBound) + 1
while remainingTags > 0 {
if remainingTags >= self.rule.maxTags {
remainingTags -= self.rule.maxTags
if let style = self.rule.styles[ self.rule.maxTags ] {
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
if let style = self.rule.styles[remainingTags] {
remainingTags -= remainingTags
if !styles.contains(where: { $0.isEqualTo(style)}) {
styles.append(style)
}
}
}
for idx in (openRange.upperBound)...(closeRange.lowerBound) {
self.elements[idx].styles.append(contentsOf: styles)
self.elements[idx].metadata.append(metadataString)
if self.rule.definesBoundary {
self.elements[idx].boundaryCount += 1
}
if self.rule.shouldCancelRemainingRules {
self.elements[idx].boundaryCount = 1000
}
}
if self.rule.isRepeatingTag {
let difference = ( openRange.upperBound - openRange.lowerBound ) - (closeRange.upperBound - closeRange.lowerBound)
switch difference {
case 1...:
shouldRemove = false
self.tagGroups[tagIdx].count = difference
self.tagGroups[tagIdx].tagRanges.append( openRange.upperBound - (abs(difference) - 1)...openRange.upperBound )
case ...(-1):
for idx in closeRange.upperBound - (abs(difference) - 1)...closeRange.upperBound {
self.elements[idx].type = .string
}
default:
break
}
}
}
if shouldRemove {
self.tagGroups.removeAll(where: { $0.groupID == id })
}
self.isMetadataOpen = false
}
func emptyRanges( _ ranges : inout [ClosedRange<Int>] ) {
while !ranges.isEmpty {
self.resetLastTag(for: &ranges)
ranges.removeLast()
}
}
func scanNonRepeatingTags() {
var groupID = ""
let closeTag = self.rule.tag(for: .close)?.tag
let metadataOpen = self.rule.tag(for: .metadataOpen)?.tag
let metadataClose = self.rule.tag(for: .metadataClose)?.tag
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if let range = self.range(for: metadataClose) {
if self.isMetadataOpen {
guard let groupIdx = self.tagGroups.firstIndex(where: { $0.groupID == groupID }) else {
self.pointer += 1
continue
}
guard !self.tagGroups.isEmpty else {
self.resetTagGroup(withID: groupID)
continue
}
guard self.isMetadataOpen else {
self.resetTagGroup(withID: groupID)
continue
}
if self.enableLog {
os_log("Closing metadata tag found. Closing tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.tagGroups[groupIdx].tagRanges.append(range)
self.closeTag(closeTag!, withGroupID: groupID)
self.isMetadataOpen = false
continue
} else {
self.resetTag(in: range)
self.pointer -= metadataClose!.count
}
}
if let openRange = self.range(for: self.rule.primaryTag.tag) {
if self.isMetadataOpen {
self.resetTagGroup(withID: groupID)
}
let tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
if self.enableLog {
os_log("New open tag found. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
if self.rule.isRepeatingTag {
}
self.tagGroups.append(tagGroup)
continue
}
if let range = self.range(for: closeTag) {
guard !self.tagGroups.isEmpty else {
if self.enableLog {
os_log("No open tags exist, resetting this close tag", log: OSLog.swiftyScanner, type:.info)
}
self.resetTag(in: range)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
groupID = self.tagGroups[self.tagGroups.count - 1].groupID
if self.enableLog {
os_log("New close tag found. Appending to group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
guard metadataOpen != nil else {
if self.enableLog {
os_log("No metadata tags exist, closing valid tag with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.closeTag(closeTag!, withGroupID: groupID)
continue
}
guard self.pointer != self.elements.count else {
continue
}
guard let range = self.range(for: metadataOpen) else {
if self.enableLog {
os_log("No metadata tag found, resetting group with ID %@", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTagGroup(withID: groupID)
continue
}
self.tagGroups[self.tagGroups.count - 1].tagRanges.append(range)
self.isMetadataOpen = true
continue
}
if let range = self.range(for: metadataOpen) {
if self.enableLog {
os_log("Multiple open metadata tags found!", log: OSLog.swiftyScanner, type:.info , groupID)
}
self.resetTag(in: range)
self.resetTagGroup(withID: groupID)
self.isMetadataOpen = false
continue
}
self.pointer += 1
}
}
func scanRepeatingTags() {
var groupID = ""
let escapeCharacters = "" //self.rule.escapeCharacters.map( { String( $0 ) }).joined()
let unionSet = spaceAndNewLine.union(CharacterSet(charactersIn: escapeCharacters))
while self.pointer < self.elements.count {
if self.enableLog {
os_log("CHARACTER: %@", log: OSLog.swiftyScanner, type:.info , String(self.elements[self.pointer].character))
}
if var openRange = self.range(for: self.rule.primaryTag.tag) {
if self.elements[openRange].first?.boundaryCount == 1000 {
self.resetTag(in: openRange)
continue
}
var count = 1
var tagType : RepeatingTagType = .open
if let prevElement = self.element(for: .backward(self.rule.primaryTag.tag.count + 1)) {
if !unionSet.containsUnicodeScalars(of: prevElement.character) {
tagType = .either
}
} else {
tagType = .open
}
while let nextRange = self.range(for: self.rule.primaryTag.tag) {
count += 1
openRange = openRange.lowerBound...nextRange.upperBound
}
if self.rule.minTags > 1 {
if (openRange.upperBound - openRange.lowerBound) + 1 < self.rule.minTags {
self.resetTag(in: openRange)
os_log("Tag does not meet minimum length", log: .swiftyScanner, type: .info)
continue
}
}
var validTagGroup = true
if let nextElement = self.element(for: .forward(0)) {
if unionSet.containsUnicodeScalars(of: nextElement.character) {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
} else {
if tagType == .either {
tagType = .close
} else {
validTagGroup = tagType != .open
}
}
if !validTagGroup {
if self.enableLog {
os_log("Tag has whitespace on both sides", log: .swiftyScanner, type: .info)
}
self.resetTag(in: openRange)
continue
}
if let idx = tagGroups.firstIndex(where: { $0.groupID == groupID }) {
if tagType == .either {
if tagGroups[idx].count == count {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
if let last = self.tagGroups.last {
groupID = last.groupID
}
continue
}
} else {
if let prevRange = tagGroups[idx].tagRanges.first {
if self.elements[prevRange].first?.boundaryCount == self.elements[openRange].first?.boundaryCount {
self.tagGroups[idx].tagRanges.append(openRange)
self.closeTag(self.rule.primaryTag.tag, withGroupID: groupID)
}
}
continue
}
}
var tagGroup = TagGroup(tagRanges: [openRange])
groupID = tagGroup.groupID
tagGroup.tagType = tagType
tagGroup.count = count
if self.enableLog {
os_log("New open tag found with characters %@. Starting new Group with ID %@", log: OSLog.swiftyScanner, type:.info, self.elements[openRange].map( { String($0.character) }).joined(), groupID)
}
self.tagGroups.append(tagGroup)
continue
}
self.pointer += 1
}
}
func scan() -> [Element] {
guard self.elements.filter({ $0.type == .string }).map({ String($0.character) }).joined().contains(self.rule.primaryTag.tag) else {
return self.elements
}
self.currentPerfomanceLog.tag(with: "Beginning \(self.rule.primaryTag.tag)")
if self.enableLog {
os_log("RULE: %@", log: OSLog.swiftyScanner, type:.info , self.rule.description)
}
if self.rule.isRepeatingTag {
self.scanRepeatingTags()
} else {
self.scanNonRepeatingTags()
}
for tagGroup in self.tagGroups {
self.resetTagGroup(withID: tagGroup.groupID)
}
if self.enableLog {
for element in self.elements {
print(element)
}
}
return self.elements
}
}
@@ -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: "\", \""))\"]"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SwiftyMarkdown"
s.version = "1.0.2"
s.version = "1.2.4"
s.summary = "Converts Markdown to NSAttributed String"
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
s.license = 'MIT'
@@ -14,7 +14,7 @@ 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"
@@ -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>1.0.2</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>
+376 -315
View File
@@ -6,151 +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 */
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */; };
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */; };
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */; };
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */; };
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */; };
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */; };
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */; };
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */; };
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */; };
F4A48ABB23A8521C00713437 /* test.md in Resources */ = {isa = PBXBuildFile; fileRef = F4A48ABA23A8518900713437 /* test.md */; };
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */; settings = {ATTRIBUTES = (Public, ); }; };
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; };
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */; };
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */; };
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E61C8AF01300D735C1 /* LICENSE */; };
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */; };
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 */
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = "<group>"; };
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = "<group>"; };
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = "<group>"; };
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdownAttributedStringTests copy.swift"; sourceTree = "<group>"; };
F4A48ABA23A8518900713437 /* test.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = test.md; sourceTree = "<group>"; };
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyMarkdown.h; sourceTree = "<group>"; };
F4CE98861C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = "<group>"; };
F4CE98921C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
F4CE98E61C8AF01300D735C1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
F4CE98E71C8AF01300D735C1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
F4FEF07423E13365007219EF /* metadataTest.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = metadataTest.md; sourceTree = "<group>"; };
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 */,
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */,
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */,
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */,
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */,
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.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 = (
F4A48ABA23A8518900713437 /* test.md */,
F4FEF07423E13365007219EF /* metadataTest.md */,
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */,
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */,
F4CE98921C8A921300D735C1 /* Info.plist */,
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */,
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */,
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */,
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 = (
);
@@ -158,343 +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 = 1120;
ORGANIZATIONNAME = "Voyage Travel Apps";
TargetAttributes = {
F4CE98801C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
LastSwiftMigration = 1120;
};
F4CE988A1C8A921300D735C1 = {
CreatedOnToolsVersion = 7.2.1;
DevelopmentTeam = 52T262DA8V;
LastSwiftMigration = 1120;
};
};
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 = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
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 = (
F4A48ABB23A8521C00713437 /* test.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F4CE987C1C8A921300D735C1 /* Sources */ = {
OBJ_39 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 0;
files = (
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */,
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */,
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */,
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */,
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */,
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */,
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 = (
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */,
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */,
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */,
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */,
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */,
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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
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 = 11.0;
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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 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 = 11.0;
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 = "";
"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 = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "-fembed-bitcode";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
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 = "";
"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 = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "-fembed-bitcode";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
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 = {
DEVELOPMENT_TEAM = 52T262DA8V;
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F4CE989A1C8A921300D735C1 /* Release */ = {
OBJ_56 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = 52T262DA8V;
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
};
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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>
@@ -11,7 +11,7 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.01346</real>
<real>0.1</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
@@ -21,7 +21,17 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0064581</real>
<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>
@@ -11,11 +11,11 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.1</real>
<real>0.212</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
<key>maxPercentRelativeStandardDeviation</key>
<real>5</real>
<real>10</real>
</dict>
</dict>
<key>testThatStringsAreProcessedQuickly()</key>
@@ -23,11 +23,9 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.01</real>
<real>0.016</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
<key>maxPercentRelativeStandardDeviation</key>
<real>5</real>
</dict>
</dict>
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
@@ -35,7 +33,7 @@
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0392</real>
<real>0.016</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
@@ -4,7 +4,7 @@
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>3EC61D89-CC18-40D5-9AC6-F4504ECE42B5</key>
<key>88991ED5-B954-422F-B610-BDC9A4AEC008</key>
<dict>
<key>localComputer</key>
<dict>
@@ -26,16 +26,9 @@
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>x86_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone10,6</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
<string>x86_64h</string>
</dict>
<key>8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4</key>
<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
<dict>
<key>localComputer</key>
<dict>
@@ -58,13 +51,6 @@
</dict>
<key>targetArchitecture</key>
<string>x86_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone11,8</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
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">
@@ -27,21 +27,12 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE988A1C8A921300D735C1"
BlueprintIdentifier = "SwiftyMarkdown::SwiftyMarkdownTests"
BuildableName = "SwiftyMarkdownTests.xctest"
BlueprintName = "SwiftyMarkdownTests"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
@@ -59,15 +50,43 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
BuildableName = "SwiftyMarkdown.framework"
BlueprintName = "SwiftyMarkdown"
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
</BuildableReference>
</MacroExpansion>
<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"
@@ -75,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.
//
//! 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>
-784
View File
@@ -1,784 +0,0 @@
//
// SwiftyTokeniser.swift
// SwiftyMarkdown
//
// Created by Simon Fairbairn on 16/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import Foundation
import os.log
extension OSLog {
private static var subsystem = "SwiftyTokeniser"
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
static let styling = OSLog(subsystem: subsystem, category: "Styling")
}
// Tag definition
public protocol CharacterStyling {
func isEqualTo( _ other : CharacterStyling ) -> Bool
}
public enum SpaceAllowed {
case no
case bothSides
case oneSide
case leadingSide
case trailingSide
}
public enum Cancel {
case none
case allRemaining
case currentSet
}
public struct CharacterRule : CustomStringConvertible {
public let openTag : String
public let intermediateTag : String?
public let closingTag : String?
public let escapeCharacter : Character?
public let styles : [Int : [CharacterStyling]]
public var maxTags : Int = 1
public var spacesAllowed : SpaceAllowed = .oneSide
public var cancels : Cancel = .none
public var description: String {
return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) "
}
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
self.openTag = openTag
self.intermediateTag = intermediateTag
self.closingTag = closingTag
self.escapeCharacter = escapeCharacter
self.styles = styles
self.maxTags = maxTags
self.cancels = cancels
}
}
// Token definition
public enum TokenType {
case repeatingTag
case openTag
case intermediateTag
case closeTag
case string
case escape
case replacement
}
public struct Token {
public let id = UUID().uuidString
public let type : TokenType
public let inputString : String
public fileprivate(set) var metadataString : String? = nil
public fileprivate(set) var characterStyles : [CharacterStyling] = []
public fileprivate(set) var count : Int = 0
public fileprivate(set) var shouldSkip : Bool = false
public fileprivate(set) var tokenIndex : Int = -1
public fileprivate(set) var isProcessed : Bool = false
public fileprivate(set) var isMetadata : Bool = false
public var outputString : String {
get {
switch self.type {
case .repeatingTag:
if count <= 0 {
return ""
} else {
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
return String(inputString[range])
}
case .openTag, .closeTag, .intermediateTag:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .escape, .string:
return (self.isProcessed || self.isMetadata) ? "" : inputString
case .replacement:
return self.inputString
}
}
}
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
self.type = type
self.inputString = inputString
self.characterStyles = characterStyles
}
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
newToken.metadataString = self.metadataString
newToken.isMetadata = self.isMetadata
newToken.isProcessed = self.isProcessed
return newToken
}
}
extension Sequence where Iterator.Element == Token {
var oslogDisplay: String {
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
}
}
public class SwiftyTokeniser {
let rules : [CharacterRule]
var replacements : [String : [Token]] = [:]
public init( with rules : [CharacterRule] ) {
self.rules = rules
}
/// This goes through every CharacterRule in order and applies it to the input string, tokenising the string
/// if there are any matches.
///
/// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to
/// those outside them.
///
/// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately.
///
/// This is to prevent situations like **\[link**\](url) from returing a bold string.
///
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
public func process( _ inputString : String ) -> [Token] {
guard rules.count > 0 else {
return [Token(type: .string, inputString: inputString)]
}
var currentTokens : [Token] = []
var mutableRules = self.rules
while !mutableRules.isEmpty {
let nextRule = mutableRules.removeFirst()
os_log("------------------------------", log: .tokenising, type: .info)
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
if currentTokens.isEmpty {
// This means it's the first time through
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
continue
}
var outerStringTokens : [Token] = []
var innerStringTokens : [Token] = []
var isOuter = true
for idx in 0..<currentTokens.count {
let nextToken = currentTokens[idx]
if nextToken.type == .openTag && nextToken.isProcessed {
isOuter = false
}
if nextToken.type == .closeTag {
let ref = UUID().uuidString
outerStringTokens.append(Token(type: .replacement, inputString: ref))
innerStringTokens.append(nextToken)
self.replacements[ref] = self.handleReplacementTokens(innerStringTokens, with: nextRule)
innerStringTokens.removeAll()
isOuter = true
continue
}
(isOuter) ? outerStringTokens.append(nextToken) : innerStringTokens.append(nextToken)
}
currentTokens = self.handleReplacementTokens(outerStringTokens, with: nextRule)
var finalTokens : [Token] = []
for token in currentTokens {
guard token.type == .replacement else {
finalTokens.append(token)
continue
}
if let hasReplacement = self.replacements[token.inputString] {
os_log("Found replacement for %@", log: .tokenising, type: .info, token.inputString)
for var repToken in hasReplacement {
guard repToken.type == .string else {
finalTokens.append(repToken)
continue
}
for style in token.characterStyles {
if !repToken.characterStyles.contains(where: { $0.isEqualTo(style)}) {
repToken.characterStyles.append(contentsOf: token.characterStyles)
}
}
finalTokens.append(repToken)
}
}
}
currentTokens = finalTokens
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
// The one string token might then be exploded into multiple more tokens
}
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
os_log("==================================", log: .tokenising, type: .info)
return currentTokens
}
/// In order to reinsert the original replacements into the new string token, the replacements
/// need to be searched for in the incoming string one by one.
///
/// Using the `newToken(fromSubstring:isReplacement:)` function ensures that any metadata and character styles
/// are passed over into the newly created tokens.
///
/// E.g. A string token that has an `outputString` of "This string AAAAA-BBBBB-CCCCC replacements", with
/// a characterStyle of `bold` for the entire string, needs to be separated into the following tokens:
///
/// - `string`: "This string "
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
/// - `string`: " replacements"
///
/// Each of these need to have a character style of `bold`.
///
/// - Parameters:
/// - replacements: An array of `replacement` tokens
/// - token: The new `string` token that may contain replacement IDs contained in the `replacements` array
func reinsertReplacements(_ replacements : [Token], from stringToken : Token ) -> [Token] {
guard !stringToken.outputString.isEmpty && !replacements.isEmpty else {
return [stringToken]
}
var outputTokens : [Token] = []
let scanner = Scanner(string: stringToken.outputString)
scanner.charactersToBeSkipped = nil
// Remove any replacements that don't appear in the incoming string
var repTokens = replacements.filter({ stringToken.outputString.contains($0.inputString) })
var testString = "\n"
while !scanner.isAtEnd {
var outputString : String = ""
if repTokens.count > 0 {
testString = repTokens.removeFirst().inputString
}
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let nextString = scanner.scanUpToString(testString) {
outputString = nextString
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
if let outputToken = scanner.scanString(testString) {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else if let outputToken = scanner.scanString(testString) {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else {
var oldString : NSString? = nil
var tokenString : NSString? = nil
scanner.scanUpTo(testString, into: &oldString)
if let nextString = oldString {
outputString = nextString as String
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
scanner.scanString(testString, into: &tokenString)
if let outputToken = tokenString as String? {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
} else {
scanner.scanString(testString, into: &tokenString)
if let outputToken = tokenString as String? {
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
}
}
}
}
return outputTokens
}
/// This function is necessary because a previously tokenised string might have
///
/// Consider a previously tokenised string, where AAAAA-BBBBB-CCCCC represents a replaced \[link\](url) instance.
///
/// The incoming tokens will look like this:
///
/// - `string`: "A \*\*Bold"
/// - `replacement` : "AAAAA-BBBBB-CCCCC"
/// - `string`: " with a trailing string**"
///
/// However, because the scanner can only tokenise individual strings, passing in the string values
/// of these tokens individually and applying the styles will not correctly detect the starting and
/// ending `repeatingTag` instances. (e.g. the scanner will see "A \*\*Bold", and then "AAAAA-BBBBB-CCCCC",
/// and finally " with a trailing string\*\*")
///
/// The strings need to be combined, so that they form a single string:
/// A \*\*Bold AAAAA-BBBBB-CCCCC with a trailing string\*\*.
/// This string is then parsed and tokenised so that it looks like this:
///
/// - `string`: "A "
/// - `repeatingTag`: "\*\*"
/// - `string`: "Bold AAAAA-BBBBB-CCCCC with a trailing string"
/// - `repeatingTag`: "\*\*"
///
/// Finally, the replacements from the original incoming token array are searched for and pulled out
/// of this new string, so the final result looks like this:
///
/// - `string`: "A "
/// - `repeatingTag`: "\*\*"
/// - `string`: "Bold "
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
/// - `string`: " with a trailing string"
/// - `repeatingTag`: "\*\*"
///
/// - Parameters:
/// - tokens: The tokens to be combined, scanned, re-tokenised, and merged
/// - rule: The character rule currently being applied
func scanReplacementTokens( _ tokens : [Token], with rule : CharacterRule ) -> [Token] {
guard tokens.count > 0 else {
return []
}
let combinedString = tokens.map({ $0.outputString }).joined()
let nextTokens = self.scan(combinedString, with: rule)
var replacedTokens = self.applyStyles(to: nextTokens, usingRule: rule)
/// It's necessary here to check to see if the first token (which will always represent the styles
/// to be applied from previous scans) has any existing metadata or character styles and apply them
/// to *all* the string and replacement tokens found by the new scan.
for idx in 0..<replacedTokens.count {
guard replacedTokens[idx].type == .string || replacedTokens[idx].type == .replacement else {
continue
}
if tokens.first!.metadataString != nil && replacedTokens[idx].metadataString == nil {
replacedTokens[idx].metadataString = tokens.first!.metadataString
}
replacedTokens[idx].characterStyles.append(contentsOf: tokens.first!.characterStyles)
}
// Swap the original replacement tokens back in
let replacements = tokens.filter({ $0.type == .replacement })
var outputTokens : [Token] = []
for token in replacedTokens {
guard token.type == .string else {
outputTokens.append(token)
continue
}
outputTokens.append(contentsOf: self.reinsertReplacements(replacements, from: token))
}
return outputTokens
}
/// This function ensures that only concurrent `string` and `replacement` tokens are processed together.
///
/// i.e. If there is an existing `repeatingTag` token between two strings, then those strings will be
/// processed individually. This prevents incorrect parsing of strings like "\*\*\_Should only be bold\*\*\_"
///
/// - Parameters:
/// - incomingTokens: A group of tokens whose string tokens and replacement tokens should be combined and re-tokenised
/// - rule: The current rule being processed
func handleReplacementTokens( _ incomingTokens : [Token], with rule : CharacterRule) -> [Token] {
// Only combine string and replacements that are next to each other.
var newTokenSet : [Token] = []
var currentTokenSet : [Token] = []
for i in 0..<incomingTokens.count {
guard incomingTokens[i].type == .string || incomingTokens[i].type == .replacement else {
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
newTokenSet.append(incomingTokens[i])
currentTokenSet.removeAll()
continue
}
guard !incomingTokens[i].isProcessed && !incomingTokens[i].isMetadata && !incomingTokens[i].shouldSkip else {
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
newTokenSet.append(incomingTokens[i])
currentTokenSet.removeAll()
continue
}
currentTokenSet.append(incomingTokens[i])
}
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
return newTokenSet
}
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
guard rule.closingTag != nil else {
return
}
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag && !$0.isProcessed }) else {
return
}
var metadataIndex = index
// If there's an intermediate tag, get the index of that
if rule.intermediateTag != nil {
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag && !$0.isProcessed }) else {
return
}
metadataIndex = nextTokenIdx
let styles : [CharacterStyling] = rule.styles[1] ?? []
for i in index..<nextTokenIdx {
for style in styles {
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
tokens[i].characterStyles.append(style)
}
}
}
}
var metadataString : String = ""
for i in metadataIndex..<closeTokenIdx {
if tokens[i].type == .string {
metadataString.append(tokens[i].outputString)
tokens[i].isMetadata = true
}
}
for i in index..<metadataIndex {
if tokens[i].type == .string {
tokens[i].metadataString = metadataString
}
}
tokens[closeTokenIdx].isProcessed = true
tokens[metadataIndex].isProcessed = true
tokens[index].isProcessed = true
}
/// This is here to manage how opening tags are matched with closing tags when they're all the same
/// character.
///
/// Of course, because Markdown is about as loose as a spec can be while still being considered any
/// kind of spec, the number of times this character repeats causes different effects. Then there
/// is the ill-defined way it should work if the number of opening and closing tags are different.
///
/// - Parameters:
/// - index: The index of the current token in the loop
/// - tokens: An inout variable of the loop tokens of interest
/// - rule: The character rule being applied
func handleClosingTagFromRepeatingTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule) {
let theToken = tokens[index]
os_log("Found repeating tag with tag count: %i, tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString, rule.openTag )
guard theToken.count > 0 else {
return
}
let startIdx = index
var endIdx : Int? = nil
let maxCount = (theToken.count > rule.maxTags) ? rule.maxTags : theToken.count
// Try to find exact match first
if let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id && !$0.isProcessed }) {
endIdx = nextTokenIdx
}
if endIdx == nil, let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count >= 1 && $0.id != theToken.id && !$0.isProcessed }) {
endIdx = nextTokenIdx
}
guard let existentEnd = endIdx else {
return
}
let styles : [CharacterStyling] = rule.styles[maxCount] ?? []
for i in startIdx..<existentEnd {
for style in styles {
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
tokens[i].characterStyles.append(style)
}
}
if rule.cancels == .allRemaining {
tokens[i].shouldSkip = true
}
}
let maxEnd = (tokens[existentEnd].count > rule.maxTags) ? rule.maxTags : tokens[existentEnd].count
tokens[index].count = theToken.count - maxEnd
tokens[existentEnd].count = tokens[existentEnd].count - maxEnd
if maxEnd < rule.maxTags {
self.handleClosingTagFromRepeatingTag(withIndex: index, in: &tokens, following: rule)
} else {
tokens[existentEnd].isProcessed = true
tokens[index].isProcessed = true
}
}
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
var mutableTokens : [Token] = tokens
os_log("Applying styles to tokens: %@", log: .tokenising, type: .info, tokens.oslogDisplay )
for idx in 0..<mutableTokens.count {
let token = mutableTokens[idx]
switch token.type {
case .escape:
os_log("Found escape: %@", log: .tokenising, type: .info, token.inputString )
case .repeatingTag:
self.handleClosingTagFromRepeatingTag(withIndex: idx, in: &mutableTokens, following: rule)
case .openTag:
let theToken = mutableTokens[idx]
os_log("Found repeating tag with tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.inputString, rule.openTag )
guard rule.closingTag != nil else {
// If there's an intermediate tag, get the index of that
// Get the index of the closing tag
continue
}
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
case .intermediateTag:
let theToken = mutableTokens[idx]
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
case .closeTag:
let theToken = mutableTokens[idx]
os_log("Found close tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
case .string:
let theToken = mutableTokens[idx]
if theToken.isMetadata {
os_log("Found Metadata: %@", log: .tokenising, type: .info, theToken.inputString )
} else {
os_log("Found String: %@", log: .tokenising, type: .info, theToken.inputString )
}
if let hasMetadata = theToken.metadataString {
os_log("...with metadata: %@", log: .tokenising, type: .info, hasMetadata )
}
case .replacement:
os_log("Found replacement with ID: %@", log: .tokenising, type: .info, mutableTokens[idx].inputString )
}
}
return mutableTokens
}
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
let scanner = Scanner(string: string)
scanner.charactersToBeSkipped = nil
var tokens : [Token] = []
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
if let existentEscape = rule.escapeCharacter {
set.insert(charactersIn: String(existentEscape))
}
var openTagFound = false
var openingString = ""
while !scanner.isAtEnd {
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
if let start = scanner.scanUpToCharacters(from: set) {
openingString.append(start)
}
} else {
var string : NSString?
scanner.scanUpToCharacters(from: set, into: &string)
if let existentString = string as String? {
openingString.append(existentString)
}
// Fallback on earlier versions
}
let lastChar : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
} else {
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
} else {
lastChar = nil
}
}
let maybeFoundChars : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
maybeFoundChars = scanner.scanCharacters(from: set )
} else {
var string : NSString?
scanner.scanCharacters(from: set, into: &string)
maybeFoundChars = string as String?
}
let nextChar : String?
if #available(iOS 13.0, OSX 10.15, watchOS 6.0,tvOS 13.0, *) {
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
} else {
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
} else {
nextChar = nil
}
}
guard let foundChars = maybeFoundChars else {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
continue
}
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
if let hasIntermediateTag = rule.intermediateTag {
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
}
if let existentClosingTag = rule.closingTag {
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
}
openingString.append(escaped)
continue
}
var cumulativeString = ""
var openString = ""
var intermediateString = ""
var closedString = ""
var maybeEscapeNext = false
func addToken( for type : TokenType ) {
var inputString : String
switch type {
case .openTag:
inputString = openString
case .intermediateTag:
inputString = intermediateString
case .closeTag:
inputString = closedString
default:
inputString = ""
}
guard !inputString.isEmpty else {
return
}
if !openingString.isEmpty {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
openingString = ""
}
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
var token = Token(type: actualType, inputString: inputString)
if rule.closingTag == nil {
token.count = inputString.count
}
tokens.append(token)
switch type {
case .openTag:
openString = ""
case .intermediateTag:
intermediateString = ""
case .closeTag:
closedString = ""
default:
break
}
}
// Here I am going through and adding the characters in the found set to a cumulative string.
// If there is an escape character, then the loop stops and any open tags are tokenised.
for char in foundChars {
cumulativeString.append(char)
if maybeEscapeNext {
var escaped = cumulativeString
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
}
openingString.append(escaped)
cumulativeString = ""
maybeEscapeNext = false
}
if let existentEscape = rule.escapeCharacter {
if cumulativeString == String(existentEscape) {
maybeEscapeNext = true
addToken(for: .openTag)
addToken(for: .intermediateTag)
addToken(for: .closeTag)
continue
}
}
if cumulativeString == rule.openTag {
openString.append(char)
cumulativeString = ""
openTagFound = true
} else if cumulativeString == rule.intermediateTag, openTagFound {
intermediateString.append(cumulativeString)
cumulativeString = ""
} else if cumulativeString == rule.closingTag, openTagFound {
closedString.append(char)
cumulativeString = ""
openTagFound = false
}
}
// If we're here, it means that an escape character was found but without a corresponding
// tag, which means it might belong to a different rule.
// It should be added to the next group of regular characters
addToken(for: .openTag)
addToken(for: .intermediateTag)
addToken(for: .closeTag)
openingString.append( cumulativeString )
}
if !openingString.isEmpty {
tokens.append(Token(type: .string, inputString: "\(openingString)"))
}
return tokens
}
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
switch rule.spacesAllowed {
case .leadingSide:
guard nextCharacter != nil else {
return true
}
if nextCharacter == " " {
return false
}
case .trailingSide:
guard previousCharacter != nil else {
return true
}
if previousCharacter == " " {
return false
}
case .no:
switch (previousCharacter, nextCharacter) {
case (nil, nil), ( " ", _ ), ( _, " " ):
return false
default:
return true
}
case .oneSide:
switch (previousCharacter, nextCharacter) {
case (nil, " " ), (" ", nil), (" ", " " ):
return false
default:
return true
}
default:
break
}
return true
}
}
@@ -1,557 +0,0 @@
//
// SwiftyMarkdownCharacterTests.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
@testable import SwiftyMarkdown
import UIKit
import XCTest
class SwiftyMarkdownCharacterTests: XCTestCase {
func testIsolatedCase() {
let challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
])
let results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatRegularTraitsAreParsedCorrectly() {
var challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [
Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with `code` (should not be indented)", output: "A string with code (should not be indented)", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " (should not be indented)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [
Token(type: .string, inputString: "A string with ", characterStyles: []),
Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " text", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "with a ", characterStyles: []),
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " bold ", characterStyles: []),
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.bold])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "`A code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [
Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " with multiple ", characterStyles: []),
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "instances", characterStyles: [CharacterStyle.code])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_ with a *mix* _of_ italic *styles*", output: "An italic string with a mix of italic styles", tokens : [
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " with a ", characterStyles: []),
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " italic ", characterStyles: []),
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.italic])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "with some code", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: ", **and some** _escaped_ `characters`, ", characterStyles: []),
Token(type: .string, inputString: "ending", characterStyles: [CharacterStyle.code]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "with", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "more", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " variety.", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatExtraCharactersAreHandles() {
var challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "*bold italic*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ****bold italic*** word", output: "A string with a *bold italic word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "*bold italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: " italic", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [
Token(type: .string, inputString: "A string with a ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " word", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
// The new version of SwiftyMarkdown is a lot more strict than the old version, although this may change in future
func offtestThatMarkdownMistakesAreHandledAppropriately() {
let mismatchedBoldCharactersAtStart = "**This should be bold*"
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
XCTAssertEqual(md.attributedString().string, "This should be bold")
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
XCTAssertEqual(md.attributedString().string, "A string that should be italic")
}
func testThatEscapedCharactersAreEscapedCorrectly() {
var challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [
Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [
Token(type: .string, inputString: "_A normal string_", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [
Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [
Token(type: .string, inputString: "`A normal string`", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [
Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [
Token(type: .string, inputString: "*", characterStyles: []),
Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]),
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
Token(type: .string, inputString: "A string with one *", characterStyles: []),
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func offtestAdvancedEscaping() {
var challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [
Token(type: .string, inputString: "**", characterStyles: []),
Token(type: .string, inputString: "A normal string", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "**", characterStyles: [])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
Token(type: .string, inputString: "** asterisks", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
let asteriskSpace = """
An asterisk followed by a space: *
Line break
"""
let backtickSpace = "A backtick followed by a space: `"
let underscoreSpace = "An underscore followed by a space: _"
let asteriskFullStop = "Two asterisks followed by a full stop: **."
let backtickFullStop = "Two backticks followed by a full stop: ``."
let underscoreFullStop = "Two underscores followed by a full stop: __."
let asteriskComma = "An asterisk followed by a full stop: *, *"
let backtickComma = "A backtick followed by a space: `, `"
let underscoreComma = "An underscore followed by a space: _, _"
let asteriskWithBold = "A **bold** word followed by an asterisk * "
let backtickWithCode = "A `code` word followed by a backtick ` "
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
var md = SwiftyMarkdown(string: asteriskSpace)
XCTAssertEqual(md.attributedString().string, asteriskSpace)
md = SwiftyMarkdown(string: backtickSpace)
XCTAssertEqual(md.attributedString().string, backtickSpace)
md = SwiftyMarkdown(string: underscoreSpace)
XCTAssertEqual(md.attributedString().string, underscoreSpace)
md = SwiftyMarkdown(string: asteriskFullStop)
XCTAssertEqual(md.attributedString().string, asteriskFullStop)
md = SwiftyMarkdown(string: backtickFullStop)
XCTAssertEqual(md.attributedString().string, backtickFullStop)
md = SwiftyMarkdown(string: underscoreFullStop)
XCTAssertEqual(md.attributedString().string, underscoreFullStop)
md = SwiftyMarkdown(string: asteriskComma)
XCTAssertEqual(md.attributedString().string, asteriskComma)
md = SwiftyMarkdown(string: backtickComma)
XCTAssertEqual(md.attributedString().string, backtickComma)
md = SwiftyMarkdown(string: underscoreComma)
XCTAssertEqual(md.attributedString().string, underscoreComma)
md = SwiftyMarkdown(string: asteriskWithBold)
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk *")
md = SwiftyMarkdown(string: backtickWithCode)
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick `")
md = SwiftyMarkdown(string: underscoreWithItalic)
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _")
}
func testForLinks() {
var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [
Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/")
} else {
XCTFail("Failed to find an open link tag")
}
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: ", ", characterStyles: []),
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 2)
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [
Token(type: .string, inputString: "Email us at ", characterStyles: []),
Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " Twitter", characterStyles: []),
Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 2)
XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com")
XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp")
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []),
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "[Link](", characterStyles: []),
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: " ", characterStyles: []),
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
}
func testLinksWithOtherStyles() {
var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A ", characterStyles: []),
Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
])
var results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
// XCTAssertEqual(results.attributedString.string, challenge.output)
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 1)
if links.count == 1 {
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
} else {
XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
}
challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
Token(type: .string, inputString: "A Bold ", characterStyles: []),
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link])
])
results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
XCTAssertEqual(results.attributedString.string, challenge.output)
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
}
func testForImages() {
let challenge = TokenTest(input: "An ![Image](imageName)", output: "An Image", tokens: [
Token(type: .string, inputString: "An Image", characterStyles: []),
Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image])
])
let results = self.attempt(challenge)
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
XCTAssertEqual(results.foundStyles, results.expectedStyles)
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
XCTAssertEqual(links.count, 1)
XCTAssertEqual(links[0].metadataString, "imageName")
}
}
@@ -1,24 +0,0 @@
//
// XCTest+SwiftyMarkdown.swift
// SwiftyMarkdownTests
//
// Created by Simon Fairbairn on 17/12/2019.
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
//
import XCTest
@testable import SwiftyMarkdown
extension XCTestCase {
func attempt( _ challenge : TokenTest ) -> (tokens : [Token], stringTokens: [Token], attributedString : NSAttributedString, foundStyles : [[CharacterStyle]], expectedStyles : [[CharacterStyle]] ) {
let md = SwiftyMarkdown(string: challenge.input)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
let tokens = tokeniser.process(challenge.input)
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
return (tokens, stringTokens, md.attributedString(), existentTokenStyles, expectedStyles)
}
}
-21
View File
@@ -1,21 +0,0 @@
# 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.
+7
View File
@@ -0,0 +1,7 @@
import XCTest
import AppLibrarianTests
var tests = [XCTestCaseEntry]()
tests += AppLibrarianTests.allTests()
XCTMain(tests)
@@ -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)
}
}
@@ -114,6 +114,63 @@ class SwiftyMarkdownTests: XCTestCase {
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)
}
/*
@@ -132,17 +189,13 @@ class SwiftyMarkdownTests: XCTestCase {
}
func testReportedCrashingStrings() {
let text = "[**\\!bang**](https://duckduckgo.com/bang) "
let expected = "\\!bang"
let output = SwiftyMarkdown(string: text).attributedString().string
XCTAssertEqual(output, expected)
}
func testThatYAMLMetadataIsRemoved() {
// let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!", expectedOutput: "Finally some Markdown!")
// let md = SwiftyMarkdown(string: yaml.input)
// XCTAssertEqual(md.attributedString().string, yaml.expectedOutput)
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)")
}
}
}
@@ -13,13 +13,14 @@ class SwiftyMarkdownPerformanceTests: XCTestCase {
func testThatFilesAreProcessedQuickly() {
guard let url = Bundle(for: SwiftyMarkdownPerformanceTests.self).url(forResource: "test", withExtension: "md") else {
XCTFail("Failed to load test.md in test bundle")
return
}
let url = self.resourceURL(for: "test.md")
measure {
let md = SwiftyMarkdown(url: url)
_ = md?.attributedString()
guard let md = SwiftyMarkdown(url: url) else {
XCTFail("Failed to load file")
return
}
_ = md.attributedString()
}
}
@@ -32,7 +33,7 @@ class SwiftyMarkdownPerformanceTests: XCTestCase {
}
}
func testThatVeryLongStringsAreProcessedQuickly() {
func testThatVeryLongStringsAreProcessedQuickly() {
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
let md = SwiftyMarkdown(string: string)
measure {
@@ -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"
+35 -20
View File
@@ -1,25 +1,26 @@
fastlane documentation
================
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
```sh
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## iOS
### ios patch
```sh
[bundle exec] fastlane ios patch
```
fastlane ios patch
```
This does the following:
@@ -29,10 +30,13 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the patch version
### ios minor
```sh
[bundle exec] fastlane ios minor
```
fastlane ios minor
```
This does the following:
@@ -42,10 +46,13 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the minor version
### ios major
```sh
[bundle exec] fastlane ios major
```
fastlane ios major
```
This does the following:
@@ -55,19 +62,27 @@ This does the following:
- Ensures Cocoapods compatibility
- Bumps the major version
### ios test
```
fastlane ios test
```sh
[bundle exec] fastlane ios test
```
### ios submit_pod
```sh
[bundle exec] fastlane ios submit_pod
```
fastlane ios submit_pod
```
Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to run tests, bump versions, and commit changes.
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).