Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dde451ab4e | |||
| ae97f0fbea | |||
| 040d7896fe | |||
| 403285e46f | |||
| af94dd615e | |||
| 99ded8446b | |||
| 2bbf1213a6 | |||
| aa748f2cdd | |||
| 851b547dcd | |||
| 5b0a1e7633 | |||
| 13955aac92 | |||
| 0b40036eda | |||
| 2e3d59324a | |||
| 9d1457a96a | |||
| eb9742b518 | |||
| 569daaf779 | |||
| 649d7690e1 | |||
| 2b8df10a0d | |||
| c9fcecafc3 | |||
| 6c64c16475 | |||
| d87627c957 | |||
| 1ecc196398 | |||
| 6103068a26 | |||
| 429d557c9c | |||
| 2e69439003 | |||
| a93ccedac5 | |||
| bc81245371 | |||
| ecebb6d8b8 | |||
| 1df35418f2 | |||
| 5e8c7d03ff | |||
| 6ab69d3d82 | |||
| 9cf9d6bfbe | |||
| f4529f5ff9 | |||
| 4592cdd4fa | |||
| 17a2b6b5ff | |||
| 20d2827573 | |||
| 98f6beced3 | |||
| fab59935e2 | |||
| 54e1f56cad | |||
| abc0768701 | |||
| 95c1e59344 | |||
| 4581712c5e | |||
| b0e9e36c6b | |||
| 61490b1e13 | |||
| bcbf3bd38c | |||
| 79467395b5 | |||
| 4e2f5c234e | |||
| 898a1ca2ed | |||
| ba5d8ce9c6 | |||
| 68336ab630 | |||
| 368f0da628 | |||
| 72ff31b6ff | |||
| 924d06bcfe | |||
| a1aa1a0eac | |||
| 71a64dee21 | |||
| 71eb29ada0 | |||
| 711850056d | |||
| d2cbd0ce96 | |||
| 61c6701518 | |||
| 8518dfb406 | |||
| a06d690000 | |||
| 09d4166025 | |||
| 1f162f124e | |||
| 79f35fa4aa | |||
| 095b2d3a6c | |||
| 8a77770f4d | |||
| 6389a8cbe5 | |||
| e467e992e2 | |||
| f68e64505b | |||
| d2c2af66fd | |||
| b7f451993b | |||
| b095feaca5 | |||
| f963bfec5b | |||
| 36f0c4d9c7 | |||
| 3b9f770ea9 | |||
| a1dfe192af | |||
| 050ca10694 | |||
| 45d03f88b7 | |||
| 586cecff2f | |||
| 124e0f0a48 | |||
| d510f69d10 | |||
| 19c473e81d | |||
| a209277d97 | |||
| 3546711696 | |||
| 3aa40eaa38 | |||
| 0bd12bd5a7 | |||
| d3a820b6e9 | |||
| 56f3c87925 | |||
| aac0c4a74c | |||
| 757e3fd212 | |||
| 2c2da71337 | |||
| d779ce978d | |||
| 0da260626a | |||
| 732a01872f | |||
| e7d60503cd | |||
| 9c9e6c3597 | |||
| ff36dbb07f | |||
| d149b1aa0e | |||
| e117006ad2 | |||
| cb49124b9c | |||
| c2325bac2f | |||
| 81857b5f4d | |||
| 4c1743ab80 | |||
| 35352af0a8 | |||
| 1d25236fec | |||
| d95ee23795 | |||
| e7d6f0ec35 | |||
| c88e81a51f | |||
| 6e6de412c9 | |||
| 6d6f83d4b0 | |||
| 9ab2d38c7d | |||
| e339833936 | |||
| bddcfa43a1 | |||
| 12ce9a365f | |||
| 616ede7202 | |||
| b000ab0be5 | |||
| 5ddb58a95d | |||
| f4520a11b5 | |||
| abaac24b16 | |||
| 57293f4261 | |||
| 1e94e474ed | |||
| 7ffab1963b | |||
| a63fb0dfe4 | |||
| e0e3f9a555 | |||
| 77b51a9592 | |||
| f1ead65825 | |||
| 7bb5c1232d | |||
| 1db338f7da | |||
| 65f1c9a552 | |||
| f6d752dec9 | |||
| 1a594e6841 | |||
| cb2ff2e3ea | |||
| 1fff74b946 | |||
| db1c0f7902 | |||
| 9a2cd21b15 | |||
| fdbfbcda54 | |||
| 04b2f41c00 | |||
| 957ec2b442 | |||
| 3848d54e45 | |||
| 85b1a0b944 | |||
| 289925e34d | |||
| f8567bd974 | |||
| 10ed2abd88 | |||
| 0e7ede23c3 | |||
| d316d83848 | |||
| 3c149a65e7 | |||
| 7a00026c2d | |||
| 5f8a6ba7dd | |||
| 6797288c0e | |||
| 96e6ac313c | |||
| 76f76d8178 | |||
| ea0717903e | |||
| 845985236a | |||
| e3e89715f8 | |||
| b077d32f52 | |||
| fc9d8b2646 | |||
| 43f94723c8 | |||
| ea306673e8 | |||
| 15f2a353be | |||
| e9fed1ced9 | |||
| 67297197e8 | |||
| 6ee2001d43 | |||
| 4bd6bf498f | |||
| 1f5a6d4e50 | |||
| 04de344a43 | |||
| f386286a7e | |||
| 7c07c37f10 | |||
| 07e9cf69a4 | |||
| c86ae343f1 | |||
| 170e4b6fa2 | |||
| d62bf1dee9 | |||
| c8e6bc6b44 | |||
| 18a1f2922a | |||
| 6d1ec33196 | |||
| 63f489eebe | |||
| 01d153b3ca | |||
| 5148e8ad77 | |||
| 994af3ad6d | |||
| 2b7bfc81d7 | |||
| aaf8f3618a | |||
| a030f210b3 | |||
| be2eaf522c | |||
| d48d6b1f90 | |||
| 100bb3df20 | |||
| 29da129a24 | |||
| 9fcdddf484 | |||
| e653548441 | |||
| ff0fce7eb7 | |||
| 99726212be | |||
| 0bfeba8188 | |||
| 7985b57341 | |||
| cb0ecbd67d | |||
| 1d4fb92429 | |||
| 7f276de733 | |||
| 5870b838e7 | |||
| b2dc325510 | |||
| 195f1a5491 | |||
| cac0c00e0f | |||
| e211af53f5 | |||
| 15c6b77288 | |||
| 4c0a60faa0 | |||
| b0bcb587cb | |||
| 02837925b9 | |||
| cc2ea97b4c | |||
| 0ab154977a | |||
| ac99c2efba | |||
| 39073280de | |||
| 0bca66728f | |||
| 38e2c0d733 | |||
| f4f7c7bd55 | |||
| cd30189674 | |||
| b51f13054c | |||
| 6487a98d90 | |||
| b9a01c7f87 | |||
| c70c5671c8 | |||
| 7bdd5d35ad | |||
| 76b27f4571 | |||
| c27ff1d9a5 | |||
| 0ff9fe8c1c | |||
| eb63a4ea5e | |||
| aa01b6bc58 | |||
| 37f08f09ae | |||
| c96f17edd0 | |||
| 6288214d26 | |||
| 66f5db3d74 | |||
| 057cf6a4e9 | |||
| 1a4cfe06aa | |||
| 455a844912 | |||
| 38920d2e09 | |||
| cce77d50ce | |||
| 23a756f23e | |||
| 27741a1128 | |||
| a57899227d | |||
| 0de132c464 | |||
| f3d1cc33d0 | |||
| 6023b1e959 | |||
| 3a108fe096 | |||
| e6e074ed9f | |||
| ad2d909681 | |||
| a02c383230 | |||
| 9b99765a59 | |||
| 5fae172428 | |||
| 3e0ff29449 | |||
| 3c4b78254d | |||
| e7e4eda3c1 | |||
| 5fb8786be0 | |||
| b9b2df80c5 | |||
| 2ef78f460d |
@@ -36,6 +36,7 @@ playground.xcworkspace
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
@@ -61,3 +62,6 @@ Carthage/Build
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/screenshots
|
||||
|
||||
# OS X
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// SwiftyMarkdownExample macOS
|
||||
//
|
||||
// Created by Simon Fairbairn on 01/02/2020.
|
||||
// Copyright © 2020 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
// Insert code here to initialize your application
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="SwiftyMarkdownExample macOS" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="SwiftyMarkdownExample macOS" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About SwiftyMarkdownExample macOS" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide SwiftyMarkdownExample macOS" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit SwiftyMarkdownExample macOS" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="dMs-cI-mzQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||
<items>
|
||||
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
|
||||
<connections>
|
||||
<action selector="newDocument:" target="Ady-hI-5gd" id="4Si-XN-c54"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
|
||||
<connections>
|
||||
<action selector="openDocument:" target="Ady-hI-5gd" id="bVn-NM-KNZ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open Recent" id="tXI-mr-wws">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
|
||||
<items>
|
||||
<menuItem title="Clear Menu" id="vNY-rz-j42">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="Daa-9d-B3U"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||
<connections>
|
||||
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="Ady-hI-5gd" id="teZ-XB-qJY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
|
||||
<connections>
|
||||
<action selector="saveDocumentAs:" target="Ady-hI-5gd" id="mDf-zr-I0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
|
||||
<connections>
|
||||
<action selector="revertDocumentToSaved:" target="Ady-hI-5gd" id="iJ3-Pv-kwq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
|
||||
<connections>
|
||||
<action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Format" id="jxT-CU-nIS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
|
||||
<items>
|
||||
<menuItem title="Font" id="Gi5-1S-RQB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
|
||||
<items>
|
||||
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
|
||||
<connections>
|
||||
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
|
||||
<connections>
|
||||
<action selector="underline:" target="Ady-hI-5gd" id="FYS-2b-JAY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
|
||||
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
|
||||
<menuItem title="Kern" id="jBQ-r6-VK2">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="GUa-eO-cwY">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardKerning:" target="Ady-hI-5gd" id="6dk-9l-Ckg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="cDB-IK-hbR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffKerning:" target="Ady-hI-5gd" id="U8a-gz-Maa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Tighten" id="46P-cB-AYj">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="tightenKerning:" target="Ady-hI-5gd" id="hr7-Nz-8ro"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Loosen" id="ogc-rX-tC1">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="loosenKerning:" target="Ady-hI-5gd" id="8i4-f9-FKE"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Ligatures" id="o6e-r0-MWq">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="agt-UL-0e3">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardLigatures:" target="Ady-hI-5gd" id="7uR-wd-Dx6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="J7y-lM-qPV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffLigatures:" target="Ady-hI-5gd" id="iX2-gA-Ilz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use All" id="xQD-1f-W4t">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useAllLigatures:" target="Ady-hI-5gd" id="KcB-kA-TuK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Baseline" id="OaQ-X3-Vso">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="3Om-Ey-2VK">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unscript:" target="Ady-hI-5gd" id="0vZ-95-Ywn"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Superscript" id="Rqc-34-cIF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="superscript:" target="Ady-hI-5gd" id="3qV-fo-wpU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Subscript" id="I0S-gh-46l">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="subscript:" target="Ady-hI-5gd" id="Q6W-4W-IGz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Raise" id="2h7-ER-AoG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="raiseBaseline:" target="Ady-hI-5gd" id="4sk-31-7Q9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Lower" id="1tx-W0-xDw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowerBaseline:" target="Ady-hI-5gd" id="OF1-bc-KW4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
|
||||
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
|
||||
<connections>
|
||||
<action selector="orderFrontColorPanel:" target="Ady-hI-5gd" id="mSX-Xz-DV3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
|
||||
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyFont:" target="Ady-hI-5gd" id="GJO-xA-L4q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteFont:" target="Ady-hI-5gd" id="JfD-CL-leO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Text" id="Fal-I4-PZk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Text" id="d9c-me-L2H">
|
||||
<items>
|
||||
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
|
||||
<connections>
|
||||
<action selector="alignLeft:" target="Ady-hI-5gd" id="zUv-R1-uAa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
|
||||
<connections>
|
||||
<action selector="alignCenter:" target="Ady-hI-5gd" id="spX-mk-kcS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Justify" id="J5U-5w-g23">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="alignJustified:" target="Ady-hI-5gd" id="ljL-7U-jND"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
|
||||
<connections>
|
||||
<action selector="alignRight:" target="Ady-hI-5gd" id="r48-bG-YeY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
|
||||
<menuItem title="Writing Direction" id="H1b-Si-o9J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
|
||||
<items>
|
||||
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="YGs-j5-SAR">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionNatural:" target="Ady-hI-5gd" id="qtV-5e-UBP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="Lbh-J2-qVU">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="S0X-9S-QSf"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="jFq-tB-4Kx">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="5fk-qB-AqJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
|
||||
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="Nop-cj-93Q">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionNatural:" target="Ady-hI-5gd" id="lPI-Se-ZHp"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="BgM-ve-c93">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="caW-Bv-w94"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="RB4-Sm-HuC">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="EXD-6r-ZUu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
|
||||
<menuItem title="Show Ruler" id="vLm-3I-IUL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleRuler:" target="Ady-hI-5gd" id="FOx-HJ-KwY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyRuler:" target="Ady-hI-5gd" id="71i-fW-3W2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteRuler:" target="Ady-hI-5gd" id="cSh-wd-qM2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="SwiftyMarkdownExample macOS Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -3,9 +3,11 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
@@ -13,12 +15,22 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6</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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+280
-68
@@ -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,74 +279,72 @@
|
||||
F4CE98A01C8AEF7D00D735C1 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0720;
|
||||
LastUpgradeCheck = 0720;
|
||||
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;
|
||||
};
|
||||
F4CE98BB1C8AEF7D00D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
TestTargetID = F4CE98A71C8AEF7D00D735C1;
|
||||
};
|
||||
F4CE98C61C8AEF7D00D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
TestTargetID = F4CE98A71C8AEF7D00D735C1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = F4CE98A31C8AEF7D00D735C1 /* Build configuration list for PBXProject "SwiftyMarkdownExample" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
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;
|
||||
@@ -321,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;
|
||||
@@ -362,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 = (
|
||||
@@ -381,21 +443,96 @@
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
F4B4A45723E4E17400550249 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 52T262DA8V;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = "SwiftyMarkdownExample macOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.voyagetravelapps.SwiftyMarkdownExample-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F4B4A45823E4E17400550249 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 52T262DA8V;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = "SwiftyMarkdownExample macOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.voyagetravelapps.SwiftyMarkdownExample-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
F4CE98CE1C8AEF7D00D735C1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
@@ -417,7 +554,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -430,17 +567,28 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
@@ -456,9 +604,11 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
@@ -469,9 +619,14 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -480,9 +635,14 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -491,9 +651,15 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -503,9 +669,15 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
|
||||
};
|
||||
name = Release;
|
||||
@@ -514,9 +686,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SwiftyMarkdownExample;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
@@ -526,9 +704,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SwiftyMarkdownExample;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
@@ -537,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 = (
|
||||
@@ -574,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 */;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+16
@@ -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
|
||||
}
|
||||
+6
-6
@@ -14,30 +14,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
|
||||
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(application: UIApplication) {
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(application: UIApplication) {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(application: UIApplication) {
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(application: UIApplication) {
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(application: UIApplication) {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
+30
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
@@ -30,6 +40,16 @@
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
@@ -59,6 +79,16 @@
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bubble.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait" appearance="dark"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftyMarkdownExample" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qZP-CU-74n">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="583"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<attributedString key="attributedText"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<dataDetectorType key="dataDetectorTypes" link="YES"/>
|
||||
</textView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="je8-G1-qej">
|
||||
<rect key="frame" x="24" y="591" width="327" height="68"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="EOv-1r-ixy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="327" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
</textField>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="XzJ-SY-mRx">
|
||||
<rect key="frame" x="0.0" y="34" width="327" height="34"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ksq-jO-8Gt">
|
||||
<rect key="frame" x="0.0" y="0.0" width="163.5" height="34"/>
|
||||
<state key="normal" title="Reload File"/>
|
||||
<connections>
|
||||
<action selector="reloadText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="dg3-xi-352"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JK2-Lo-xRa">
|
||||
<rect key="frame" x="163.5" y="0.0" width="163.5" height="34"/>
|
||||
<state key="normal" title="Process Text"/>
|
||||
<connections>
|
||||
<action selector="processText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="e9I-rJ-ifc"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="1yU-8N-26a"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="qZP-CU-74n" secondAttribute="trailing" id="F5p-iG-zTB"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="je8-G1-qej" secondAttribute="trailing" constant="8" id="JKP-2n-p24"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="je8-G1-qej" secondAttribute="bottom" constant="8" id="cvL-k7-iKp"/>
|
||||
<constraint firstItem="je8-G1-qej" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" constant="8" id="gnq-BF-hFJ"/>
|
||||
<constraint firstItem="je8-G1-qej" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="8" id="kJ0-Hf-RwN"/>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="srf-u0-j0n"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="textField" destination="EOv-1r-ixy" id="k2p-vH-CR0"/>
|
||||
<outlet property="textView" destination="qZP-CU-74n" id="VO1-kx-Lpd"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="24.800000000000001" y="34.632683658170919"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// SwiftyMarkdownExample
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftyMarkdown
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
|
||||
@IBOutlet weak var textField : UITextField!
|
||||
@IBOutlet weak var textView : UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// This is to help debugging.
|
||||
reloadText(nil)
|
||||
|
||||
self.textField.text = "Yo I'm a *single* line **string**. How do I look?"
|
||||
}
|
||||
|
||||
@IBAction func processText( _ sender : UIButton? ) {
|
||||
guard let existentText = self.textField.text else {
|
||||
return
|
||||
}
|
||||
self.textView.attributedText = SwiftyMarkdown(string: existentText).attributedString()
|
||||
}
|
||||
|
||||
@IBAction func reloadText( _ sender : UIButton? ) {
|
||||
|
||||
self.textView.dataDetectorTypes = UIDataDetectorTypes.all
|
||||
|
||||
if let url = Bundle.main.url(forResource: "example", withExtension: "md"), let md = SwiftyMarkdown(url: url) {
|
||||
md.h2.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h2.color = UIColor.blue
|
||||
md.h2.alignment = .center
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
md.strikethrough.color = .tertiaryLabel
|
||||
} else {
|
||||
md.strikethrough.color = .lightGray
|
||||
}
|
||||
|
||||
md.blockquotes.fontStyle = .italic
|
||||
|
||||
md.underlineLinks = true
|
||||
|
||||
self.textView.attributedText = md.attributedString()
|
||||
|
||||
} else {
|
||||
fatalError("Error loading file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Swifty Markdown
|
||||
|
||||
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even with custom fonts.
|
||||
|
||||
Show Images From Your App Bundle!
|
||||
---
|
||||

|
||||
|
||||
Customise fonts and colors easily in a Swift-like way:
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
md.h2.fontName = "AvenirNextCondensed-Medium"
|
||||
md.h2.color = UIColor.redColor()
|
||||
md.h2.alignment = .center
|
||||
|
||||
It supports the standard Markdown syntax, like *italics*, _underline italics_, **bold**, `backticks for code`, ~~strikethrough~~, and headings.
|
||||
|
||||
It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and \`backticks\`. It also supports inline Markdown [Links](http://voyagetravelapps.com/).
|
||||
|
||||
> It also now supports blockquotes
|
||||
> and it supports whole-line italic and bold styles so you can go completely wild with styling! Wow! Such styles! Much fun!
|
||||
|
||||
**Lists**
|
||||
|
||||
- It Supports
|
||||
- Unordered
|
||||
- Lists
|
||||
- Indented item with a longer string to make sure indentation is consistent
|
||||
- Second level indent with a longer string to make sure indentation is consistent
|
||||
- List item with a longer string to make sure indentation is consistent
|
||||
|
||||
1. And
|
||||
1. Ordered
|
||||
1. Lists
|
||||
1. Indented item
|
||||
1. Second level indent
|
||||
1. (Use `1.` as the list item identifier)
|
||||
1. List item
|
||||
1. List item
|
||||
- Mix
|
||||
- List styles
|
||||
1. List item with a longer string to make sure indentation is consistent
|
||||
1. List item
|
||||
1. List item
|
||||
1. List item
|
||||
1. List item
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class SwiftyMarkdownExampleTests: XCTestCase {
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock {
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
# A sample Gemfile
|
||||
source "https://rubygems.org"
|
||||
|
||||
# gem "rails"
|
||||
gem "cocoapods"
|
||||
gem 'fastlane'
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (6.1.6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.588.0)
|
||||
aws-sdk-core (3.131.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.11.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-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.8.0)
|
||||
nap (~> 1.0)
|
||||
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)
|
||||
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.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
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.6)
|
||||
emoji_regex (3.2.3)
|
||||
escape (0.0.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-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.206.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.21.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.5.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.14.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.2)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.1.3)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.3.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.15.0)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.16.1)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.1)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.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.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.8
|
||||
@@ -0,0 +1,19 @@
|
||||
// swift-tools-version:5.1
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftyMarkdown",
|
||||
platforms: [
|
||||
.iOS(SupportedPlatform.IOSVersion.v11),
|
||||
.tvOS(SupportedPlatform.TVOSVersion.v11),
|
||||
.macOS(.v10_12),
|
||||
.watchOS(.v4)
|
||||
],
|
||||
products: [
|
||||
.library(name: "SwiftyMarkdown", targets: ["SwiftyMarkdown"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "SwiftyMarkdown"),
|
||||
.testTarget(name: "SwiftyMarkdownTests", dependencies: ["SwiftyMarkdown"])
|
||||
]
|
||||
)
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
}
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static let lineRules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static let characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = BasicStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = BasicStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
let string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
}
|
||||
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
}
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
var strings : [String] = []
|
||||
for line in foundAttributes {
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
fontName = h1.fontName
|
||||
fontSize = h1.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
fontName = h2.fontName
|
||||
fontSize = h2.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
fontName = h3.fontName
|
||||
fontSize = h3.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
fontName = h4.fontName
|
||||
fontSize = h4.fontSize
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
fontName = h5.fontName
|
||||
fontSize = h5.fontSize
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
fontName = h6.fontName
|
||||
fontSize = h6.fontSize
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
default:
|
||||
fontName = body.fontName
|
||||
fontSize = body.fontSize
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return body.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
var outputLine = line.line
|
||||
if let style = line.lineStyle as? MarkdownLineStyle, style == .codeblock {
|
||||
outputLine = "\t\(outputLine)"
|
||||
}
|
||||
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
for token in tokens {
|
||||
var font = self.font(for: line)
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
if let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
if let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
}
|
||||
attributes[.font] = font
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
}
|
||||
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
continue
|
||||
}
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let image = UIImage(named: "bubble")
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = image
|
||||
let att = NSAttributedString(attachment: image1Attachment)
|
||||
|
||||
|
||||
|
||||
var str = "# Hello, *playground* `code` **bold** "
|
||||
|
||||
let md = SwiftyMarkdown(string: str)
|
||||
md.body.color = .red
|
||||
md.h1.color = .white
|
||||
md.h1.fontName = "Noteworthy-Light"
|
||||
|
||||
md.link.color = .red
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
md.attributedString()
|
||||
|
||||
//: [Next](@next)
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,285 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
func repeating( _ max : Int ) -> String {
|
||||
var output = self
|
||||
for _ in 1..<max {
|
||||
output += self
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
enum TagState {
|
||||
case none
|
||||
case open
|
||||
case intermediate
|
||||
case closed
|
||||
}
|
||||
|
||||
struct TagString {
|
||||
var state : TagState = .none
|
||||
var preOpenString = ""
|
||||
var openTagString = ""
|
||||
var intermediateString = ""
|
||||
var intermediateTagString = ""
|
||||
var metadataString = ""
|
||||
var closedTagString = ""
|
||||
var postClosedString = ""
|
||||
|
||||
let rule : Rule
|
||||
|
||||
init( with rule : Rule ) {
|
||||
self.rule = rule
|
||||
}
|
||||
|
||||
mutating func append( _ string : String? ) {
|
||||
guard let existentString = string else {
|
||||
return
|
||||
}
|
||||
switch self.state {
|
||||
case .none:
|
||||
self.preOpenString += existentString
|
||||
case .open:
|
||||
self.intermediateString += existentString
|
||||
case .intermediate:
|
||||
self.metadataString += existentString
|
||||
case .closed:
|
||||
self.postClosedString += existentString
|
||||
}
|
||||
}
|
||||
|
||||
mutating func append( contentsOf tokenGroup: [TokenGroup] ) {
|
||||
print(tokenGroup)
|
||||
for token in tokenGroup {
|
||||
switch token.state {
|
||||
case .none:
|
||||
self.append(token.string)
|
||||
case .open:
|
||||
if self.state != .none {
|
||||
self.preOpenString += token.string
|
||||
} else {
|
||||
self.openTagString += token.string
|
||||
}
|
||||
case .intermediate:
|
||||
if self.state != .open {
|
||||
self.intermediateString += token.string
|
||||
} else {
|
||||
self.intermediateTagString += token.string
|
||||
}
|
||||
case .closed:
|
||||
if self.rule.intermediateTag != nil && self.state != .intermediate {
|
||||
self.metadataString += token.string
|
||||
} else {
|
||||
self.closedTagString += token.string
|
||||
}
|
||||
}
|
||||
self.state = token.state
|
||||
}
|
||||
}
|
||||
|
||||
mutating func tokens() -> [Token] {
|
||||
print(self)
|
||||
var tokens : [Token] = []
|
||||
|
||||
if !self.preOpenString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: self.preOpenString))
|
||||
}
|
||||
if !self.openTagString.isEmpty {
|
||||
tokens.append(Token(type: .openTag, inputString: self.openTagString))
|
||||
}
|
||||
if !self.intermediateString.isEmpty {
|
||||
var token = Token(type: .string, inputString: self.intermediateString)
|
||||
token.metadataString = self.metadataString
|
||||
tokens.append(token)
|
||||
}
|
||||
if !self.intermediateTagString.isEmpty {
|
||||
tokens.append(Token(type: .intermediateTag, inputString: self.intermediateTagString))
|
||||
}
|
||||
if !self.metadataString.isEmpty {
|
||||
tokens.append(Token(type: .metadata, inputString: self.metadataString))
|
||||
}
|
||||
if !self.closedTagString.isEmpty {
|
||||
tokens.append(Token(type: .closeTag, inputString: self.closedTagString))
|
||||
}
|
||||
|
||||
self.preOpenString = ""
|
||||
self.openTagString = ""
|
||||
self.intermediateString = ""
|
||||
self.intermediateTagString = ""
|
||||
self.metadataString = ""
|
||||
self.closedTagString = ""
|
||||
self.postClosedString = ""
|
||||
|
||||
self.state = .none
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
struct TokenGroup {
|
||||
enum TokenGroupType {
|
||||
case string
|
||||
case tag
|
||||
case escape
|
||||
}
|
||||
|
||||
let string : String
|
||||
let isEscaped : Bool
|
||||
let type : TokenGroupType
|
||||
var state : TagState = .none
|
||||
}
|
||||
|
||||
|
||||
|
||||
func getTokenGroups( for string : inout String, with rule : Rule, shouldEmpty : Bool = false ) -> [TokenGroup] {
|
||||
if string.isEmpty {
|
||||
return []
|
||||
}
|
||||
let maxCount = rule.openTag.count * rule.maxTags
|
||||
var groups : [TokenGroup] = []
|
||||
|
||||
let maxTag = rule.openTag.repeating(rule.maxTags)
|
||||
|
||||
if maxTag.contains(string) {
|
||||
if string.count == maxCount || shouldEmpty {
|
||||
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
|
||||
token.state = .open
|
||||
groups.append(token)
|
||||
string.removeAll()
|
||||
}
|
||||
|
||||
} else if string == rule.intermediateTag {
|
||||
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
|
||||
token.state = .intermediate
|
||||
groups.append(token)
|
||||
string.removeAll()
|
||||
} else if string == rule.closingTag {
|
||||
var token = TokenGroup(string: string, isEscaped: false, type: .tag)
|
||||
token.state = .closed
|
||||
groups.append(token)
|
||||
string.removeAll()
|
||||
}
|
||||
|
||||
if shouldEmpty && !string.isEmpty {
|
||||
let token = TokenGroup(string: string, isEscaped: false, type: .tag)
|
||||
groups.append(token)
|
||||
string.removeAll()
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func scan( _ string : String, with rule : Rule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTag = rule.openTag.repeating(rule.maxTags)
|
||||
|
||||
var tagString = TagString(with: rule)
|
||||
|
||||
var openTagFound : TagState = .none
|
||||
var regularCharacters = ""
|
||||
var tagGroupCount = 0
|
||||
while !scanner.isAtEnd {
|
||||
tagGroupCount += 1
|
||||
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
tagString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
tagString.append(existentString)
|
||||
}
|
||||
}
|
||||
|
||||
// The end of the string
|
||||
let maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
guard let foundTag = maybeFoundChars else {
|
||||
continue
|
||||
}
|
||||
|
||||
if foundTag == rule.openTag && foundTag.count < rule.minTags {
|
||||
tagString.append(foundTag)
|
||||
continue
|
||||
}
|
||||
|
||||
//:--
|
||||
print(foundTag)
|
||||
var tokenGroups : [TokenGroup] = []
|
||||
var escapeCharacter : Character? = nil
|
||||
var cumulatedString = ""
|
||||
for char in foundTag {
|
||||
if let existentEscapeCharacter = escapeCharacter {
|
||||
|
||||
// If any of the tags feature the current character
|
||||
let escape = String(existentEscapeCharacter)
|
||||
let nextTagCharacter = String(char)
|
||||
if rule.openTag.contains(nextTagCharacter) || rule.intermediateTag?.contains(nextTagCharacter) ?? false || rule.closingTag?.contains(nextTagCharacter) ?? false {
|
||||
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: true, type: .tag))
|
||||
escapeCharacter = nil
|
||||
} else if nextTagCharacter == escape {
|
||||
// Doesn't apply to this rule
|
||||
tokenGroups.append(TokenGroup(string: nextTagCharacter, isEscaped: false, type: .escape))
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if char == existentEscape {
|
||||
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
|
||||
escapeCharacter = char
|
||||
continue
|
||||
}
|
||||
}
|
||||
cumulatedString.append(char)
|
||||
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule))
|
||||
|
||||
}
|
||||
if let remainingEscape = escapeCharacter {
|
||||
tokenGroups.append(TokenGroup(string: String(remainingEscape), isEscaped: false, type: .escape))
|
||||
}
|
||||
|
||||
tokenGroups.append(contentsOf: getTokenGroups(for: &cumulatedString, with: rule, shouldEmpty: true))
|
||||
tagString.append(contentsOf: tokenGroups)
|
||||
|
||||
if tagString.state == .closed {
|
||||
tokens.append(contentsOf: tagString.tokens())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
tokens.append(contentsOf: tagString.tokens())
|
||||
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
//: [Next](@next)
|
||||
|
||||
|
||||
|
||||
var string = "[]([[\\[Some Link]\\]](\\(\\(\\url) [Regular link](url)"
|
||||
//string = "Text before [Regular link](url) Text after"
|
||||
var output = "[]([[Some Link]] Regular link"
|
||||
|
||||
var tokens = scan(string, with: LinkRule())
|
||||
print( tokens.filter( { $0.type == .string }).map({ $0.outputString }).joined())
|
||||
//print( tokens )
|
||||
|
||||
//string = "**\\*\\Bold\\*\\***"
|
||||
//output = "*\\Bold**"
|
||||
|
||||
//tokens = scan(string, with: AsteriskRule())
|
||||
//print( tokens )
|
||||
|
||||
+32
@@ -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() { }
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
let lineProcesser = SwiftyLineProcessor(rules: rules, defaultRule: MarkdownLineStyle.body)
|
||||
print(lineProcesser.process("#### Heading 4 ###").first?.line ?? "")
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
import SpriteKit
|
||||
import PlaygroundSupport
|
||||
|
||||
|
||||
|
||||
class GameScene : SKScene {
|
||||
var str = "# Text\n## Speaker 1\nHello, **playground**. *I* don't want to be here, you know. *I* want to be somewhere else."
|
||||
override func didMove(to view: SKView) {
|
||||
|
||||
let md = SwiftyMarkdown(string: str)
|
||||
md.h2.alignment = .center
|
||||
md.body.alignment = .center
|
||||
|
||||
let label = SKLabelNode(attributedText: md.attributedString())
|
||||
label.position = CGPoint(x: 100, y: 100)
|
||||
label.numberOfLines = 0
|
||||
label.preferredMaxLayoutWidth = 400
|
||||
label.horizontalAlignmentMode = .left
|
||||
self.addChild(label)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let view = SKView(frame: CGRect(x: 0, y: 0, width: 600, height: 500))
|
||||
let scene = GameScene(size: view.frame.size)
|
||||
scene.scaleMode = .aspectFit
|
||||
view.presentScene(scene)
|
||||
PlaygroundPage.current.liveView = view
|
||||
|
||||
//: [Next](@next)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
//: [Next](@next)
|
||||
+680
@@ -0,0 +1,680 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case processed
|
||||
case string
|
||||
case escape
|
||||
case metadata
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public var type : TokenType
|
||||
public let inputString : String
|
||||
public var metadataString : String? = nil
|
||||
public var characterStyles : [CharacterStyling] = []
|
||||
public var count : Int = 0
|
||||
public var shouldSkip : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count == 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return inputString
|
||||
case .metadata, .processed:
|
||||
return ""
|
||||
case .escape, .string:
|
||||
return inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
var replacements : [Int : [Token]] = [:]
|
||||
for (idx,token) in currentTokens.enumerated() {
|
||||
switch token.type {
|
||||
case .string:
|
||||
|
||||
if !token.shouldSkip {
|
||||
let nextTokens = self.scan(token.outputString, with: nextRule)
|
||||
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// This replaces the individual string tokens with the new token arrays
|
||||
// making sure to apply any previously found styles to the new tokens.
|
||||
for key in replacements.keys.sorted(by: { $0 > $1 }) {
|
||||
let existingToken = currentTokens[key]
|
||||
var newTokens : [Token] = []
|
||||
for token in replacements[key]! {
|
||||
var newToken = token
|
||||
if existingToken.metadataString != nil {
|
||||
newToken.metadataString = existingToken.metadataString
|
||||
}
|
||||
|
||||
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
|
||||
newTokens.append(newToken)
|
||||
}
|
||||
currentTokens.replaceSubrange(key...key, with: newTokens)
|
||||
}
|
||||
}
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].type = .metadata
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].type = .processed
|
||||
tokens[metadataIndex].type = .processed
|
||||
tokens[index].type = .processed
|
||||
}
|
||||
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
print( tokens.map( { ( $0.outputString, $0.count )}))
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
print( "Found escape (\(token.inputString))" )
|
||||
case .repeatingTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
continue
|
||||
}
|
||||
|
||||
let startIdx = idx
|
||||
var endIdx : Int? = nil
|
||||
|
||||
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
continue
|
||||
}
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
mutableTokens[i].characterStyles.append(style)
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
mutableTokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
mutableTokens[idx].count = 0
|
||||
mutableTokens[existentEnd].count = 0
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found String: \(theToken.inputString)" )
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
print ("With metadata: \(hasMetadata)" )
|
||||
}
|
||||
case .metadata:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found metadata: \(theToken.inputString)" )
|
||||
|
||||
case .processed:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found already processed tag: \(theToken.inputString)" )
|
||||
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Example customisation
|
||||
public enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
|
||||
|
||||
var str = "A standard paragraph with an *italic*, * spaced asterisk, \\*escaped asterisks\\*, _underscored italics_, \\_escaped underscores\\_, **bold** \\*\\*escaped double asterisks\\*\\*, __underscored bold__, _ spaced underscore \\_\\_escaped double underscores\\_\\_ and a `code block *with an italic that should be ignored*`."
|
||||
|
||||
//str = "**AAAA*BB\\*BB*AAAAAA**"
|
||||
str = "*_*Bold and italic*_*"
|
||||
str = "*Italic* `Code block with *ignored* italic` __Bold__"
|
||||
|
||||
struct TokenTest {
|
||||
let input : String
|
||||
let output : String
|
||||
let tokens : [Token]
|
||||
}
|
||||
|
||||
let challenge1 = TokenTest(input: "*_*italic*_*", output: "italic", tokens: [
|
||||
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
let challenge2 = TokenTest(input: "*Italic* `Code block with *ignored* italic` __Bold__", output : "Italic `Code block with *ignored* italic` Bold", tokens : [
|
||||
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Code block with *ignored* italic", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
let challenge3 = TokenTest(input: " * ", output : " * ", tokens : [
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: " ", characterStyles: [])
|
||||
])
|
||||
let challenge4 = TokenTest(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [
|
||||
Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "BB*BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]),
|
||||
])
|
||||
let challenge5 = TokenTest(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [
|
||||
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " __Not Bold__ ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
|
||||
let challenge6 = TokenTest(input: " *\\** ", output : " *** ", tokens : [
|
||||
Token(type: .string, inputString: " *** ", characterStyles: [])
|
||||
])
|
||||
|
||||
let challenge7 = TokenTest(input: " *\\**Italic*\\** ", output : " *Italic* ", tokens : [
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*Italic*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
])
|
||||
|
||||
let challenge8 = TokenTest(input: "[*Link*](https://www.neverendingvoyage.com/)", output : "Link", tokens : [
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
|
||||
])
|
||||
|
||||
let challenge9 = TokenTest(input: "`Code (should not be indented)`", output: "Code (should not be indented)", tokens: [
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
|
||||
])
|
||||
|
||||
let challenge10 = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskOneNormalAtStart = TokenTest(input: "\\**A normal string\\**", output: "*A normal string*", tokens: [
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: "A normal string*", characterStyles: [CharacterStyle.italic]),
|
||||
])
|
||||
|
||||
|
||||
let escapedBoldAtStart = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
|
||||
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
|
||||
])
|
||||
|
||||
let escapedBoldWithin = TokenTest(input: "A string with \\*\\*escaped\\*\\* asterisks", output: "A string with **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with **escaped** asterisks", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskOneNormalWithin = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
|
||||
Token(type: .string, inputString: "A string with one *", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskTwoNormalWithin = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "** asterisks", characterStyles: [])
|
||||
])
|
||||
|
||||
"Given an array of integers, return **indices** of the two numbers such that they add up to a specific target.\n\nYou may assume that each input would have **_exactly_** one solution, and you may not use the _same_ element twice.\n\n**Example:**\n\nGiven nums = [2, 7, 11, 15], target = 9,\n\nBecause nums[**0**] + nums[**1**] = 2 + 7 = 9,\nreturn [**0**, **1**]."
|
||||
|
||||
let challenges = [challenge8]
|
||||
|
||||
var images = CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1)
|
||||
var links = CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1)
|
||||
var codeblock = CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1)
|
||||
codeblock.cancels = .allRemaining
|
||||
let asterisks = CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
let underscores = CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
|
||||
let scan = SwiftyTokeniser(with: [ images, links, codeblock, asterisks])
|
||||
|
||||
|
||||
for challenge in challenges {
|
||||
let finalTokens = scan.process(challenge.input)
|
||||
let stringTokens = finalTokens.filter({ $0.type == .string })
|
||||
|
||||
guard stringTokens.count == challenge.tokens.count else {
|
||||
print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(stringTokens.count)")
|
||||
print("-------EXPECTED--------")
|
||||
for token in challenge.tokens {
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("\(token.outputString): \(token.characterStyles)")
|
||||
default:
|
||||
print("\(token.outputString)")
|
||||
}
|
||||
}
|
||||
print("-------OUTPUT--------")
|
||||
for token in finalTokens {
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("\(token.outputString): \(token.characterStyles)")
|
||||
default:
|
||||
if !token.outputString.isEmpty {
|
||||
print("\(token.outputString)")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
print("-----EXPECTATIONS-----")
|
||||
for (idx, token) in stringTokens.enumerated() {
|
||||
let expected = challenge.tokens[idx]
|
||||
|
||||
if expected.type != token.type {
|
||||
print("Failure: Token types are different. Expected: \(expected.type), found: \(token.type)")
|
||||
}
|
||||
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("Expected: \(expected.outputString): \(expected.characterStyles)")
|
||||
print("Found: \(token.outputString): \(token.characterStyles)")
|
||||
if token.metadataString != nil {
|
||||
print("Metadata: \(token.metadataString!)")
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let string = finalTokens.map({ $0.outputString }).joined()
|
||||
print("-------OUTPUT VS INPUT--------")
|
||||
print("Input: \(challenge.input)")
|
||||
print("Expected: \(challenge.output)")
|
||||
print("Output: \(string)")
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//: [Next](@next)
|
||||
@@ -0,0 +1,36 @@
|
||||
// Code inside modules can be shared between pages and other source files.
|
||||
import Foundation
|
||||
|
||||
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
|
||||
public extension StringProtocol {
|
||||
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.upperBound
|
||||
}
|
||||
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
|
||||
var indices: [Index] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
indices.append(range.lowerBound)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return indices
|
||||
}
|
||||
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
|
||||
var result: [Range<Index>] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
result.append(range)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
let defaultType : LineStyling
|
||||
public var processEmptyStrings : LineStyling?
|
||||
let lineRules : [LineRule]
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
|
||||
output.removeSubrange(output.startIndex..<range)
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
let token = element.token.trimmingCharacters(in: .whitespaces)
|
||||
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
|
||||
output.removeSubrange(range..<output.endIndex)
|
||||
return output
|
||||
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func processLineLevelAttributes( _ text : String ) -> SwiftyLine {
|
||||
if text.isEmpty, let style = processEmptyStrings {
|
||||
return SwiftyLine(line: "", lineStyle: style)
|
||||
}
|
||||
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
|
||||
for element in previousLines {
|
||||
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let charSet = CharacterSet(charactersIn: element.token )
|
||||
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
|
||||
return SwiftyLine(line: "", lineStyle: element.type)
|
||||
}
|
||||
}
|
||||
for element in lineRules {
|
||||
guard element.token.count > 0 else {
|
||||
continue
|
||||
}
|
||||
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let unprocessed = output
|
||||
|
||||
switch element.removeFrom {
|
||||
case .leading:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
case .trailing:
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .both:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Only if the output has changed in some way
|
||||
guard unprocessed != output else {
|
||||
continue
|
||||
}
|
||||
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
|
||||
return SwiftyLine(line: output, lineStyle: element.type)
|
||||
|
||||
}
|
||||
|
||||
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
for heading in string.split(separator: "\n") {
|
||||
|
||||
if processEmptyStrings == nil, heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
let input : SwiftyLine
|
||||
input = processLineLevelAttributes(String(heading))
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case processed
|
||||
case string
|
||||
case escape
|
||||
case metadata
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public var type : TokenType
|
||||
public let inputString : String
|
||||
public var metadataString : String? = nil
|
||||
public var characterStyles : [CharacterStyling] = []
|
||||
public var group : Int = 0
|
||||
public var count : Int = 0
|
||||
public var shouldSkip : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count == 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return inputString
|
||||
case .metadata, .processed:
|
||||
return ""
|
||||
case .escape, .string:
|
||||
return inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
var replacements : [Int : [Token]] = [:]
|
||||
for (idx,token) in currentTokens.enumerated() {
|
||||
switch token.type {
|
||||
case .string:
|
||||
|
||||
if !token.shouldSkip {
|
||||
let nextTokens = self.scan(token.outputString, with: nextRule)
|
||||
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// This replaces the individual string tokens with the new token arrays
|
||||
// making sure to apply any previously found styles to the new tokens.
|
||||
for key in replacements.keys.sorted(by: { $0 > $1 }) {
|
||||
let existingToken = currentTokens[key]
|
||||
var newTokens : [Token] = []
|
||||
for token in replacements[key]! {
|
||||
var newToken = token
|
||||
if existingToken.metadataString != nil {
|
||||
newToken.metadataString = existingToken.metadataString
|
||||
}
|
||||
|
||||
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
|
||||
newTokens.append(newToken)
|
||||
}
|
||||
currentTokens.replaceSubrange(key...key, with: newTokens)
|
||||
}
|
||||
}
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].type = .metadata
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].type = .processed
|
||||
tokens[metadataIndex].type = .processed
|
||||
tokens[index].type = .processed
|
||||
}
|
||||
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
print( tokens.map( { ( $0.outputString, $0.count )}))
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
print( "Found escape (\(token.inputString))" )
|
||||
case .repeatingTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
continue
|
||||
}
|
||||
|
||||
let startIdx = idx
|
||||
var endIdx : Int? = nil
|
||||
|
||||
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
continue
|
||||
}
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
mutableTokens[i].characterStyles.append(style)
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
mutableTokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
mutableTokens[idx].count = 0
|
||||
mutableTokens[existentEnd].count = 0
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found String: \(theToken.inputString)" )
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
print ("With metadata: \(hasMetadata)" )
|
||||
}
|
||||
case .metadata:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found metadata: \(theToken.inputString)" )
|
||||
|
||||
case .processed:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found already processed tag: \(theToken.inputString)" )
|
||||
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static let lineRules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static let characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
|
||||
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
let string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
if #available(iOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
if #available(iOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
}
|
||||
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
}
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
for line in foundAttributes {
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
|
||||
let lineProperties : LineProperties
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList:
|
||||
lineProperties = body
|
||||
finalTokens.insert(Token(type: .string, inputString: "・ "), at: 0)
|
||||
default:
|
||||
lineProperties = body
|
||||
break
|
||||
}
|
||||
|
||||
if lineProperties.alignment != .left {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
|
||||
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
continue
|
||||
}
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='ios' last-migration='1120'>
|
||||
<pages>
|
||||
<page name='Scanner Tests'/>
|
||||
<page name='Line Processing'/>
|
||||
<page name='Tokenising'/>
|
||||
<page name='Attributed String'/>
|
||||
<page name='SKLabelNode'/>
|
||||
<page name='Groups'/>
|
||||
</pages>
|
||||
</playground>
|
||||
@@ -1,27 +1,403 @@
|
||||
# SwiftyMarkdown
|
||||
# SwiftyMarkdown 1.0
|
||||
|
||||
SwiftyMarkdown converts Markdown files and strings into NSAttributedString using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size for whatever font you want
|
||||
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
|
||||
|
||||
## Usage
|
||||
- [What's New](#fully-rebuilt-for-2020)
|
||||
- [Installation](#installation)
|
||||
- [How to Use](#how-to-use-swiftymarkdown)
|
||||
- [Screenshot](#screenshot)
|
||||
- [Front Matter](#front-matter)
|
||||
- [Appendix](#appendix)
|
||||
|
||||
Text string
|
||||
## Fully Rebuilt For 2020!
|
||||
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
SwiftyMarkdown now features a more robust and reliable rules-based line processing and character tokenisation engine. It has added support for images stored in the bundle (``), codeblocks, blockquotes, and unordered lists!
|
||||
|
||||
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be optionally underlined by setting `underlineLinks` to `true`.
|
||||
|
||||
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
|
||||
|
||||
Support for all of Apple's platforms has been enabled.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods:
|
||||
|
||||
`pod 'SwiftyMarkdown'`
|
||||
|
||||
### SPM:
|
||||
|
||||
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
|
||||
|
||||
## How To Use SwiftyMarkdown
|
||||
|
||||
Read Markdown from a text string...
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString()
|
||||
```
|
||||
|
||||
...or from a URL.
|
||||
|
||||
```swift
|
||||
if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
|
||||
md.attributedString()
|
||||
}
|
||||
```
|
||||
|
||||
URL
|
||||
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")
|
||||
```
|
||||
|
||||
if let url = NSBundle.mainBundle().URLForResource("file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
|
||||
md.attributedString()
|
||||
}
|
||||
The attributed string can then be assigned to any label or text control that has support for attributed text.
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
let label = UILabel()
|
||||
label.attributedText = md.attributedString()
|
||||
```
|
||||
|
||||
## Supported Markdown Features
|
||||
|
||||
*italics* or _italics_
|
||||
**bold** or __bold__
|
||||
~~Linethrough~~Strikethroughs.
|
||||
`code`
|
||||
|
||||
# Header 1
|
||||
|
||||
or
|
||||
|
||||
Header 1
|
||||
====
|
||||
|
||||
## Header 2
|
||||
|
||||
or
|
||||
|
||||
Header 2
|
||||
---
|
||||
|
||||
### Header 3
|
||||
#### Header 4
|
||||
##### Header 5 #####
|
||||
###### Header 6 ######
|
||||
|
||||
Indented code blocks (spaces or tabs)
|
||||
|
||||
[Links](http://voyagetravelapps.com/)
|
||||

|
||||
|
||||
[Referenced Links][1]
|
||||
![Referenced Images][2]
|
||||
|
||||
[1]: http://voyagetravelapps.com/
|
||||
[2]: <Name of asset in bundle>
|
||||
|
||||
> Blockquotes
|
||||
|
||||
- Bulleted
|
||||
- Lists
|
||||
- Including indented lists
|
||||
- Up to three levels
|
||||
- Neat!
|
||||
|
||||
1. Ordered
|
||||
1. Lists
|
||||
1. Including indented lists
|
||||
- Up to three levels
|
||||
|
||||
|
||||
|
||||
Compound rules also work, for example:
|
||||
|
||||
It recognises **[Bold Links](http://voyagetravelapps.com/)**
|
||||
|
||||
Or [**Bold Links**](http://voyagetravelapps.com/)
|
||||
|
||||
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
|
||||
|
||||
## Customisation
|
||||
|
||||
// Setting the body name will use this font for all the heading styles as well, unless explicitly overridden
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
Set the attributes of every paragraph and character style type using straightforward dot syntax:
|
||||
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
```swift
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h1.fontSize = 16
|
||||
md.h1.alignmnent = .center
|
||||
|
||||
md.italic.color = UIColor.blueColor()
|
||||
|
||||
md.underlineLinks = true
|
||||
|
||||
md.bullet = "🍏"
|
||||
```
|
||||
|
||||
On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
There's an example project included in the repository. Open the `Example/SwiftyMarkdown.xcodeproj` file to get started.
|
||||
|
||||
## Front Matter
|
||||
|
||||
SwiftyMarkdown recognises YAML front matter and will populate the `frontMatterAttributes` property with the key-value pairs that it fines.
|
||||
|
||||
## Appendix
|
||||
|
||||
### A) All Customisable Properties
|
||||
|
||||
```swift
|
||||
h1.fontName : String
|
||||
h1.fontSize : CGFloat
|
||||
h1.color : UI/NSColor
|
||||
h1.fontStyle : FontStyle
|
||||
h1.alignment : NSTextAlignment
|
||||
|
||||
h2.fontName : String
|
||||
h2.fontSize : CGFloat
|
||||
h2.color : UI/NSColor
|
||||
h2.fontStyle : FontStyle
|
||||
h2.alignment : NSTextAlignment
|
||||
|
||||
h3.fontName : String
|
||||
h3.fontSize : CGFloat
|
||||
h3.color : UI/NSColor
|
||||
h3.fontStyle : FontStyle
|
||||
h3.alignment : NSTextAlignment
|
||||
|
||||
h4.fontName : String
|
||||
h4.fontSize : CGFloat
|
||||
h4.color : UI/NSColor
|
||||
h4.fontStyle : FontStyle
|
||||
h4.alignment : NSTextAlignment
|
||||
|
||||
h5.fontName : String
|
||||
h5.fontSize : CGFloat
|
||||
h5.color : UI/NSColor
|
||||
h5.fontStyle : FontStyle
|
||||
h5.alignment : NSTextAlignment
|
||||
|
||||
h6.fontName : String
|
||||
h6.fontSize : CGFloat
|
||||
h6.color : UI/NSColor
|
||||
h6.fontStyle : FontStyle
|
||||
h6.alignment : NSTextAlignment
|
||||
|
||||
body.fontName : String
|
||||
body.fontSize : CGFloat
|
||||
body.color : UI/NSColor
|
||||
body.fontStyle : FontStyle
|
||||
body.alignment : NSTextAlignment
|
||||
|
||||
blockquotes.fontName : String
|
||||
blockquotes.fontSize : CGFloat
|
||||
blockquotes.color : UI/NSColor
|
||||
blockquotes.fontStyle : FontStyle
|
||||
blockquotes.alignment : NSTextAlignment
|
||||
|
||||
link.fontName : String
|
||||
link.fontSize : CGFloat
|
||||
link.color : UI/NSColor
|
||||
link.fontStyle : FontStyle
|
||||
|
||||
bold.fontName : String
|
||||
bold.fontSize : CGFloat
|
||||
bold.color : UI/NSColor
|
||||
bold.fontStyle : FontStyle
|
||||
|
||||
italic.fontName : String
|
||||
italic.fontSize : CGFloat
|
||||
italic.color : UI/NSColor
|
||||
italic.fontStyle : FontStyle
|
||||
|
||||
code.fontName : String
|
||||
code.fontSize : CGFloat
|
||||
code.color : UI/NSColor
|
||||
code.fontStyle : FontStyle
|
||||
|
||||
strikethrough.fontName : String
|
||||
strikethrough.fontSize : CGFloat
|
||||
strikethrough.color : UI/NSColor
|
||||
strikethrough.fontStyle : FontStyle
|
||||
|
||||
underlineLinks : Bool
|
||||
|
||||
bullet : String
|
||||
```
|
||||
|
||||
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look. For example, perhaps you want blockquotes to default to having the italic style:
|
||||
|
||||
```swift
|
||||
md.blockquotes.fontStyle = .italic
|
||||
```
|
||||
Or, if you like a bit of chaos:
|
||||
|
||||
```swift
|
||||
md.bold.fontStyle = .italic
|
||||
md.italic.fontStyle = .bold
|
||||
```
|
||||
|
||||
### B) Advanced Customisation
|
||||
|
||||
SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules.
|
||||
|
||||
For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:
|
||||
|
||||
```swift
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
case h1
|
||||
case h2
|
||||
case previousH1
|
||||
case codeblock
|
||||
case body
|
||||
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static public var lineRules = [
|
||||
LineRule(token: " ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)
|
||||
```
|
||||
|
||||
Similarly, the character styles all follow rules:
|
||||
|
||||
```swift
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case link, bold, italic, code
|
||||
}
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
|
||||
CharacterRuleTag(tag: "]", type: .close),
|
||||
CharacterRuleTag(tag: "[", type: .metadataOpen),
|
||||
CharacterRuleTag(tag: "]", type: .metadataClose)
|
||||
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingTags: true, balancedTags: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
|
||||
]
|
||||
```
|
||||
|
||||
These Character Rules are defined by SwiftyMarkdown:
|
||||
|
||||
public struct CharacterRule : CustomStringConvertible {
|
||||
|
||||
public let primaryTag : CharacterRuleTag
|
||||
public let tags : [CharacterRuleTag]
|
||||
public let escapeCharacters : [Character]
|
||||
public let styles : [Int : CharacterStyling]
|
||||
public let minTags : Int
|
||||
public let maxTags : Int
|
||||
public var metadataLookup : Bool = false
|
||||
public var definesBoundary = false
|
||||
public var shouldCancelRemainingRules = false
|
||||
public var balancedTags = false
|
||||
}
|
||||
|
||||
1. `primaryTag`: Each rule must have at least one tag and it can be one of `repeating`, `open`, `close`, `metadataOpen`, or `metadataClose`. `repeating` tags are tags that have identical open and close characters (and often have more than 1 style depending on how many are in a group). For example, the `*` tag used in Markdown.
|
||||
2. `tags`: An array of other tags that the rule can look for. This is where you would put the `close` tag for a custom rule, for example.
|
||||
3. `escapeCharacters`: The characters that appear prior to any of the tag characters that tell the scanner to ignore the tag.
|
||||
4. `styles`: The styles that should be applied to every character between the opening and closing tags.
|
||||
5. `minTags`: The minimum number of repeating characters to be considered a successful match. For example, setting the `primaryTag` to `*` and the `minTag` to 2 would mean that `**foo**` would be a successful match wheras `*bar*` would not.
|
||||
6. `maxTags`: The maximum number of repeating characters to be considered a successful match.
|
||||
7. `metadataLookup`: Used for Markdown reference links. Tells the scanner to try to look up the metadata from this dictionary, rather than from the inline result.
|
||||
8. `definesBoundary`: In order for open and close tags to be successful, the `boundaryCount` for a given location in the string needs to be the same. Setting this property to `true` means that this rule will increase the `boundaryCount` for every character between its opening and closing tags. For example, the `[` rule defines a boundary. After it is applied, the string `*foo[bar*]` becomes `*foobar*` with a boundaryCount `00001111`. Applying the `*` rule results in the output `*foobar*` because the opening `*` tag and the closing `*` tag now have different `boundaryCount` values. It's basically a way to fix the `**[should not be bold**](url)` problem in Markdown.
|
||||
9. `shouldCancelRemainingTags`: A successful match will mark every character between the opening and closing tags as complete, thereby preventing any further rules from being applied to those characters.
|
||||
10. `balancedTags`: This flag requires that the opening and closing tags be of exactly equal length. E.g. If this is set to true, `**foo*` would result in `**foo*`. If it was false, the output would be `*foo`.
|
||||
|
||||
|
||||
|
||||
#### Rule Subsets
|
||||
|
||||
If you want to only support a small subset of Markdown, it's now easy to do.
|
||||
|
||||
This example would only process strings with `*` and `_` characters, ignoring links, images, code, and all line-level attributes (headings, blockquotes, etc.)
|
||||
```swift
|
||||
SwiftyMarkdown.lineRules = []
|
||||
|
||||
SwiftyMarkdown.characterRules = [
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
|
||||
]
|
||||
```
|
||||
|
||||
#### Custom Rules
|
||||
|
||||
If you wanted to create a rule that applied a style of `Elf` to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:
|
||||
|
||||
```swift
|
||||
enum Characters : CharacterStyling {
|
||||
case elf
|
||||
|
||||
func isEqualTo( _ other : CharacterStyling) -> Bool {
|
||||
if let other = other as? Characters else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
let characterRules = [
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "%", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.elf])
|
||||
]
|
||||
|
||||
let processor = SwiftyTokeniser( with : characterRules )
|
||||
let string = "The elf will speak now: %Here is my elf speaking%"
|
||||
let tokens = processor.process(string)
|
||||
```
|
||||
|
||||
The output is an array of tokens would be equivalent to:
|
||||
|
||||
```swift
|
||||
[
|
||||
Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: []),
|
||||
Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: [])
|
||||
]
|
||||
```
|
||||
|
||||
### C) SpriteKit Support
|
||||
|
||||
Did you know that `SKLabelNode` supports attributed text? I didn't.
|
||||
|
||||
```swift
|
||||
let smd = SwiftyMarkdown(string: "My Character's **Dialogue**")
|
||||
|
||||
let label = SKLabelNode()
|
||||
label.preferredMaxLayoutWidth = 500
|
||||
label.numberOfLines = 0
|
||||
label.attributedText = smd.attributedString()
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
layout: page
|
||||
title: "Trail Wallet FAQ"
|
||||
date: 2015-04-22 10:59
|
||||
comments: true
|
||||
sharing: true
|
||||
liking: false
|
||||
footer: true
|
||||
sidebar: false
|
||||
---
|
||||
|
||||
# Good Day To You, Walleteer!
|
||||
|
||||
We are Erin and Simon from [Never Ending Voyage][1] and we want to thank you for trying out our app. We have been travelling non-stop for seven years and part of how we support ourselves is through Trail Wallet.
|
||||
@@ -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 (``), 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/)
|
||||

|
||||
|
||||
> 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 (``), 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/)
|
||||

|
||||
|
||||
> 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!
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// CharacterRule.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 04/02/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public enum CharacterRuleTagType {
|
||||
case open
|
||||
case close
|
||||
case metadataOpen
|
||||
case metadataClose
|
||||
case repeating
|
||||
}
|
||||
|
||||
|
||||
public struct CharacterRuleTag {
|
||||
let tag : String
|
||||
let type : CharacterRuleTagType
|
||||
|
||||
public init( tag : String, type : CharacterRuleTagType ) {
|
||||
self.tag = tag
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
public struct CharacterRule : CustomStringConvertible {
|
||||
|
||||
public let primaryTag : CharacterRuleTag
|
||||
public let tags : [CharacterRuleTag]
|
||||
public let escapeCharacters : [Character]
|
||||
public let styles : [Int : CharacterStyling]
|
||||
public let minTags : Int
|
||||
public let maxTags : Int
|
||||
public var metadataLookup : Bool = false
|
||||
public var isRepeatingTag : Bool {
|
||||
return self.primaryTag.type == .repeating
|
||||
}
|
||||
public var definesBoundary = false
|
||||
public var shouldCancelRemainingRules = false
|
||||
public var balancedTags = false
|
||||
|
||||
public var description: String {
|
||||
return "Character Rule with Open tag: \(self.primaryTag.tag) and current styles : \(self.styles) "
|
||||
}
|
||||
|
||||
public func tag( for type : CharacterRuleTagType ) -> CharacterRuleTag? {
|
||||
return self.tags.filter({ $0.type == type }).first ?? nil
|
||||
}
|
||||
|
||||
public init(primaryTag: CharacterRuleTag, otherTags: [CharacterRuleTag], escapeCharacters : [Character] = ["\\"], styles: [Int : CharacterStyling] = [:], minTags : Int = 1, maxTags : Int = 1, metadataLookup : Bool = false, definesBoundary : Bool = false, shouldCancelRemainingRules : Bool = false, balancedTags : Bool = false) {
|
||||
self.primaryTag = primaryTag
|
||||
self.tags = otherTags
|
||||
self.escapeCharacters = escapeCharacters
|
||||
self.styles = styles
|
||||
self.metadataLookup = metadataLookup
|
||||
self.definesBoundary = definesBoundary
|
||||
self.shouldCancelRemainingRules = shouldCancelRemainingRules
|
||||
self.minTags = maxTags < minTags ? maxTags : minTags
|
||||
self.maxTags = minTags > maxTags ? minTags : maxTags
|
||||
self.balancedTags = balancedTags
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
enum ElementType {
|
||||
case tag
|
||||
case escape
|
||||
case string
|
||||
case space
|
||||
case newline
|
||||
case metadata
|
||||
}
|
||||
|
||||
struct Element {
|
||||
let character : Character
|
||||
var type : ElementType
|
||||
var boundaryCount : Int = 0
|
||||
var isComplete : Bool = false
|
||||
var styles : [CharacterStyling] = []
|
||||
var metadata : [String] = []
|
||||
}
|
||||
|
||||
extension CharacterSet {
|
||||
func containsUnicodeScalars(of character: Character) -> Bool {
|
||||
return character.unicodeScalars.allSatisfy(contains(_:))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// PerfomanceLog.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 04/02/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
class PerformanceLog {
|
||||
var timer : TimeInterval = 0
|
||||
let enablePerfomanceLog : Bool
|
||||
let log : OSLog
|
||||
let identifier : String
|
||||
|
||||
init( with environmentVariableName : String, identifier : String, log : OSLog ) {
|
||||
self.log = log
|
||||
self.enablePerfomanceLog = (ProcessInfo.processInfo.environment[environmentVariableName] != nil)
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard enablePerfomanceLog else { return }
|
||||
self.timer = Date().timeIntervalSinceReferenceDate
|
||||
os_log("--- TIMER %{public}@ began", log: self.log, type: .info, self.identifier)
|
||||
}
|
||||
|
||||
func tag( with string : String) {
|
||||
guard enablePerfomanceLog else { return }
|
||||
if timer == 0 {
|
||||
self.start()
|
||||
}
|
||||
os_log("TIMER %{public}@: %f %@", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer, string)
|
||||
}
|
||||
|
||||
func end() {
|
||||
guard enablePerfomanceLog else { return }
|
||||
self.timer = Date().timeIntervalSinceReferenceDate
|
||||
os_log("--- TIMER %{public}@ finished. Total time: %f", log: self.log, type: .info, self.identifier, Date().timeIntervalSinceReferenceDate - self.timer)
|
||||
self.timer = 0
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// String+SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 08/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Some helper functions based on this:
|
||||
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
|
||||
extension StringProtocol {
|
||||
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.upperBound
|
||||
}
|
||||
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
|
||||
var indices: [Index] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
indices.append(range.lowerBound)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return indices
|
||||
}
|
||||
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
|
||||
var result: [Range<Index>] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
result.append(range)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyLineProcessor"
|
||||
static let swiftyLineProcessorPerformance = OSLog(subsystem: subsystem, category: "Swifty Line Processor Performance")
|
||||
}
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
case untilClose
|
||||
}
|
||||
|
||||
public struct FrontMatterRule {
|
||||
let openTag : String
|
||||
let closeTag : String
|
||||
let keyValueSeparator : Character
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
public var processEmptyStrings : LineStyling?
|
||||
public internal(set) var frontMatterAttributes : [String : String] = [:]
|
||||
|
||||
var closeToken : String? = nil
|
||||
let defaultType : LineStyling
|
||||
|
||||
let lineRules : [LineRule]
|
||||
let frontMatterRules : [FrontMatterRule]
|
||||
|
||||
let perfomanceLog = PerformanceLog(with: "SwiftyLineProcessorPerformanceLogging", identifier: "Line Processor", log: OSLog.swiftyLineProcessorPerformance)
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling, frontMatterRules : [FrontMatterRule] = []) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
self.frontMatterRules = frontMatterRules
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
|
||||
output.removeSubrange(output.startIndex..<range)
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
let token = element.token.trimmingCharacters(in: .whitespaces)
|
||||
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
|
||||
output.removeSubrange(range..<output.endIndex)
|
||||
return output
|
||||
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func processLineLevelAttributes( _ text : String ) -> SwiftyLine? {
|
||||
if text.isEmpty, let style = processEmptyStrings {
|
||||
return SwiftyLine(line: "", lineStyle: style)
|
||||
}
|
||||
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
|
||||
|
||||
for element in lineRules {
|
||||
guard element.token.count > 0 else {
|
||||
continue
|
||||
}
|
||||
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let unprocessed = output
|
||||
|
||||
if let hasToken = self.closeToken, unprocessed != hasToken {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !text.contains(element.token) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch element.removeFrom {
|
||||
case .leading:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
case .trailing:
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .both:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .entireLine:
|
||||
let maybeOutput = output.replacingOccurrences(of: element.token, with: "")
|
||||
output = ( maybeOutput.isEmpty ) ? maybeOutput : output
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Only if the output has changed in some way
|
||||
guard unprocessed != output else {
|
||||
continue
|
||||
}
|
||||
if element.changeAppliesTo == .untilClose {
|
||||
self.closeToken = (self.closeToken == nil) ? element.token : nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
|
||||
return SwiftyLine(line: output, lineStyle: element.type)
|
||||
|
||||
}
|
||||
|
||||
for element in previousLines {
|
||||
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let charSet = CharacterSet(charactersIn: element.token )
|
||||
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
|
||||
return SwiftyLine(line: "", lineStyle: element.type)
|
||||
}
|
||||
}
|
||||
|
||||
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
|
||||
}
|
||||
|
||||
func processFrontMatter( _ strings : [String] ) -> [String] {
|
||||
guard let firstString = strings.first?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
return strings
|
||||
}
|
||||
var rulesToApply : FrontMatterRule? = nil
|
||||
for matter in self.frontMatterRules {
|
||||
if firstString == matter.openTag {
|
||||
rulesToApply = matter
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let existentRules = rulesToApply else {
|
||||
return strings
|
||||
}
|
||||
var outputString = strings
|
||||
// Remove the first line, which is the front matter opening tag
|
||||
let _ = outputString.removeFirst()
|
||||
var closeFound = false
|
||||
while !closeFound {
|
||||
let nextString = outputString.removeFirst()
|
||||
if nextString == existentRules.closeTag {
|
||||
closeFound = true
|
||||
continue
|
||||
}
|
||||
var keyValue = nextString.components(separatedBy: "\(existentRules.keyValueSeparator)")
|
||||
if keyValue.count < 2 {
|
||||
continue
|
||||
}
|
||||
let key = keyValue.removeFirst()
|
||||
let value = keyValue.joined()
|
||||
self.frontMatterAttributes[key] = value
|
||||
}
|
||||
while outputString.first?.isEmpty ?? false {
|
||||
outputString.removeFirst()
|
||||
}
|
||||
return outputString
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
|
||||
|
||||
self.perfomanceLog.start()
|
||||
|
||||
var lines = string.components(separatedBy: CharacterSet.newlines)
|
||||
lines = self.processFrontMatter(lines)
|
||||
|
||||
self.perfomanceLog.tag(with: "(Front matter completed)")
|
||||
|
||||
|
||||
for heading in lines {
|
||||
|
||||
if processEmptyStrings == nil && heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let input = processLineLevelAttributes(String(heading)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
|
||||
self.perfomanceLog.tag(with: "(line completed: \(heading)")
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
case .strikethrough:
|
||||
fontName = strikethrough.fontName ?? fontName
|
||||
fontSize = strikethrough.fontSize
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: fontSize ?? 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: fontSize ?? 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .yaml:
|
||||
return body.color
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
case .referencedLink:
|
||||
return link.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont {
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
case .h2:
|
||||
style = self.h2
|
||||
case .h3:
|
||||
style = self.h3
|
||||
case .h4:
|
||||
style = self.h4
|
||||
case .h5:
|
||||
style = self.h5
|
||||
case .h6:
|
||||
style = self.h6
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
default:
|
||||
style = self.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
finalSize = NSFont.systemFontSize
|
||||
}
|
||||
var font : NSFont
|
||||
if let existentFontName = fontName {
|
||||
if let customFont = NSFont(name: existentFontName, size: finalSize) {
|
||||
font = customFont
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
|
||||
if globalItalic {
|
||||
let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic)
|
||||
font = NSFont(descriptor: italicDescriptor, size: 0) ?? font
|
||||
}
|
||||
if globalBold {
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
|
||||
font = NSFont(descriptor: boldDescriptor, size: 0) ?? font
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> NSColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
case .yaml:
|
||||
return body.color
|
||||
case .referencedLink:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,621 @@
|
||||
//
|
||||
// SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import os.log
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyMarkdown"
|
||||
static let swiftyMarkdownPerformance = OSLog(subsystem: subsystem, category: "Swifty Markdown Performance")
|
||||
}
|
||||
|
||||
public enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
case referencedLink
|
||||
case referencedImage
|
||||
case strikethrough
|
||||
|
||||
public func isEqualTo(_ other: CharacterStyling) -> Bool {
|
||||
guard let other = other as? CharacterStyle else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
case yaml
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
case unorderedListIndentFirstOrder
|
||||
case unorderedListIndentSecondOrder
|
||||
case orderedList
|
||||
case orderedListIndentFirstOrder
|
||||
case orderedListIndentSecondOrder
|
||||
case referencedLink
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : NSColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#else
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
var lineSpacing: CGFloat { get set }
|
||||
var paragraphSpacing: CGFloat { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
public var lineSpacing : CGFloat = 0.0
|
||||
public var paragraphSpacing : CGFloat = 0.0
|
||||
}
|
||||
|
||||
@objc open class LinkStyles : BasicStyles {
|
||||
public var underlineStyle: NSUnderlineStyle = .single
|
||||
#if os(macOS)
|
||||
public lazy var underlineColor = self.color
|
||||
#else
|
||||
public lazy var underlineColor = self.color
|
||||
#endif
|
||||
}
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
|
||||
static public var frontMatterRules = [
|
||||
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
|
||||
]
|
||||
|
||||
static public var lineRules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading),
|
||||
LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
|
||||
CharacterRuleTag(tag: "]", type: .close),
|
||||
CharacterRuleTag(tag: "[", type: .metadataOpen),
|
||||
CharacterRuleTag(tag: "]", type: .metadataClose)
|
||||
], styles: [1 : CharacterStyle.image], metadataLookup: true, definesBoundary: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "![", type: .open), otherTags: [
|
||||
CharacterRuleTag(tag: "]", type: .close),
|
||||
CharacterRuleTag(tag: "(", type: .metadataOpen),
|
||||
CharacterRuleTag(tag: ")", type: .metadataClose)
|
||||
], styles: [1 : CharacterStyle.image], metadataLookup: false, definesBoundary: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
|
||||
CharacterRuleTag(tag: "]", type: .close),
|
||||
CharacterRuleTag(tag: "[", type: .metadataOpen),
|
||||
CharacterRuleTag(tag: "]", type: .metadataClose)
|
||||
], styles: [1 : CharacterStyle.link], metadataLookup: true, definesBoundary: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "[", type: .open), otherTags: [
|
||||
CharacterRuleTag(tag: "]", type: .close),
|
||||
CharacterRuleTag(tag: "(", type: .metadataOpen),
|
||||
CharacterRuleTag(tag: ")", type: .metadataClose)
|
||||
], styles: [1 : CharacterStyle.link], metadataLookup: false, definesBoundary: true),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "`", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.code], shouldCancelRemainingRules: true, balancedTags: true),
|
||||
CharacterRule(primaryTag:CharacterRuleTag(tag: "~", type: .repeating), otherTags : [], styles: [2 : CharacterStyle.strikethrough], minTags:2 , maxTags:2),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "*", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2),
|
||||
CharacterRule(primaryTag: CharacterRuleTag(tag: "_", type: .repeating), otherTags: [], styles: [1 : CharacterStyle.italic, 2 : CharacterStyle.bold], minTags:1 , maxTags:2)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = LinkStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
open var strikethrough = BasicStyles()
|
||||
|
||||
public var bullet : String = "・"
|
||||
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
public var frontMatterAttributes : [String : String] {
|
||||
get {
|
||||
return self.lineProcessor.frontMatterAttributes
|
||||
}
|
||||
}
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
var string : String
|
||||
|
||||
var orderedListCount = 0
|
||||
var orderedListIndentFirstOrderCount = 0
|
||||
var orderedListIndentSecondOrderCount = 0
|
||||
|
||||
var previouslyFoundTokens : [Token] = []
|
||||
|
||||
var applyAttachments = true
|
||||
|
||||
let perfomanceLog = PerformanceLog(with: "SwiftyMarkdownPerformanceLogging", identifier: "Swifty Markdown", log: .swiftyMarkdownPerformance)
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
#if os(macOS)
|
||||
self.setFontColorForAllStyles(with: .labelColor)
|
||||
#elseif !os(watchOS)
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
strikethrough.fontSize = size
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
open func setFontColorForAllStyles(with color: NSColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#else
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#endif
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
strikethrough.fontName = name
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
|
||||
|
||||
self.previouslyFoundTokens.removeAll()
|
||||
self.perfomanceLog.start()
|
||||
|
||||
if let existentMarkdownString = markdownString {
|
||||
self.string = existentMarkdownString
|
||||
}
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
let references : [SwiftyLine] = foundAttributes.filter({ $0.line.starts(with: "[") && $0.line.contains("]:") })
|
||||
let referencesRemoved : [SwiftyLine] = foundAttributes.filter({ !($0.line.starts(with: "[") && $0.line.contains("]:") ) })
|
||||
var keyValuePairs : [String : String] = [:]
|
||||
for line in references {
|
||||
let strings = line.line.components(separatedBy: "]:")
|
||||
guard strings.count >= 2 else {
|
||||
continue
|
||||
}
|
||||
var key : String = strings[0]
|
||||
if !key.isEmpty {
|
||||
let newstart = key.index(key.startIndex, offsetBy: 1)
|
||||
let range : Range<String.Index> = newstart..<key.endIndex
|
||||
key = String(key[range]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
keyValuePairs[key] = strings[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
self.perfomanceLog.tag(with: "(line processing complete)")
|
||||
|
||||
self.tokeniser.metadataLookup = keyValuePairs
|
||||
|
||||
for (idx, line) in referencesRemoved.enumerated() {
|
||||
if idx > 0 {
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
self.previouslyFoundTokens.append(contentsOf: finalTokens)
|
||||
self.perfomanceLog.tag(with: "(tokenising complete for line \(idx)")
|
||||
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
|
||||
}
|
||||
|
||||
self.perfomanceLog.end()
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else {
|
||||
preconditionFailure("The passed line style is not a valid Markdown Line Style")
|
||||
}
|
||||
|
||||
var listItem = self.bullet
|
||||
switch markdownLineStyle {
|
||||
case .orderedList:
|
||||
self.orderedListCount += 1
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
listItem = "\(self.orderedListCount)."
|
||||
case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder:
|
||||
self.orderedListIndentFirstOrderCount += 1
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
if markdownLineStyle == .orderedListIndentFirstOrder {
|
||||
listItem = "\(self.orderedListIndentFirstOrderCount)."
|
||||
}
|
||||
|
||||
case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder:
|
||||
self.orderedListIndentSecondOrderCount += 1
|
||||
if markdownLineStyle == .orderedListIndentSecondOrder {
|
||||
listItem = "\(self.orderedListIndentSecondOrderCount)."
|
||||
}
|
||||
|
||||
default:
|
||||
self.orderedListCount = 0
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
}
|
||||
|
||||
let lineProperties : LineProperties
|
||||
switch markdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
paragraphStyle.headIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
|
||||
let interval : CGFloat = 30
|
||||
var addition = interval
|
||||
var indent = ""
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder:
|
||||
addition = interval * 2
|
||||
indent = "\t"
|
||||
case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder:
|
||||
addition = interval * 3
|
||||
indent = "\t\t"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
lineProperties = body
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])]
|
||||
paragraphStyle.defaultTabInterval = interval
|
||||
paragraphStyle.headIndent = addition
|
||||
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0)
|
||||
|
||||
case .yaml:
|
||||
lineProperties = body
|
||||
case .previousH1:
|
||||
lineProperties = body
|
||||
case .previousH2:
|
||||
lineProperties = body
|
||||
case .body:
|
||||
lineProperties = body
|
||||
case .referencedLink:
|
||||
lineProperties = body
|
||||
}
|
||||
|
||||
let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
||||
if lineProperties.alignment != .left {
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
}
|
||||
paragraphStyle.lineSpacing = lineProperties.lineSpacing
|
||||
paragraphStyle.paragraphSpacing = lineProperties.paragraphSpacing
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
|
||||
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.link] = nil
|
||||
attributes[.strikethroughStyle] = nil
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
attributes[.underlineStyle] = nil
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
if let linkIdx = styles.firstIndex(of: .link), linkIdx < token.metadataStrings.count {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = token.metadataStrings[linkIdx] as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = self.link.underlineStyle.rawValue as AnyObject
|
||||
attributes[.underlineColor] = self.link.underlineColor
|
||||
}
|
||||
}
|
||||
|
||||
if styles.contains(.strikethrough) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
|
||||
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
attributes[.foregroundColor] = self.strikethrough.color
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
if let imgIdx = styles.firstIndex(of: .image), imgIdx < token.metadataStrings.count {
|
||||
if !self.applyAttachments {
|
||||
continue
|
||||
}
|
||||
#if !os(macOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: token.metadataStrings[imgIdx])
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#elseif !os(watchOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = NSImage(named: token.metadataStrings[imgIdx])
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
#endif
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Token.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 04/02/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
func isEqualTo( _ other : CharacterStyling ) -> Bool
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case string
|
||||
case escape
|
||||
case replacement
|
||||
}
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public let type : TokenType
|
||||
public let inputString : String
|
||||
public var metadataStrings : [String] = []
|
||||
public internal(set) var group : Int = 0
|
||||
public internal(set) var characterStyles : [CharacterStyling] = []
|
||||
public internal(set) var count : Int = 0
|
||||
public internal(set) var shouldSkip : Bool = false
|
||||
public internal(set) var tokenIndex : Int = -1
|
||||
public internal(set) var isProcessed : Bool = false
|
||||
public internal(set) var isMetadata : Bool = false
|
||||
public var children : [Token] = []
|
||||
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count <= 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .escape, .string:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .replacement:
|
||||
return self.inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
if type == .repeatingTag {
|
||||
self.count = inputString.count
|
||||
}
|
||||
}
|
||||
|
||||
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
|
||||
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
|
||||
newToken.metadataStrings = self.metadataStrings
|
||||
newToken.isMetadata = self.isMetadata
|
||||
newToken.isProcessed = self.isProcessed
|
||||
return newToken
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Iterator.Element == Token {
|
||||
var oslogDisplay: String {
|
||||
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
//: Playground - noun: a place where people can play
|
||||
|
||||
import UIKit
|
||||
import XCPlayground
|
||||
|
||||
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 600))
|
||||
|
||||
XCPlaygroundPage.currentPage.liveView = containerView
|
||||
let label = UITextView(frame: containerView.frame)
|
||||
containerView.addSubview(label)
|
||||
|
||||
|
||||
public protocol FontProperties {
|
||||
var fontName : String { get set }
|
||||
var color : UIColor { get set }
|
||||
}
|
||||
|
||||
|
||||
public struct BasicStyles : FontProperties {
|
||||
public var fontName = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
|
||||
public var color = UIColor.blackColor()
|
||||
}
|
||||
|
||||
enum LineType : Int {
|
||||
case H1, H2, H3, H4, H5, H6, Body, Italic, Bold, Code
|
||||
}
|
||||
|
||||
|
||||
public class SwiftyMarkdown {
|
||||
|
||||
public var h1 = BasicStyles()
|
||||
public var h2 = BasicStyles()
|
||||
public var h3 = BasicStyles()
|
||||
public var h4 = BasicStyles()
|
||||
public var h5 = BasicStyles()
|
||||
public var h6 = BasicStyles()
|
||||
|
||||
public var body = BasicStyles()
|
||||
public var link = BasicStyles()
|
||||
public var italic = BasicStyles()
|
||||
public var code = BasicStyles()
|
||||
public var bold = BasicStyles()
|
||||
|
||||
let string : String
|
||||
let instructionSet = NSCharacterSet(charactersInString: "\\*_`")
|
||||
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
public init?(url : NSURL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
fatalError("Couldn't read string")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
|
||||
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
|
||||
|
||||
var lineCount = 0
|
||||
|
||||
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
|
||||
|
||||
|
||||
var skipLine = false
|
||||
for line in lines {
|
||||
lineCount++
|
||||
if skipLine {
|
||||
skipLine = false
|
||||
continue
|
||||
}
|
||||
var headingFound = false
|
||||
for heading in headings {
|
||||
|
||||
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
|
||||
|
||||
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
|
||||
let endHeadingHash = " " + heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
|
||||
let finalHeadingString = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingHash, withString: "")
|
||||
|
||||
// Make Hx where x == current index
|
||||
let string = attributedStringFromString(finalHeadingString, withType: LineType(rawValue: headings.indexOf(heading)!)!)
|
||||
attributedString.appendAttributedString(string)
|
||||
headingFound = true
|
||||
}
|
||||
}
|
||||
if headingFound {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if lineCount < lines.count {
|
||||
let nextLine = lines[lineCount]
|
||||
|
||||
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H1)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
|
||||
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
|
||||
|
||||
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H2)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if line.characters.count > 0 {
|
||||
|
||||
let scanner = NSScanner(string: line)
|
||||
|
||||
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
while !scanner.atEnd {
|
||||
|
||||
var followingString : NSString?
|
||||
var string : NSString?
|
||||
// Get all the characters up to the ones we are interested in
|
||||
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
|
||||
if let hasString = string as? String {
|
||||
let bodyString = attributedStringFromString(hasString, withType: .Body)
|
||||
attributedString.appendAttributedString(bodyString)
|
||||
|
||||
var matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
|
||||
let location = scanner.scanLocation
|
||||
// If the next string after the characters is a space, then add it to the final string and continue
|
||||
if !scanner.scanUpToString(" ", intoString: nil) {
|
||||
|
||||
let charAtts = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(charAtts)
|
||||
} else {
|
||||
scanner.scanLocation = location
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
if let hasString = followingString as? String {
|
||||
let attString : NSAttributedString
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
|
||||
} else if matchedCharacters == "**" || matchedCharacters == "__" {
|
||||
attString = attributedStringFromString(hasString, withType: .Bold)
|
||||
} else {
|
||||
attString = attributedStringFromString(hasString, withType: .Italic)
|
||||
}
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
if let hasString = followingString as? String {
|
||||
let attString : NSAttributedString
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
|
||||
} else if matchedCharacters == "**" || matchedCharacters == "__" {
|
||||
attString = attributedStringFromString(hasString, withType: .Bold)
|
||||
} else {
|
||||
attString = attributedStringFromString(hasString, withType: .Italic)
|
||||
}
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func tagFromScanner( scanner : NSScanner ) -> String {
|
||||
var matchedCharacters : String = ""
|
||||
var tempCharacters : NSString?
|
||||
|
||||
// Scan the ones we are interested in
|
||||
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
|
||||
if let chars = tempCharacters as? String {
|
||||
matchedCharacters = matchedCharacters + chars
|
||||
}
|
||||
}
|
||||
return matchedCharacters
|
||||
}
|
||||
|
||||
|
||||
// Make H1
|
||||
|
||||
func attributedStringFromString(string : String, withType type : LineType ) -> NSAttributedString {
|
||||
var attributes : [String : AnyObject]
|
||||
let textStyle : String
|
||||
let fontName : String
|
||||
|
||||
var appendNewLine = true
|
||||
|
||||
switch type {
|
||||
case .H1:
|
||||
fontName = h1.fontName
|
||||
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle1
|
||||
}
|
||||
attributes = [NSForegroundColorAttributeName : h1.color]
|
||||
case .H2:
|
||||
fontName = h2.fontName
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
attributes = [NSForegroundColorAttributeName : h2.color]
|
||||
case .H3:
|
||||
fontName = h3.fontName
|
||||
textStyle = UIFontTextStyleTitle3
|
||||
attributes = [NSForegroundColorAttributeName : h3.color]
|
||||
case .H4:
|
||||
fontName = h4.fontName
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
attributes = [NSForegroundColorAttributeName : h4.color]
|
||||
case .H5:
|
||||
fontName = h5.fontName
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
attributes = [NSForegroundColorAttributeName : h5.color]
|
||||
case .H6:
|
||||
fontName = h6.fontName
|
||||
textStyle = UIFontTextStyleFootnote
|
||||
attributes = [NSForegroundColorAttributeName : h6.color]
|
||||
case .Italic:
|
||||
fontName = italic.fontName
|
||||
attributes = [NSForegroundColorAttributeName : italic.color]
|
||||
textStyle = UIFontTextStyleBody
|
||||
appendNewLine = false
|
||||
case .Bold:
|
||||
fontName = bold.fontName
|
||||
attributes = [NSForegroundColorAttributeName : bold.color]
|
||||
appendNewLine = false
|
||||
textStyle = UIFontTextStyleBody
|
||||
default:
|
||||
appendNewLine = false
|
||||
fontName = body.fontName
|
||||
textStyle = UIFontTextStyleBody
|
||||
attributes = [NSForegroundColorAttributeName:body.color]
|
||||
break
|
||||
}
|
||||
|
||||
let font = UIFont.preferredFontForTextStyle(textStyle)
|
||||
let styleDescriptor = font.fontDescriptor()
|
||||
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
|
||||
|
||||
var finalFont : UIFont
|
||||
if let font = UIFont(name: fontName, size: styleSize) {
|
||||
finalFont = font
|
||||
} else {
|
||||
finalFont = UIFont.preferredFontForTextStyle(textStyle)
|
||||
}
|
||||
|
||||
let finalFontDescriptor = finalFont.fontDescriptor()
|
||||
if type == .Italic {
|
||||
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
|
||||
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
|
||||
}
|
||||
if type == .Bold {
|
||||
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
|
||||
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
|
||||
}
|
||||
|
||||
|
||||
attributes[NSFontAttributeName] = finalFont
|
||||
|
||||
if appendNewLine {
|
||||
return NSAttributedString(string: string + "\n", attributes: attributes)
|
||||
} else {
|
||||
return NSAttributedString(string: string, attributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let url = NSBundle.mainBundle().URLForResource("test", withExtension: "md"), md = SwiftyMarkdown(url: url) {
|
||||
|
||||
label.attributedText = md.attributedString()
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
@@ -1,16 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "SwiftyMarkdown"
|
||||
s.version = "0.1.1"
|
||||
s.version = "1.2.4"
|
||||
s.summary = "Converts Markdown to NSAttributed String"
|
||||
s.homepage = "https://github.com/SimonFairbairn/Stormcloud"
|
||||
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
|
||||
s.license = 'MIT'
|
||||
s.author = { "Simon Fairbairn" => "simon@voyagetravelapps.com" }
|
||||
s.source = { :git => "https://github.com/SimonFairbairn/SwiftyMarkdown.git", :tag => s.version }
|
||||
s.social_media_url = 'https://twitter.com/SimonFairbairn'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.tvos.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.12"
|
||||
s.watchos.deployment_target = "4.0"
|
||||
s.requires_arc = true
|
||||
|
||||
s.source_files = 'SwiftyMarkdown/'
|
||||
s.source_files = 'Sources/SwiftyMarkdown/**/*'
|
||||
|
||||
s.swift_version = "5.0"
|
||||
|
||||
end
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -15,11 +15,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.2.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
@@ -6,120 +6,207 @@
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
"SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget" /* SwiftyMarkdownPackageTests */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = OBJ_54 /* Build configuration list for PBXAggregateTarget "SwiftyMarkdownPackageTests" */;
|
||||
buildPhases = (
|
||||
);
|
||||
dependencies = (
|
||||
OBJ_57 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SwiftyMarkdownPackageTests;
|
||||
productName = SwiftyMarkdownPackageTests;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; };
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */; };
|
||||
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */; };
|
||||
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E61C8AF01300D735C1 /* LICENSE */; };
|
||||
F4CE98EA1C8AF01300D735C1 /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98E71C8AF01300D735C1 /* README.md */; };
|
||||
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */; };
|
||||
F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */; };
|
||||
F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CD23E8A88400EA665D /* Token.swift */; };
|
||||
F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */; };
|
||||
F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */; };
|
||||
OBJ_40 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* String+SwiftyMarkdown.swift */; };
|
||||
OBJ_41 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* SwiftyLineProcessor.swift */; };
|
||||
OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* SwiftyMarkdown+iOS.swift */; };
|
||||
OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* SwiftyMarkdown+macOS.swift */; };
|
||||
OBJ_44 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* SwiftyMarkdown.swift */; };
|
||||
OBJ_45 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* SwiftyTokeniser.swift */; };
|
||||
OBJ_52 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
|
||||
OBJ_63 /* SwiftyMarkdownAttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */; };
|
||||
OBJ_64 /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* SwiftyMarkdownCharacterTests.swift */; };
|
||||
OBJ_65 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* SwiftyMarkdownLineTests.swift */; };
|
||||
OBJ_66 /* SwiftyMarkdownLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* SwiftyMarkdownLinkTests.swift */; };
|
||||
OBJ_67 /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */; };
|
||||
OBJ_68 /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* XCTest+SwiftyMarkdown.swift */; };
|
||||
OBJ_70 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */ = {
|
||||
F4ACB6CC23E8A5C600EA665D /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = F4CE98781C8A921300D735C1 /* Project object */;
|
||||
containerPortal = OBJ_1 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = F4CE98801C8A921300D735C1;
|
||||
remoteGlobalIDString = "SwiftyMarkdown::SwiftyMarkdownTests";
|
||||
remoteInfo = SwiftyMarkdownTests;
|
||||
};
|
||||
F4B37A0723E507C900833479 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = OBJ_1 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = "SwiftyMarkdown::SwiftyMarkdown";
|
||||
remoteInfo = SwiftyMarkdown;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyMarkdown.h; sourceTree = "<group>"; };
|
||||
F4CE98861C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownTests.swift; sourceTree = "<group>"; };
|
||||
F4CE98921C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
|
||||
F4CE98E61C8AF01300D735C1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
F4CE98E71C8AF01300D735C1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
|
||||
F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyScanner.swift; sourceTree = "<group>"; };
|
||||
F4ACB6CD23E8A88400EA665D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
|
||||
F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRule.swift; sourceTree = "<group>"; };
|
||||
F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerfomanceLog.swift; sourceTree = "<group>"; };
|
||||
OBJ_10 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
|
||||
OBJ_11 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
|
||||
OBJ_12 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
|
||||
OBJ_13 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
|
||||
OBJ_14 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = "<group>"; };
|
||||
OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownAttributedStringTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_18 /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_19 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_20 /* SwiftyMarkdownLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLinkTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_22 /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
|
||||
OBJ_26 /* Playground */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Playground; sourceTree = SOURCE_ROOT; };
|
||||
OBJ_27 /* Example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Example; sourceTree = SOURCE_ROOT; };
|
||||
OBJ_28 /* Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources; sourceTree = SOURCE_ROOT; };
|
||||
OBJ_29 /* fastlane */ = {isa = PBXFileReference; lastKnownFileType = folder; path = fastlane; sourceTree = SOURCE_ROOT; };
|
||||
OBJ_30 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
OBJ_31 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
OBJ_32 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = "<group>"; };
|
||||
OBJ_33 /* Gemfile.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile.lock; sourceTree = "<group>"; };
|
||||
OBJ_34 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
|
||||
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||
OBJ_9 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
|
||||
"SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
"SwiftyMarkdown::SwiftyMarkdownTests::Product" /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
F4CE987D1C8A921300D735C1 /* Frameworks */ = {
|
||||
OBJ_46 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F4CE98881C8A921300D735C1 /* Frameworks */ = {
|
||||
OBJ_69 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */,
|
||||
OBJ_70 /* SwiftyMarkdown.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
F4CE98771C8A921300D735C1 = {
|
||||
OBJ_15 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F4CE98831C8A921300D735C1 /* SwiftyMarkdown */,
|
||||
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */,
|
||||
F4CE98821C8A921300D735C1 /* Products */,
|
||||
F4CE98E61C8AF01300D735C1 /* LICENSE */,
|
||||
F4CE98E71C8AF01300D735C1 /* README.md */,
|
||||
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */,
|
||||
OBJ_16 /* SwiftyMarkdownTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
name = Tests;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
F4CE98821C8A921300D735C1 /* Products */ = {
|
||||
OBJ_16 /* SwiftyMarkdownTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */,
|
||||
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */,
|
||||
OBJ_17 /* SwiftyMarkdownAttributedStringTests.swift */,
|
||||
OBJ_18 /* SwiftyMarkdownCharacterTests.swift */,
|
||||
OBJ_19 /* SwiftyMarkdownLineTests.swift */,
|
||||
OBJ_20 /* SwiftyMarkdownLinkTests.swift */,
|
||||
OBJ_21 /* SwiftyMarkdownPerformanceTests.swift */,
|
||||
OBJ_22 /* XCTest+SwiftyMarkdown.swift */,
|
||||
);
|
||||
name = SwiftyMarkdownTests;
|
||||
path = Tests/SwiftyMarkdownTests;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
OBJ_23 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
"SwiftyMarkdown::SwiftyMarkdown::Product" /* SwiftyMarkdown.framework */,
|
||||
"SwiftyMarkdown::SwiftyMarkdownTests::Product" /* SwiftyMarkdownTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
F4CE98831C8A921300D735C1 /* SwiftyMarkdown */ = {
|
||||
OBJ_5 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */,
|
||||
F4CE98861C8A921300D735C1 /* Info.plist */,
|
||||
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */,
|
||||
OBJ_6 /* Package.swift */,
|
||||
OBJ_7 /* Sources */,
|
||||
OBJ_15 /* Tests */,
|
||||
OBJ_23 /* Products */,
|
||||
OBJ_26 /* Playground */,
|
||||
OBJ_27 /* Example */,
|
||||
OBJ_28 /* Resources */,
|
||||
OBJ_29 /* fastlane */,
|
||||
OBJ_30 /* LICENSE */,
|
||||
OBJ_31 /* README.md */,
|
||||
OBJ_32 /* Gemfile */,
|
||||
OBJ_33 /* Gemfile.lock */,
|
||||
OBJ_34 /* SwiftyMarkdown.podspec */,
|
||||
);
|
||||
path = SwiftyMarkdown;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */ = {
|
||||
OBJ_7 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */,
|
||||
F4CE98921C8A921300D735C1 /* Info.plist */,
|
||||
OBJ_8 /* SwiftyMarkdown */,
|
||||
);
|
||||
path = SwiftyMarkdownTests;
|
||||
sourceTree = "<group>";
|
||||
name = Sources;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
OBJ_8 /* SwiftyMarkdown */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
OBJ_9 /* String+SwiftyMarkdown.swift */,
|
||||
OBJ_10 /* SwiftyLineProcessor.swift */,
|
||||
OBJ_11 /* SwiftyMarkdown+iOS.swift */,
|
||||
OBJ_12 /* SwiftyMarkdown+macOS.swift */,
|
||||
OBJ_13 /* SwiftyMarkdown.swift */,
|
||||
OBJ_14 /* SwiftyTokeniser.swift */,
|
||||
F4ACB6CD23E8A88400EA665D /* Token.swift */,
|
||||
F4ACB6CA23E8A5C500EA665D /* SwiftyScanner.swift */,
|
||||
F4ACB6CF23E8A8A500EA665D /* CharacterRule.swift */,
|
||||
F4ACB6D123E8B08400EA665D /* PerfomanceLog.swift */,
|
||||
);
|
||||
name = SwiftyMarkdown;
|
||||
path = Sources/SwiftyMarkdown;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
F4CE987E1C8A921300D735C1 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
F4CE98801C8A921300D735C1 /* SwiftyMarkdown */ = {
|
||||
"SwiftyMarkdown::SwiftPMPackageDescription" /* SwiftyMarkdownPackageDescription */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F4CE98951C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */;
|
||||
buildConfigurationList = OBJ_48 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownPackageDescription" */;
|
||||
buildPhases = (
|
||||
F4CE987C1C8A921300D735C1 /* Sources */,
|
||||
F4CE987D1C8A921300D735C1 /* Frameworks */,
|
||||
F4CE987E1C8A921300D735C1 /* Headers */,
|
||||
F4CE987F1C8A921300D735C1 /* Resources */,
|
||||
OBJ_51 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SwiftyMarkdownPackageDescription;
|
||||
productName = SwiftyMarkdownPackageDescription;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
"SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = OBJ_36 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */;
|
||||
buildPhases = (
|
||||
OBJ_39 /* Sources */,
|
||||
OBJ_46 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -127,288 +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 = 0720;
|
||||
ORGANIZATIONNAME = "Voyage Travel Apps";
|
||||
TargetAttributes = {
|
||||
F4CE98801C8A921300D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
};
|
||||
F4CE988A1C8A921300D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
};
|
||||
};
|
||||
LastSwiftMigration = 9999;
|
||||
LastUpgradeCheck = 9999;
|
||||
};
|
||||
buildConfigurationList = F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
|
||||
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = F4CE98771C8A921300D735C1;
|
||||
productRefGroup = F4CE98821C8A921300D735C1 /* Products */;
|
||||
mainGroup = OBJ_5;
|
||||
productRefGroup = OBJ_23 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
F4CE98801C8A921300D735C1 /* SwiftyMarkdown */,
|
||||
F4CE988A1C8A921300D735C1 /* SwiftyMarkdownTests */,
|
||||
"SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */,
|
||||
"SwiftyMarkdown::SwiftPMPackageDescription" /* SwiftyMarkdownPackageDescription */,
|
||||
"SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget" /* SwiftyMarkdownPackageTests */,
|
||||
"SwiftyMarkdown::SwiftyMarkdownTests" /* SwiftyMarkdownTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
F4CE987F1C8A921300D735C1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */,
|
||||
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F4CE98891C8A921300D735C1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
F4CE987C1C8A921300D735C1 /* Sources */ = {
|
||||
OBJ_39 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
F4CE98EA1C8AF01300D735C1 /* README.md in Sources */,
|
||||
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */,
|
||||
OBJ_40 /* String+SwiftyMarkdown.swift in Sources */,
|
||||
OBJ_41 /* SwiftyLineProcessor.swift in Sources */,
|
||||
F4ACB6D223E8B08400EA665D /* PerfomanceLog.swift in Sources */,
|
||||
F4ACB6CB23E8A5C500EA665D /* SwiftyScanner.swift in Sources */,
|
||||
OBJ_42 /* SwiftyMarkdown+iOS.swift in Sources */,
|
||||
OBJ_43 /* SwiftyMarkdown+macOS.swift in Sources */,
|
||||
OBJ_44 /* SwiftyMarkdown.swift in Sources */,
|
||||
OBJ_45 /* SwiftyTokeniser.swift in Sources */,
|
||||
F4ACB6D023E8A8A500EA665D /* CharacterRule.swift in Sources */,
|
||||
F4ACB6CE23E8A88400EA665D /* Token.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F4CE98871C8A921300D735C1 /* Sources */ = {
|
||||
OBJ_51 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */,
|
||||
OBJ_52 /* Package.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
OBJ_62 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 0;
|
||||
files = (
|
||||
OBJ_63 /* SwiftyMarkdownAttributedStringTests.swift in Sources */,
|
||||
OBJ_64 /* SwiftyMarkdownCharacterTests.swift in Sources */,
|
||||
OBJ_65 /* SwiftyMarkdownLineTests.swift in Sources */,
|
||||
OBJ_66 /* SwiftyMarkdownLinkTests.swift in Sources */,
|
||||
OBJ_67 /* SwiftyMarkdownPerformanceTests.swift in Sources */,
|
||||
OBJ_68 /* XCTest+SwiftyMarkdown.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
F4CE988E1C8A921300D735C1 /* PBXTargetDependency */ = {
|
||||
OBJ_57 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = F4CE98801C8A921300D735C1 /* SwiftyMarkdown */;
|
||||
targetProxy = F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */;
|
||||
target = "SwiftyMarkdown::SwiftyMarkdownTests" /* SwiftyMarkdownTests */;
|
||||
targetProxy = F4ACB6CC23E8A5C600EA665D /* PBXContainerItemProxy */;
|
||||
};
|
||||
OBJ_71 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = "SwiftyMarkdown::SwiftyMarkdown" /* SwiftyMarkdown */;
|
||||
targetProxy = F4B37A0723E507C900833479 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
F4CE98931C8A921300D735C1 /* Debug */ = {
|
||||
OBJ_3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
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 = 6;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
"SWIFT_PACKAGE=1",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -DXcode";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
USE_HEADERMAP = NO;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F4CE98941C8A921300D735C1 /* Release */ = {
|
||||
OBJ_37 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = SwiftyMarkdown;
|
||||
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGET_NAME = SwiftyMarkdown;
|
||||
TVOS_DEPLOYMENT_TARGET = 11.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 4.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
OBJ_38 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = SwiftyMarkdown;
|
||||
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGET_NAME = SwiftyMarkdown;
|
||||
TVOS_DEPLOYMENT_TARGET = 11.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 4.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
OBJ_4 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
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 = 6;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
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";
|
||||
USE_HEADERMAP = NO;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
F4CE98961C8A921300D735C1 /* Debug */ = {
|
||||
OBJ_49 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 6;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
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 = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 6;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
LD = /usr/bin/true;
|
||||
OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.1";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
F4CE98991C8A921300D735C1 /* Debug */ = {
|
||||
OBJ_55 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F4CE989A1C8A921300D735C1 /* Release */ = {
|
||||
OBJ_56 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
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 */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,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>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>SwiftyMarkdownPerformanceTests</key>
|
||||
<dict>
|
||||
<key>testThatFilesAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.1</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.1</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.1</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>SwiftyMarkdownPerformanceTests</key>
|
||||
<dict>
|
||||
<key>testThatFilesAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.212</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
<key>maxPercentRelativeStandardDeviation</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.016</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.016</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>runDestinationsByUUID</key>
|
||||
<dict>
|
||||
<key>88991ED5-B954-422F-B610-BDC9A4AEC008</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>400</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>8-Core Intel Core i9</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>2400</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>16</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro16,1</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>8</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64h</string>
|
||||
</dict>
|
||||
<key>AD1DF83E-20BC-4E7E-8C14-683818ED0A26</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>400</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>8-Core Intel Core i9</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>2400</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>16</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro16,1</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>8</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "9999"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "SwiftyMarkdown::SwiftyMarkdown"
|
||||
BuildableName = "SwiftyMarkdown.framework"
|
||||
BlueprintName = "SwiftyMarkdown"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "SwiftyMarkdown::SwiftyMarkdownTests"
|
||||
BuildableName = "SwiftyMarkdownTests.xctest"
|
||||
BlueprintName = "SwiftyMarkdownTests"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<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"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown.h
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for SwiftyMarkdown.
|
||||
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
|
||||
|
||||
//! Project version string for SwiftyMarkdown.
|
||||
FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SwiftyMarkdown/PublicHeader.h>
|
||||
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
public protocol FontProperties {
|
||||
var fontName : String { get set }
|
||||
var color : UIColor { get set }
|
||||
}
|
||||
|
||||
|
||||
public struct BasicStyles : FontProperties {
|
||||
public var fontName = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
|
||||
public var color = UIColor.blackColor()
|
||||
}
|
||||
|
||||
enum LineType : Int {
|
||||
case H1, H2, H3, H4, H5, H6, Body, Italic, Bold, Code
|
||||
}
|
||||
|
||||
|
||||
public class SwiftyMarkdown {
|
||||
|
||||
public var h1 = BasicStyles()
|
||||
public var h2 = BasicStyles()
|
||||
public var h3 = BasicStyles()
|
||||
public var h4 = BasicStyles()
|
||||
public var h5 = BasicStyles()
|
||||
public var h6 = BasicStyles()
|
||||
|
||||
public var body = BasicStyles()
|
||||
public var link = BasicStyles()
|
||||
public var italic = BasicStyles()
|
||||
public var code = BasicStyles()
|
||||
public var bold = BasicStyles()
|
||||
|
||||
var previousStyle : String = UIFontTextStyleBody
|
||||
|
||||
let string : String
|
||||
let instructionSet = NSCharacterSet(charactersInString: "\\*_`")
|
||||
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
public init?(url : NSURL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
fatalError("Couldn't read string")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
|
||||
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
|
||||
|
||||
var lineCount = 0
|
||||
|
||||
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
|
||||
|
||||
|
||||
var skipLine = false
|
||||
for line in lines {
|
||||
lineCount++
|
||||
if skipLine {
|
||||
skipLine = false
|
||||
continue
|
||||
}
|
||||
var headingFound = false
|
||||
for heading in headings {
|
||||
|
||||
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
|
||||
|
||||
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
|
||||
let endHeadingHash = " " + heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
|
||||
let finalHeadingString = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingHash, withString: "")
|
||||
|
||||
// Make Hx where x == current index
|
||||
let string = attributedStringFromString(finalHeadingString, withType: LineType(rawValue: headings.indexOf(heading)!)!)
|
||||
attributedString.appendAttributedString(string)
|
||||
headingFound = true
|
||||
}
|
||||
}
|
||||
if headingFound {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if lineCount < lines.count {
|
||||
let nextLine = lines[lineCount]
|
||||
|
||||
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H1)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
|
||||
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
|
||||
|
||||
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H2)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if line.characters.count > 0 {
|
||||
|
||||
let scanner = NSScanner(string: line)
|
||||
|
||||
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
while !scanner.atEnd {
|
||||
var string : NSString?
|
||||
// Get all the characters up to the ones we are interested in
|
||||
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
|
||||
if let hasString = string as? String {
|
||||
let bodyString = attributedStringFromString(hasString, withType: .Body)
|
||||
attributedString.appendAttributedString(bodyString)
|
||||
|
||||
let location = scanner.scanLocation
|
||||
|
||||
let matchedCharacters = tagFromScanner(scanner)
|
||||
// If the next string after the characters is a space, then add it to the final string and continue
|
||||
if !scanner.scanUpToString(" ", intoString: nil) {
|
||||
let charAtts = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
attributedString.appendAttributedString(charAtts)
|
||||
} else {
|
||||
scanner.scanLocation = location
|
||||
|
||||
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
|
||||
}
|
||||
}
|
||||
}
|
||||
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func attributedStringFromScanner( scanner : NSScanner) -> NSAttributedString {
|
||||
var followingString : NSString?
|
||||
var matchedCharacters = self.tagFromScanner(scanner)
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
if let hasString = followingString as? String {
|
||||
let attString : NSAttributedString
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
attString = attributedStringFromString(matchedCharacters.stringByReplacingOccurrencesOfString("\\", withString: "") + hasString, withType: .Body)
|
||||
} else if matchedCharacters == "**" || matchedCharacters == "__" {
|
||||
attString = attributedStringFromString(hasString, withType: .Bold)
|
||||
} else if matchedCharacters == "`" {
|
||||
attString = attributedStringFromString("\t" + hasString, withType: .Code)
|
||||
} else {
|
||||
attString = attributedStringFromString(hasString, withType: .Italic)
|
||||
}
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
|
||||
let attString = attributedStringFromString(matchedCharacters.stringByReplacingOccurrencesOfString("\\", withString: ""), withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func tagFromScanner( scanner : NSScanner ) -> String {
|
||||
var matchedCharacters : String = ""
|
||||
var tempCharacters : NSString?
|
||||
|
||||
// Scan the ones we are interested in
|
||||
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
|
||||
if let chars = tempCharacters as? String {
|
||||
matchedCharacters = matchedCharacters + chars
|
||||
}
|
||||
}
|
||||
return matchedCharacters
|
||||
}
|
||||
|
||||
|
||||
// Make H1
|
||||
|
||||
func attributedStringFromString(string : String, withType type : LineType ) -> NSAttributedString {
|
||||
var attributes : [String : AnyObject]
|
||||
let textStyle : String
|
||||
let fontName : String
|
||||
|
||||
var appendNewLine = true
|
||||
|
||||
switch type {
|
||||
case .H1:
|
||||
fontName = h1.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle1
|
||||
} else {
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
}
|
||||
attributes = [NSForegroundColorAttributeName : h1.color]
|
||||
case .H2:
|
||||
fontName = h2.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
} else {
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
}
|
||||
attributes = [NSForegroundColorAttributeName : h2.color]
|
||||
case .H3:
|
||||
fontName = h3.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
} else {
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
}
|
||||
attributes = [NSForegroundColorAttributeName : h3.color]
|
||||
case .H4:
|
||||
fontName = h4.fontName
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
attributes = [NSForegroundColorAttributeName : h4.color]
|
||||
case .H5:
|
||||
fontName = h5.fontName
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
attributes = [NSForegroundColorAttributeName : h5.color]
|
||||
case .H6:
|
||||
fontName = h6.fontName
|
||||
textStyle = UIFontTextStyleFootnote
|
||||
attributes = [NSForegroundColorAttributeName : h6.color]
|
||||
case .Italic:
|
||||
fontName = italic.fontName
|
||||
attributes = [NSForegroundColorAttributeName : italic.color]
|
||||
textStyle = previousStyle
|
||||
appendNewLine = false
|
||||
case .Bold:
|
||||
fontName = bold.fontName
|
||||
attributes = [NSForegroundColorAttributeName : bold.color]
|
||||
appendNewLine = false
|
||||
textStyle = previousStyle
|
||||
case .Code:
|
||||
fontName = code.fontName
|
||||
attributes = [NSForegroundColorAttributeName : code.color]
|
||||
appendNewLine = false
|
||||
textStyle = previousStyle
|
||||
|
||||
default:
|
||||
appendNewLine = false
|
||||
fontName = body.fontName
|
||||
textStyle = UIFontTextStyleBody
|
||||
attributes = [NSForegroundColorAttributeName:body.color]
|
||||
break
|
||||
}
|
||||
previousStyle = textStyle
|
||||
|
||||
let font = UIFont.preferredFontForTextStyle(textStyle)
|
||||
let styleDescriptor = font.fontDescriptor()
|
||||
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
|
||||
|
||||
var finalFont : UIFont
|
||||
if let font = UIFont(name: fontName, size: styleSize) {
|
||||
finalFont = font
|
||||
} else {
|
||||
finalFont = UIFont.preferredFontForTextStyle(textStyle)
|
||||
}
|
||||
|
||||
let finalFontDescriptor = finalFont.fontDescriptor()
|
||||
if type == .Italic {
|
||||
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
|
||||
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
|
||||
}
|
||||
if type == .Bold {
|
||||
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
|
||||
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
|
||||
}
|
||||
|
||||
|
||||
attributes[NSFontAttributeName] = finalFont
|
||||
|
||||
if appendNewLine {
|
||||
return NSAttributedString(string: string + "\n", attributes: attributes)
|
||||
} else {
|
||||
return NSAttributedString(string: string, attributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftyMarkdownExample" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qZP-CU-74n">
|
||||
<rect key="frame" x="20" y="20" width="560" height="580"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<attributedString key="attributedText"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<dataDetectorType key="dataDetectorTypes" link="YES"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="1yU-8N-26a"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" id="7lJ-OQ-4eh"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="qZP-CU-74n" secondAttribute="trailing" id="F5p-iG-zTB"/>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="srf-u0-j0n"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="textView" destination="qZP-CU-74n" id="VO1-kx-Lpd"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// SwiftyMarkdownExample
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftyMarkdown
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var textView : UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
if let url = NSBundle.mainBundle().URLForResource("example", withExtension: "md"), md = SwiftyMarkdown(url: url) {
|
||||
md.h2.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h2.color = UIColor.redColor()
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
self.textView.attributedText = md.attributedString()
|
||||
|
||||
} else {
|
||||
fatalError("Error loading file")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Swifty Markdown
|
||||
|
||||
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even if you use custom fonts.
|
||||
|
||||
## Features
|
||||
|
||||
Customise fonts and colours easily in a Swift like way:
|
||||
|
||||
`md.code.fontName = "CourierNewPSMT"`
|
||||
|
||||
`md.h2.fontName = "AvenirNextCondensed-Medium"`
|
||||
`md.h2.color = UIColor.redColor()`
|
||||
|
||||
*An italic line*
|
||||
|
||||
It ignores random * and correctly handles \*escaped\* asterisks and \_underlines\_.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdownTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import XCTest
|
||||
|
||||
import AppLibrarianTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += AppLibrarianTests.allTests()
|
||||
XCTMain(tests)
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// SwiftyMarkdownAttributedStringTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownAttributedStringTests: XCTestCase {
|
||||
|
||||
func testThatAttributesAreAppliedCorrectly() {
|
||||
|
||||
let string = """
|
||||
# Heading 1
|
||||
|
||||
A more *complicated* example. This one has **it all**. Here is a [link](http://voyagetravelapps.com/).
|
||||
|
||||
## Heading 2
|
||||
|
||||
## Heading 3
|
||||
|
||||
> This one is a blockquote
|
||||
"""
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
let attributedString = md.attributedString()
|
||||
|
||||
XCTAssertNotNil(attributedString)
|
||||
|
||||
XCTAssertEqual(attributedString.string, "Heading 1\n\nA more complicated example. This one has it all. Here is a link.\n\nHeading 2\n\nHeading 3\n\nThis one is a blockquote")
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
//
|
||||
// SwiftyMarkdownCharacterTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import SwiftyMarkdown
|
||||
import XCTest
|
||||
|
||||
class SwiftyMarkdownStylingTests: SwiftyMarkdownCharacterTests {
|
||||
|
||||
func off_testIsolatedCase() {
|
||||
|
||||
challenge = TokenTest(input: "*\\***\\****b*\\***\\****\\", output: "***b***\\", tokens : [
|
||||
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "*b**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "\\", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
return
|
||||
|
||||
challenge = TokenTest(input: """
|
||||
An asterisk: *
|
||||
Line break
|
||||
""", output: """
|
||||
An asterisk: *
|
||||
Line break
|
||||
""", tokens: [
|
||||
Token(type: .string, inputString: "An asterisk: *", characterStyles: []),
|
||||
Token(type: .string, inputString: "Line break", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
return
|
||||
|
||||
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
if results.links.count == 1 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
|
||||
|
||||
|
||||
challenge = TokenTest(input: "A [referenced link][link]\n[notLink]: https://www.neverendingvoyage.com/", output: "A [referenced link][link]", tokens: [
|
||||
Token(type: .string, inputString: "A [referenced link][link]", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge, rules: [.links, .images, .referencedLinks])
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
XCTAssertEqual(results.links.count, 0)
|
||||
|
||||
}
|
||||
|
||||
func testThatBoldTraitsAreRecognised() {
|
||||
challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
|
||||
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
|
||||
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\\\*\\*A normal \\\\ string\\*\\*", output: "\\**A normal \\\\ string**", tokens: [
|
||||
Token(type: .string, inputString: "\\**A normal \\\\ string**", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]),
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
|
||||
Token(type: .string, inputString: "A string with one *", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatCodeTraitsAreRecognised() {
|
||||
challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [
|
||||
Token(type: .string, inputString: "Code (**should** not process internal tags)", characterStyles: [CharacterStyle.code])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with `code` (should not be indented)", output: "A string with code (should not be indented)", tokens : [
|
||||
Token(type: .string, inputString: "A string with ", characterStyles: []),
|
||||
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " (should not be indented)", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "`A code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [
|
||||
Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " with multiple ", characterStyles: []),
|
||||
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "instances", characterStyles: [CharacterStyle.code])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [
|
||||
Token(type: .string, inputString: "`A normal string`", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [
|
||||
Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A lonely backtick: `", output: "A lonely backtick: `", tokens: [
|
||||
Token(type: .string, inputString: "A lonely backtick: `", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "Two backticks followed by a full stop ``.", output: "Two backticks followed by a full stop ``.", tokens: [
|
||||
Token(type: .string, inputString: "Two backticks followed by a full stop ``.", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatItalicTraitsAreParsedCorrectly() {
|
||||
|
||||
challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
|
||||
Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [
|
||||
Token(type: .string, inputString: "A string with ", characterStyles: []),
|
||||
Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " text", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
|
||||
challenge = TokenTest(input: "_An italic string_ with a *mix* _of_ italic *styles*", output: "An italic string with a mix of italic styles", tokens : [
|
||||
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " italic ", characterStyles: []),
|
||||
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
|
||||
challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [
|
||||
Token(type: .string, inputString: "_A normal string_", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [
|
||||
Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: """
|
||||
An asterisk: *
|
||||
Line break
|
||||
""", output: """
|
||||
An asterisk: *
|
||||
Line break
|
||||
""", tokens: [
|
||||
Token(type: .string, inputString: "An asterisk: *", characterStyles: []),
|
||||
Token(type: .string, inputString: "Line break", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count )
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
func testThatStrikethroughTraitsAreRecognised() {
|
||||
challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [
|
||||
Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]),
|
||||
Token(type: .string, inputString: "A crossed-out string", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
|
||||
challenge = TokenTest(input: "A **Bold** string and a ~~removed~~crossed-out string", output: "A Bold string and a removedcrossed-out string", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " string and a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "removed", characterStyles: [CharacterStyle.strikethrough]),
|
||||
Token(type: .string, inputString: "crossed-out string", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
|
||||
challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [
|
||||
Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
|
||||
}
|
||||
|
||||
func testThatMixedTraitsAreRecognised() {
|
||||
|
||||
challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [
|
||||
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " bold ", characterStyles: []),
|
||||
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "_An italic string_, **followed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, followed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
|
||||
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: ", ", characterStyles: []),
|
||||
Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: ", ", characterStyles: []),
|
||||
Token(type: .string, inputString: "with some code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: ", **and some** _escaped_ `characters`, ", characterStyles: []),
|
||||
Token(type: .string, inputString: "ending", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "with", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "more", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " variety.", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testForExtremeEscapeCombinations() {
|
||||
|
||||
challenge = TokenTest(input: "\\****b\\****", output: "*b*", tokens : [
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: "b*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "**\\**b*\\***", output: "*b*", tokens : [
|
||||
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "b", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.bold]),
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
// challenge = TokenTest(input: "Before *\\***\\****A bold string*\\***\\****\\ After", output: "Before ***A bold string***\\ After", tokens : [
|
||||
// Token(type: .string, inputString: "Before ", characterStyles: []),
|
||||
// Token(type: .string, inputString: "*", characterStyles: [CharacterStyle.italic]),
|
||||
// Token(type: .string, inputString: "**", characterStyles: [CharacterStyle.bold]),
|
||||
// Token(type: .string, inputString: "A bold string**", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
// Token(type: .string, inputString: "\\ After", characterStyles: [])
|
||||
// ])
|
||||
// results = self.attempt(challenge)
|
||||
// if results.stringTokens.count == challenge.tokens.count {
|
||||
// for (idx, token) in results.stringTokens.enumerated() {
|
||||
// XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
// XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
// }
|
||||
// } else {
|
||||
// XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
// }
|
||||
// XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
// XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatExtraCharactersAreHandles() {
|
||||
challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
|
||||
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ****bold**** word", output: "A string with a bold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ****bold italic*** word", output: "A string with a *bold italic word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*bold italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic, CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with ```code`", output: "A string with ```code`", tokens : [
|
||||
Token(type: .string, inputString: "A string with ```code`", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with ```code```", output: "A string with code", tokens : [
|
||||
Token(type: .string, inputString: "A string with ", characterStyles: []),
|
||||
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// The new version of SwiftyMarkdown is a lot more strict than the old version, although this may change in future
|
||||
func offtestThatMarkdownMistakesAreHandledAppropriately() {
|
||||
let mismatchedBoldCharactersAtStart = "**This should be bold*"
|
||||
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
|
||||
|
||||
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "This should be bold")
|
||||
|
||||
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string that should be italic")
|
||||
|
||||
}
|
||||
|
||||
func offtestAdvancedEscaping() {
|
||||
|
||||
challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [
|
||||
Token(type: .string, inputString: "**", characterStyles: []),
|
||||
Token(type: .string, inputString: "A normal string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "**", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "** asterisks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
|
||||
|
||||
let asteriskFullStop = "Two asterisks followed by a full stop: **."
|
||||
let asteriskWithBold = "A **bold** word followed by an asterisk * "
|
||||
let underscoreFullStop = "Two underscores followed by a full stop: __."
|
||||
let asteriskComma = "An asterisk followed by a full stop: *, *"
|
||||
|
||||
let backtickSpace = "A backtick followed by a space: `"
|
||||
|
||||
let underscoreSpace = "An underscore followed by a space: _"
|
||||
|
||||
let backtickComma = "A backtick followed by a space: `, `"
|
||||
let underscoreComma = "An underscore followed by a space: _, _"
|
||||
|
||||
let backtickWithCode = "A `code` word followed by a backtick ` "
|
||||
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
|
||||
|
||||
var md = SwiftyMarkdown(string: backtickSpace)
|
||||
SwiftyMarkdown.characterRules = self.defaultRules
|
||||
XCTAssertEqual(md.attributedString().string, backtickSpace)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreSpace)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreSpace)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskFullStop)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreFullStop)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskComma)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskComma)
|
||||
|
||||
md = SwiftyMarkdown(string: backtickComma)
|
||||
XCTAssertEqual(md.attributedString().string, backtickComma)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreComma)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreComma)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskWithBold)
|
||||
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk *")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickWithCode)
|
||||
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick `")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreWithItalic)
|
||||
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _")
|
||||
|
||||
}
|
||||
|
||||
func testReportedCrashingStrings() {
|
||||
challenge = TokenTest(input: "[**\\!bang**](https://duckduckgo.com/bang)", output: "\\!bang", tokens: [
|
||||
Token(type: .string, inputString: "\\!bang", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// SwiftyMarkdownTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
struct StringTest {
|
||||
let input : String
|
||||
let expectedOutput : String
|
||||
var acutalOutput : String = ""
|
||||
}
|
||||
|
||||
struct TokenTest {
|
||||
let input : String
|
||||
let output : String
|
||||
let tokens : [Token]
|
||||
}
|
||||
|
||||
class SwiftyMarkdownTests: XCTestCase {
|
||||
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testThatOctothorpeHeadersAreHandledCorrectly() {
|
||||
|
||||
let heading1 = StringTest(input: "# Heading 1", expectedOutput: "Heading 1")
|
||||
var smd = SwiftyMarkdown(string:heading1.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading1.expectedOutput)
|
||||
|
||||
let heading2 = StringTest(input: "## Heading 2", expectedOutput: "Heading 2")
|
||||
smd = SwiftyMarkdown(string:heading2.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading2.expectedOutput)
|
||||
|
||||
let heading3 = StringTest(input: "### #Heading #3", expectedOutput: "#Heading #3")
|
||||
smd = SwiftyMarkdown(string:heading3.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading3.expectedOutput)
|
||||
|
||||
let heading4 = StringTest(input: " #### #Heading 4 ####", expectedOutput: "#Heading 4")
|
||||
smd = SwiftyMarkdown(string:heading4.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading4.expectedOutput)
|
||||
|
||||
let heading5 = StringTest(input: " ##### Heading 5 #### ", expectedOutput: "Heading 5 ####")
|
||||
smd = SwiftyMarkdown(string:heading5.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading5.expectedOutput)
|
||||
|
||||
let heading6 = StringTest(input: " ##### Heading 5 #### More ", expectedOutput: "Heading 5 #### More")
|
||||
smd = SwiftyMarkdown(string:heading6.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading6.expectedOutput)
|
||||
|
||||
let heading7 = StringTest(input: "# **Bold Header 1** ", expectedOutput: "Bold Header 1")
|
||||
smd = SwiftyMarkdown(string:heading7.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading7.expectedOutput)
|
||||
|
||||
let heading8 = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
|
||||
smd = SwiftyMarkdown(string:heading8.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading8.expectedOutput)
|
||||
|
||||
let heading9 = StringTest(input: " # Heading 1", expectedOutput: "# Heading 1")
|
||||
smd = SwiftyMarkdown(string:heading9.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading9.expectedOutput)
|
||||
|
||||
let allHeaders = [heading1, heading2, heading3, heading4, heading5, heading6, heading7, heading8, heading9]
|
||||
smd = SwiftyMarkdown(string: allHeaders.map({ $0.input }).joined(separator: "\n"))
|
||||
XCTAssertEqual(smd.attributedString().string, allHeaders.map({ $0.expectedOutput}).joined(separator: "\n"))
|
||||
|
||||
let headerString = StringTest(input: "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6", expectedOutput: "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6")
|
||||
smd = SwiftyMarkdown(string: headerString.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerString.expectedOutput)
|
||||
|
||||
let headerStringWithBold = StringTest(input: "# **Bold Header 1**", expectedOutput: "Bold Header 1")
|
||||
smd = SwiftyMarkdown(string: headerStringWithBold.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerStringWithBold.expectedOutput )
|
||||
|
||||
let headerStringWithItalic = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
|
||||
smd = SwiftyMarkdown(string: headerStringWithItalic.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerStringWithItalic.expectedOutput)
|
||||
|
||||
}
|
||||
|
||||
|
||||
func testThatUndelinedHeadersAreHandledCorrectly() {
|
||||
|
||||
let h1String = StringTest(input: "Header 1\n===\nSome following text", expectedOutput: "Header 1\nSome following text")
|
||||
var md = SwiftyMarkdown(string: h1String.input)
|
||||
XCTAssertEqual(md.attributedString().string, h1String.expectedOutput)
|
||||
|
||||
let h2String = StringTest(input: "Header 2\n---\nSome following text", expectedOutput: "Header 2\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2String.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2String.expectedOutput)
|
||||
|
||||
let h1StringWithBold = StringTest(input: "Header 1 **With Bold**\n===\nSome following text", expectedOutput: "Header 1 With Bold\nSome following text")
|
||||
md = SwiftyMarkdown(string: h1StringWithBold.input)
|
||||
XCTAssertEqual(md.attributedString().string, h1StringWithBold.expectedOutput)
|
||||
|
||||
let h2StringWithItalic = StringTest(input: "Header 2 _With Italic_\n---\nSome following text", expectedOutput: "Header 2 With Italic\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2StringWithItalic.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2StringWithItalic.expectedOutput)
|
||||
|
||||
let h2StringWithCode = StringTest(input: "Header 2 `With Code`\n---\nSome following text", expectedOutput: "Header 2 With Code\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2StringWithCode.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2StringWithCode.expectedOutput)
|
||||
}
|
||||
|
||||
func testThatUnorderedListsAreHandledCorrectly() {
|
||||
let dashBullets = StringTest(input: "An Unordered List\n- Item 1\n\t- Indented\n- Item 2", expectedOutput: "An Unordered List\n-\tItem 1\n\t-\tIndented\n-\tItem 2")
|
||||
var md = SwiftyMarkdown(string: dashBullets.input)
|
||||
md.bullet = "-"
|
||||
XCTAssertEqual(md.attributedString().string, dashBullets.expectedOutput)
|
||||
|
||||
let starBullets = StringTest(input: "An Unordered List\n* Item 1\n\t* Indented\n* Item 2", expectedOutput: "An Unordered List\n-\tItem 1\n\t-\tIndented\n-\tItem 2")
|
||||
md = SwiftyMarkdown(string: starBullets.input)
|
||||
md.bullet = "-"
|
||||
XCTAssertEqual(md.attributedString().string, starBullets.expectedOutput)
|
||||
|
||||
}
|
||||
|
||||
func testThatOrderedListsAreHandled() {
|
||||
let dashBullets = StringTest(input: "An Ordered List\n1. Item 1\n\t1. Indented\n1. Item 2", expectedOutput: "An Ordered List\n1.\tItem 1\n\t1.\tIndented\n2.\tItem 2")
|
||||
var md = SwiftyMarkdown(string: dashBullets.input)
|
||||
XCTAssertEqual(md.attributedString().string, dashBullets.expectedOutput)
|
||||
|
||||
let moreComplicatedList = StringTest(input: """
|
||||
A long ordered list:
|
||||
|
||||
1. Item 1
|
||||
1. Item 2
|
||||
1. First Indent 1
|
||||
1. First Indent 2
|
||||
1. Second Indent 1
|
||||
1. First Indent 3
|
||||
1. Second Indent 2
|
||||
1. Item 3
|
||||
|
||||
A break
|
||||
|
||||
1. Item 1
|
||||
1. Item 2
|
||||
""", expectedOutput: """
|
||||
A long ordered list:
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
1. First Indent 1
|
||||
2. First Indent 2
|
||||
1. Second Indent 1
|
||||
3. First Indent 3
|
||||
1. Second Indent 2
|
||||
3. Item 3
|
||||
|
||||
A break
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
""")
|
||||
md = SwiftyMarkdown(string: moreComplicatedList.input)
|
||||
XCTAssertEqual(md.attributedString().string, moreComplicatedList.expectedOutput)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
The reason for this test is because the list of items dropped every other item in bullet lists marked with "-"
|
||||
The faulty result was: "A cool title\n \n- Här har vi svenska ÅÄÖåäö tecken\n \nA Link"
|
||||
As you can see, "- Point number one" and "- Point number two" are mysteriously missing.
|
||||
It incorrectly identified rows as `Alt-H2`
|
||||
*/
|
||||
func offtestInternationalCharactersInList() {
|
||||
|
||||
let expected = "A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n \nA Link"
|
||||
let input = "# A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n\n[A Link](http://dooer.com)"
|
||||
let output = SwiftyMarkdown(string: input).attributedString().string
|
||||
|
||||
XCTAssertEqual(output, expected)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
func testThatYAMLMetadataIsRemoved() {
|
||||
let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!\n\nWith A Heading\n---", expectedOutput: "Finally some Markdown!\n\nWith A Heading")
|
||||
let md = SwiftyMarkdown(string: yaml.input)
|
||||
XCTAssertEqual(md.attributedString().string, yaml.expectedOutput)
|
||||
XCTAssertEqual(md.frontMatterAttributes.count, 8)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
//
|
||||
// SwiftyMarkdownCharacterTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import SwiftyMarkdown
|
||||
import XCTest
|
||||
|
||||
class SwiftyMarkdownLinkTests: SwiftyMarkdownCharacterTests {
|
||||
|
||||
func testSingleLinkPositions() {
|
||||
challenge = TokenTest(input: "[a](b)", output: "a", tokens: [
|
||||
Token(type: .string, inputString: "a", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge, rules: [.links])
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
|
||||
XCTAssertEqual(existentOpen.metadataStrings.first, "b")
|
||||
} else {
|
||||
XCTFail("Failed to find an open link tag")
|
||||
}
|
||||
|
||||
challenge = TokenTest(input: "[Link at](http://voyagetravelapps.com/) start", output: "Link at start", tokens: [
|
||||
Token(type: .string, inputString: "Link at", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " start")
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
|
||||
XCTAssertEqual(existentOpen.metadataStrings.first, "http://voyagetravelapps.com/")
|
||||
} else {
|
||||
XCTFail("Failed to find an open link tag")
|
||||
}
|
||||
|
||||
challenge = TokenTest(input: "A [link at end](http://voyagetravelapps.com/)", output: "A link at end", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "link at end", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A [link in the](http://voyagetravelapps.com/) middle", output: "A link in the middle", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "link in the", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " middle", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testEscapedLinks() {
|
||||
challenge = TokenTest(input: "\\[a](b)", output: "[a](b)", tokens: [
|
||||
Token(type: .string, inputString: "[a](b)", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge, rules: [.images, .referencedLinks, .links])
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "", 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 ", 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 [](https://www.neverendingvoyage.com/)", output: "An ", tokens: [
|
||||
Token(type: .string, inputString: "An ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Image", characterStyles: [CharacterStyle.image, CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
if results.images.count == 1 {
|
||||
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.images.count)")
|
||||
}
|
||||
if results.links.count == 1 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.last, "https://www.neverendingvoyage.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
}
|
||||
|
||||
func testForReferencedImages() {
|
||||
challenge = TokenTest(input: "A ![referenced image][image]\n[image]: imageName", output: "A ", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced image", characterStyles: [CharacterStyle.image])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
if results.images.count == 1 {
|
||||
XCTAssertEqual(results.images[0].metadataStrings.first, "imageName")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
}
|
||||
|
||||
func testForReferencedLinks() {
|
||||
challenge = TokenTest(input: "A [referenced link][link]\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
if results.links.count == 1 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
|
||||
challenge = TokenTest(input: "A [referenced link][link]\n [link]: https://www.neverendingvoyage.com/", output: "A referenced link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
if results.links.count == 1 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
|
||||
challenge = TokenTest(input: "An *\\*italic\\** [referenced link][a]\n[a]: link", output: "An *italic* referenced link", tokens: [
|
||||
Token(type: .string, inputString: "An ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*italic*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge, rules: [.asterisks, .links, .referencedLinks])
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
if results.links.count == 1 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.first, "link")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testForMixedLinkStyles() {
|
||||
challenge = TokenTest(input: "A [referenced link][link] and a [regular link](http://voyagetravelapps.com/)\n[link]: https://www.neverendingvoyage.com/", output: "A referenced link and a regular link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "referenced link", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " and a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "regular link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
if results.stringTokens.count == challenge.tokens.count {
|
||||
for (idx, token) in results.stringTokens.enumerated() {
|
||||
XCTAssertEqual(token.inputString, challenge.tokens[idx].inputString)
|
||||
XCTAssertEqual(token.characterStyles as? [CharacterStyle], challenge.tokens[idx].characterStyles as? [CharacterStyle])
|
||||
}
|
||||
} else {
|
||||
XCTAssertEqual(results.stringTokens.count, challenge.tokens.count)
|
||||
}
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
if results.links.count == 2 {
|
||||
XCTAssertEqual(results.links[0].metadataStrings.first, "https://www.neverendingvoyage.com/")
|
||||
XCTAssertEqual(results.links[1].metadataStrings.first, "http://voyagetravelapps.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(results.links.count)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// SwiftyMarkdownAttributedStringTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownPerformanceTests: XCTestCase {
|
||||
|
||||
func testThatFilesAreProcessedQuickly() {
|
||||
|
||||
let url = self.resourceURL(for: "test.md")
|
||||
|
||||
measure {
|
||||
guard let md = SwiftyMarkdown(url: url) else {
|
||||
XCTFail("Failed to load file")
|
||||
return
|
||||
}
|
||||
_ = md.attributedString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testThatStringsAreProcessedQuickly() {
|
||||
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
measure {
|
||||
_ = md.attributedString(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func testThatVeryLongStringsAreProcessedQuickly() {
|
||||
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
measure {
|
||||
_ = md.attributedString(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// XCTest+SwiftyMarkdown.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
|
||||
struct ChallengeReturn {
|
||||
let tokens : [Token]
|
||||
let stringTokens : [Token]
|
||||
let links : [Token]
|
||||
let images : [Token]
|
||||
let attributedString : NSAttributedString
|
||||
let foundStyles : [[CharacterStyle]]
|
||||
let expectedStyles : [[CharacterStyle]]
|
||||
}
|
||||
|
||||
enum Rule {
|
||||
case asterisks
|
||||
case backticks
|
||||
case underscores
|
||||
case images
|
||||
case links
|
||||
case referencedLinks
|
||||
case referencedImages
|
||||
case tildes
|
||||
|
||||
func asCharacterRule() -> CharacterRule {
|
||||
switch self {
|
||||
case .images:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && !$0.metadataLookup }).first!
|
||||
case .links:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && !$0.metadataLookup }).first!
|
||||
case .backticks:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "`" }).first!
|
||||
case .tildes:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "~" }).first!
|
||||
case .asterisks:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "*" }).first!
|
||||
case .underscores:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "_" }).first!
|
||||
case .referencedLinks:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "[" && $0.metadataLookup }).first!
|
||||
case .referencedImages:
|
||||
return SwiftyMarkdown.characterRules.filter({ $0.primaryTag.tag == "![" && $0.metadataLookup }).first!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SwiftyMarkdownCharacterTests : XCTestCase {
|
||||
let defaultRules = SwiftyMarkdown.characterRules
|
||||
|
||||
var challenge : TokenTest!
|
||||
var results : ChallengeReturn!
|
||||
|
||||
func attempt( _ challenge : TokenTest, rules : [Rule]? = nil ) -> ChallengeReturn {
|
||||
if let validRules = rules {
|
||||
SwiftyMarkdown.characterRules = validRules.map({ $0.asCharacterRule() })
|
||||
} else {
|
||||
SwiftyMarkdown.characterRules = self.defaultRules
|
||||
}
|
||||
|
||||
let md = SwiftyMarkdown(string: challenge.input)
|
||||
md.applyAttachments = false
|
||||
let attributedString = md.attributedString()
|
||||
let tokens : [Token] = md.previouslyFoundTokens
|
||||
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
|
||||
|
||||
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
|
||||
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
|
||||
|
||||
let linkTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
|
||||
let imageTokens = tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
|
||||
|
||||
return ChallengeReturn(tokens: tokens, stringTokens: stringTokens, links : linkTokens, images: imageTokens, attributedString: attributedString, foundStyles: existentTokenStyles, expectedStyles : expectedStyles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension XCTestCase {
|
||||
|
||||
func resourceURL(for filename : String ) -> URL {
|
||||
let thisSourceFile = URL(fileURLWithPath: #file)
|
||||
let thisDirectory = thisSourceFile.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
return thisDirectory.appendingPathComponent("Resources", isDirectory: true).appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
+4
-69
@@ -16,73 +16,8 @@
|
||||
|
||||
# This is the minimum version number required.
|
||||
# Update this, if you use features of a newer version
|
||||
fastlane_version "1.36.1"
|
||||
$scheme = "SwiftyMarkdown"
|
||||
$spec = "SwiftyMarkdown.podspec"
|
||||
$project = './SwiftyMarkdown.xcodeproj'
|
||||
|
||||
default_platform :ios
|
||||
|
||||
|
||||
|
||||
platform :ios do
|
||||
|
||||
spec = "SwiftyMarkdown.podspec"
|
||||
|
||||
before_all do
|
||||
ensure_git_branch
|
||||
ensure_git_status_clean
|
||||
increment_build_number
|
||||
commit_version_bump(xcodeproj: './SwiftyMarkdown.xcodeproj')
|
||||
end
|
||||
|
||||
desc "Bumps the patch version, increments it, and pushes it to both the remote and spec repos"
|
||||
lane :patch do
|
||||
# snapshot
|
||||
update(type: "patch")
|
||||
end
|
||||
|
||||
desc "Bumps the minor version, increments it, and pushes it to both the remote and spec repos"
|
||||
lane :minor do
|
||||
# snapshot
|
||||
update(type: "minor")
|
||||
|
||||
end
|
||||
|
||||
lane :initial do
|
||||
version = version_get_podspec
|
||||
bump(version: version)
|
||||
end
|
||||
|
||||
private_lane :update do |options|
|
||||
|
||||
type = options[:type]
|
||||
version = version_bump_podspec(path: spec, bump_type: type)
|
||||
increment_version_number( bump_type: type, version_number: version )
|
||||
git_commit(path: spec, message: "Updating podspec")
|
||||
|
||||
bump(version: version)
|
||||
|
||||
end
|
||||
|
||||
private_lane :bump do |options|
|
||||
version = options[:version]
|
||||
add_git_tag( tag: version)
|
||||
push_to_git_remote
|
||||
pod_push(path: spec)
|
||||
|
||||
end
|
||||
|
||||
# You can define as many lanes as you want
|
||||
|
||||
after_all do |lane|
|
||||
# This block is called, only if the executed lane was successful
|
||||
notification(subtitle: "SwiftyMarkdown fastlane finished", message: "Completed '#{lane}' successfully")
|
||||
end
|
||||
|
||||
error do |lane, exception|
|
||||
notification(subtitle: "Error: SwiftyMarkdown fastlane", message: "'#{exception.message}'")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# More information about multiple platforms in fastlane: https://github.com/KrauseFx/fastlane/blob/master/docs/Platforms.md
|
||||
# All available actions: https://github.com/KrauseFx/fastlane/blob/master/docs/Actions.md
|
||||
import "/Users/simon/Developer/Fastlane/FastfilePods"
|
||||
+81
-22
@@ -1,29 +1,88 @@
|
||||
fastlane documentation
|
||||
================
|
||||
----
|
||||
|
||||
# Installation
|
||||
```
|
||||
sudo gem install fastlane
|
||||
```
|
||||
# Available Actions
|
||||
## iOS
|
||||
### ios patch
|
||||
```
|
||||
fastlane ios patch
|
||||
```
|
||||
Bumps the patch version, increments it, and pushes it to both the remote and spec repos
|
||||
### ios minor
|
||||
```
|
||||
fastlane ios minor
|
||||
```
|
||||
Bumps the minor version, increments it, and pushes it to both the remote and spec repos
|
||||
### ios initial
|
||||
```
|
||||
fastlane ios initial
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## iOS
|
||||
|
||||
### ios patch
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios patch
|
||||
```
|
||||
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the patch version
|
||||
|
||||
### ios minor
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios minor
|
||||
```
|
||||
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the minor version
|
||||
|
||||
### ios major
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios major
|
||||
```
|
||||
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the major version
|
||||
|
||||
### ios test
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios test
|
||||
```
|
||||
|
||||
|
||||
|
||||
### ios submit_pod
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios submit_pod
|
||||
```
|
||||
|
||||
Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to run tests, bump versions, and commit changes.
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time to run [fastlane](https://fastlane.tools).
|
||||
More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools).
|
||||
The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane).
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
|
||||
Reference in New Issue
Block a user