From e467e992e209cf4fd81168af43f05b9e64112b0c Mon Sep 17 00:00:00 2001 From: Simon Fairbairn Date: Sat, 1 Feb 2020 11:29:32 +1300 Subject: [PATCH] Adds ordered lists --- .../AppDelegate.swift | 26 + .../AppIcon.appiconset/Contents.json | 58 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/Main.storyboard | 717 ++++++++++ .../SwiftyMarkdownExample macOS/Info.plist | 36 + .../SwiftyMarkdownExample_macOS.entitlements | 10 + .../ViewController.swift | 28 + .../project.pbxproj | 791 +++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 16 + .../SwiftyMarkdownExample/AppDelegate.swift | 46 + .../AppIcon.appiconset/Contents.json | 98 ++ .../Assets.xcassets/Contents.json | 6 + .../bubble.imageset/Contents.json | 21 + .../bubble.imageset/bubble.png | Bin 0 -> 1675 bytes .../Base.lproj/LaunchScreen.storyboard | 27 + .../Base.lproj/Main.storyboard | 80 ++ Example/SwiftyMarkdownExample/Info.plist | 47 + .../ViewController.swift | 63 + Example/SwiftyMarkdownExample/example.md | 50 + Example/SwiftyMarkdownExampleTests/Info.plist | 24 + .../SwiftyMarkdownExampleTests.swift | 36 + .../SwiftyMarkdownExampleUITests/Info.plist | 24 + .../SwiftyMarkdownExampleUITests.swift | 36 + Package.swift | 5 +- .../Contents.swift | 428 ++++++ .../Resources/bubble.png | Bin 0 -> 1675 bytes .../Contents.swift | 55 + .../Contents.swift | 33 + .../Contents.swift | 7 + .../Contents.swift | 680 +++++++++ .../Resources/test.md | 24 + .../Sources/String+SwiftyMarkdown.swift | 36 + .../Sources/Swifty Line Processor.swift | 157 +++ .../Sources/Swifty Tokeniser.swift | 496 +++++++ .../Sources/SwiftyMarkdown.swift | 519 +++++++ .../contents.xcplayground | 9 + README.md | 6 + Resources/metadataTest.md | 14 + Resources/test.md | 24 + .../String+SwiftyMarkdown.swift | 44 + .../SwiftyMarkdown/SwiftyLineProcessor.swift | 229 ++++ .../SwiftyMarkdown/SwiftyMarkdown+iOS.swift | 173 +++ .../SwiftyMarkdown/SwiftyMarkdown+macOS.swift | 144 ++ Sources/SwiftyMarkdown/SwiftyMarkdown.swift | 545 ++++++++ Sources/SwiftyMarkdown/SwiftyTokeniser.swift | 821 +++++++++++ .../SwiftyMarkdownTests_Info.plist | 25 + .../SwiftyMarkdown_Info.plist | 25 + SwiftyMarkdown.xcodeproj/project.pbxproj | 1214 ++++++++++------- .../contents.xcworkspacedata | 2 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + ...3EC61D89-CC18-40D5-9AC6-F4504ECE42B5.plist | 32 - .../Info.plist | 71 - ...D1DF83E-20BC-4E7E-8C14-683818ED0A26.plist} | 10 +- .../Info.plist | 33 + ...scheme => SwiftyMarkdown-Package.xcscheme} | 33 +- SwiftyMarkdown/SwiftyMarkdown+iOS.swift | 2 +- SwiftyMarkdown/SwiftyMarkdown+macOS.swift | 2 +- SwiftyMarkdown/SwiftyMarkdown.swift | 51 +- SwiftyMarkdown/SwiftyTokeniser.swift | 13 +- .../AppDelegate.swift | 26 + .../AppIcon.appiconset/Contents.json | 58 + .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/Main.storyboard | 717 ++++++++++ .../SwiftyMarkdownExample macOS/Info.plist | 36 + .../SwiftyMarkdownExample_macOS.entitlements | 10 + .../ViewController.swift | 28 + .../project.pbxproj | 172 ++- .../SwiftyMarkdownCharacterTests.swift | 518 +++---- .../SwiftyMarkdownLineTests.swift | 12 + .../SwiftyMarkdownLinkTests.swift | 180 +++ Tests/LinuxMain.swift | 7 + ...tyMarkdownAttributedStringTests copy.swift | 37 + .../SwiftyMarkdownCharacterTests.swift | 446 ++++++ .../SwiftyMarkdownLineTests.swift | 206 +++ .../SwiftyMarkdownLinkTests.swift | 179 +++ .../SwiftyMarkdownPerformanceTests.swift | 44 + .../XCTest+SwiftyMarkdown.swift | 31 + 78 files changed, 9956 insertions(+), 981 deletions(-) create mode 100644 Example/SwiftyMarkdownExample macOS/AppDelegate.swift create mode 100644 Example/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json create mode 100644 Example/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard create mode 100644 Example/SwiftyMarkdownExample macOS/Info.plist create mode 100644 Example/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements create mode 100644 Example/SwiftyMarkdownExample macOS/ViewController.swift create mode 100644 Example/SwiftyMarkdownExample.xcodeproj/project.pbxproj create mode 100644 Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Example/SwiftyMarkdownExample/AppDelegate.swift create mode 100644 Example/SwiftyMarkdownExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/SwiftyMarkdownExample/Assets.xcassets/Contents.json create mode 100644 Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/Contents.json create mode 100644 Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/bubble.png create mode 100644 Example/SwiftyMarkdownExample/Base.lproj/LaunchScreen.storyboard create mode 100644 Example/SwiftyMarkdownExample/Base.lproj/Main.storyboard create mode 100644 Example/SwiftyMarkdownExample/Info.plist create mode 100644 Example/SwiftyMarkdownExample/ViewController.swift create mode 100644 Example/SwiftyMarkdownExample/example.md create mode 100644 Example/SwiftyMarkdownExampleTests/Info.plist create mode 100644 Example/SwiftyMarkdownExampleTests/SwiftyMarkdownExampleTests.swift create mode 100644 Example/SwiftyMarkdownExampleUITests/Info.plist create mode 100644 Example/SwiftyMarkdownExampleUITests/SwiftyMarkdownExampleUITests.swift create mode 100644 Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Contents.swift create mode 100644 Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Resources/bubble.png create mode 100644 Playground/SwiftyMarkdown.playground/Pages/Line Processing.xcplaygroundpage/Contents.swift create mode 100644 Playground/SwiftyMarkdown.playground/Pages/SKLabelNode.xcplaygroundpage/Contents.swift create mode 100644 Playground/SwiftyMarkdown.playground/Pages/Scanner Tests.xcplaygroundpage/Contents.swift create mode 100644 Playground/SwiftyMarkdown.playground/Pages/Tokenising.xcplaygroundpage/Contents.swift create mode 100644 Playground/SwiftyMarkdown.playground/Resources/test.md create mode 100644 Playground/SwiftyMarkdown.playground/Sources/String+SwiftyMarkdown.swift create mode 100644 Playground/SwiftyMarkdown.playground/Sources/Swifty Line Processor.swift create mode 100644 Playground/SwiftyMarkdown.playground/Sources/Swifty Tokeniser.swift create mode 100644 Playground/SwiftyMarkdown.playground/Sources/SwiftyMarkdown.swift create mode 100644 Playground/SwiftyMarkdown.playground/contents.xcplayground create mode 100644 Resources/metadataTest.md create mode 100644 Resources/test.md create mode 100644 Sources/SwiftyMarkdown/String+SwiftyMarkdown.swift create mode 100644 Sources/SwiftyMarkdown/SwiftyLineProcessor.swift create mode 100644 Sources/SwiftyMarkdown/SwiftyMarkdown+iOS.swift create mode 100644 Sources/SwiftyMarkdown/SwiftyMarkdown+macOS.swift create mode 100644 Sources/SwiftyMarkdown/SwiftyMarkdown.swift create mode 100644 Sources/SwiftyMarkdown/SwiftyTokeniser.swift create mode 100644 SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist create mode 100644 SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist create mode 100644 SwiftyMarkdown.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/3EC61D89-CC18-40D5-9AC6-F4504ECE42B5.plist delete mode 100644 SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/Info.plist rename SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/{F4CE988A1C8A921300D735C1.xcbaseline/8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4.plist => SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/AD1DF83E-20BC-4E7E-8C14-683818ED0A26.plist} (83%) create mode 100644 SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/Info.plist rename SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/{SwiftyMarkdown.xcscheme => SwiftyMarkdown-Package.xcscheme} (64%) create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/AppDelegate.swift create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Info.plist create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements create mode 100644 SwiftyMarkdownExample/SwiftyMarkdownExample macOS/ViewController.swift create mode 100644 SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/SwiftyMarkdownTests/SwiftyMarkdownAttributedStringTests copy.swift create mode 100644 Tests/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift create mode 100644 Tests/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift create mode 100644 Tests/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift create mode 100644 Tests/SwiftyMarkdownTests/SwiftyMarkdownPerformanceTests.swift create mode 100644 Tests/SwiftyMarkdownTests/XCTest+SwiftyMarkdown.swift diff --git a/Example/SwiftyMarkdownExample macOS/AppDelegate.swift b/Example/SwiftyMarkdownExample macOS/AppDelegate.swift new file mode 100644 index 0000000..0c22e0e --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/AppDelegate.swift @@ -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 + } + + +} + diff --git a/Example/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2db2b1c --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Example/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json b/Example/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard b/Example/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000..da12ed9 --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/SwiftyMarkdownExample macOS/Info.plist b/Example/SwiftyMarkdownExample macOS/Info.plist new file mode 100644 index 0000000..1d528d1 --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2020 Voyage Travel Apps. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/Example/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements b/Example/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Example/SwiftyMarkdownExample macOS/ViewController.swift b/Example/SwiftyMarkdownExample macOS/ViewController.swift new file mode 100644 index 0000000..f12b670 --- /dev/null +++ b/Example/SwiftyMarkdownExample macOS/ViewController.swift @@ -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. + } + } + + +} + diff --git a/Example/SwiftyMarkdownExample.xcodeproj/project.pbxproj b/Example/SwiftyMarkdownExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..49fc418 --- /dev/null +++ b/Example/SwiftyMarkdownExample.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + 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 */; }; + F4CE98B31C8AEF7D00D735C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98B21C8AEF7D00D735C1 /* Assets.xcassets */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F4CE98BD1C8AEF7D00D735C1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F4CE98A01C8AEF7D00D735C1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F4CE98A71C8AEF7D00D735C1; + remoteInfo = SwiftyMarkdownExample; + }; + F4CE98C81C8AEF7D00D735C1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F4CE98A01C8AEF7D00D735C1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F4CE98A71C8AEF7D00D735C1; + remoteInfo = SwiftyMarkdownExample; + }; +/* 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 = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + F421DD951C8AF34F00B86D66 /* example.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = ""; }; + 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 = ""; }; + F4B4A44D23E4E17400550249 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + F4B4A44F23E4E17400550249 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F4B4A45223E4E17400550249 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F4B4A45423E4E17400550249 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F4B4A45523E4E17400550249 /* SwiftyMarkdownExample_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftyMarkdownExample_macOS.entitlements; sourceTree = ""; }; + F4B4A46123E4E5DD00550249 /* SwiftyMarkdown */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftyMarkdown; path = ../../../Developer/SwiftyMarkdown; sourceTree = ""; }; + 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 = ""; }; + F4CE98AD1C8AEF7D00D735C1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + F4CE98B01C8AEF7D00D735C1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F4CE98B21C8AEF7D00D735C1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F4CE98B51C8AEF7D00D735C1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F4CE98B71C8AEF7D00D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F4CE98BC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4CE98C01C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownExampleTests.swift; sourceTree = ""; }; + F4CE98C21C8AEF7D00D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ""; }; + F4CE98CD1C8AEF7D00D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F4B4A44623E4E17400550249 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98A51C8AEF7D00D735C1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4B4A46023E4E3DC00550249 /* SwiftyMarkdown in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98B91C8AEF7D00D735C1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98C41C8AEF7D00D735C1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* 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 = ""; + }; + F4B4A45923E4E18800550249 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + F4CE989F1C8AEF7D00D735C1 = { + isa = PBXGroup; + children = ( + F4B4A46123E4E5DD00550249 /* SwiftyMarkdown */, + F4CE98AA1C8AEF7D00D735C1 /* SwiftyMarkdownExample */, + F4CE98BF1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */, + F4CE98CA1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */, + F4B4A44A23E4E17400550249 /* SwiftyMarkdownExample macOS */, + F4CE98A91C8AEF7D00D735C1 /* Products */, + F4B4A45923E4E18800550249 /* Frameworks */, + ); + sourceTree = ""; + }; + F4CE98A91C8AEF7D00D735C1 /* Products */ = { + isa = PBXGroup; + children = ( + F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */, + F4CE98BC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.xctest */, + F4CE98C71C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.xctest */, + F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */, + ); + name = Products; + sourceTree = ""; + }; + F4CE98AA1C8AEF7D00D735C1 /* SwiftyMarkdownExample */ = { + isa = PBXGroup; + children = ( + F4CE98AB1C8AEF7D00D735C1 /* AppDelegate.swift */, + F4CE98AD1C8AEF7D00D735C1 /* ViewController.swift */, + F4CE98AF1C8AEF7D00D735C1 /* Main.storyboard */, + F4CE98B21C8AEF7D00D735C1 /* Assets.xcassets */, + F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */, + F4CE98B71C8AEF7D00D735C1 /* Info.plist */, + F421DD951C8AF34F00B86D66 /* example.md */, + ); + path = SwiftyMarkdownExample; + sourceTree = ""; + }; + F4CE98BF1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */ = { + isa = PBXGroup; + children = ( + F4CE98C01C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift */, + F4CE98C21C8AEF7D00D735C1 /* Info.plist */, + ); + path = SwiftyMarkdownExampleTests; + sourceTree = ""; + }; + F4CE98CA1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */ = { + isa = PBXGroup; + children = ( + F4CE98CB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift */, + F4CE98CD1C8AEF7D00D735C1 /* Info.plist */, + ); + path = SwiftyMarkdownExampleUITests; + sourceTree = ""; + }; +/* 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" */; + buildPhases = ( + F4CE98A41C8AEF7D00D735C1 /* Sources */, + F4CE98A51C8AEF7D00D735C1 /* Frameworks */, + F4CE98A61C8AEF7D00D735C1 /* Resources */, + F4CE98E41C8AEFF000D735C1 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftyMarkdownExample; + packageProductDependencies = ( + F4B4A45F23E4E3DC00550249 /* SwiftyMarkdown */, + ); + productName = SwiftyMarkdownExample; + productReference = F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */; + productType = "com.apple.product-type.application"; + }; + F4CE98BB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4CE98D31C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExampleTests" */; + buildPhases = ( + F4CE98B81C8AEF7D00D735C1 /* Sources */, + F4CE98B91C8AEF7D00D735C1 /* Frameworks */, + F4CE98BA1C8AEF7D00D735C1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4CE98BE1C8AEF7D00D735C1 /* PBXTargetDependency */, + ); + name = SwiftyMarkdownExampleTests; + productName = SwiftyMarkdownExampleTests; + productReference = F4CE98BC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F4CE98C61C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4CE98D61C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExampleUITests" */; + buildPhases = ( + F4CE98C31C8AEF7D00D735C1 /* Sources */, + F4CE98C41C8AEF7D00D735C1 /* Frameworks */, + F4CE98C51C8AEF7D00D735C1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4CE98C91C8AEF7D00D735C1 /* PBXTargetDependency */, + ); + name = SwiftyMarkdownExampleUITests; + productName = SwiftyMarkdownExampleUITests; + productReference = F4CE98C71C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F4CE98A01C8AEF7D00D735C1 /* Project object */ = { + isa = PBXProject; + attributes = { + 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 = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F4CE989F1C8AEF7D00D735C1; + packageReferences = ( + F4B4A45E23E4E3DC00550249 /* XCRemoteSwiftPackageReference "SwiftyMarkdown" */, + ); + productRefGroup = F4CE98A91C8AEF7D00D735C1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */, + F4CE98BB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */, + F4CE98C61C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */, + F4B4A44823E4E17400550249 /* SwiftyMarkdownExample macOS */, + ); + }; +/* End PBXProject 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 */, + F4CE98B31C8AEF7D00D735C1 /* Assets.xcassets in Resources */, + F421DD991C8AF4E900B86D66 /* example.md in Resources */, + F4CE98B11C8AEF7D00D735C1 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98BA1C8AEF7D00D735C1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98C51C8AEF7D00D735C1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* 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; + files = ( + F4CE98AE1C8AEF7D00D735C1 /* ViewController.swift in Sources */, + F4CE98AC1C8AEF7D00D735C1 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98B81C8AEF7D00D735C1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4CE98C11C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4CE98C31C8AEF7D00D735C1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4CE98CC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F4CE98BE1C8AEF7D00D735C1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */; + targetProxy = F4CE98BD1C8AEF7D00D735C1 /* PBXContainerItemProxy */; + }; + F4CE98C91C8AEF7D00D735C1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */; + targetProxy = F4CE98C81C8AEF7D00D735C1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F4B4A45123E4E17400550249 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F4B4A45223E4E17400550249 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F4CE98AF1C8AEF7D00D735C1 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F4CE98B01C8AEF7D00D735C1 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F4CE98B41C8AEF7D00D735C1 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F4CE98B51C8AEF7D00D735C1 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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"; + COPY_PHASE_STRIP = NO; + 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; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F4CE98CF1C8AEF7D00D735C1 /* Release */ = { + 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"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F4CE98D11C8AEF7D00D735C1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist; + 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; + }; + F4CE98D21C8AEF7D00D735C1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = SwiftyMarkdownExample/Info.plist; + 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; + }; + F4CE98D41C8AEF7D00D735C1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist; + 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; + }; + F4CE98D51C8AEF7D00D735C1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = SwiftyMarkdownExampleTests/Info.plist; + 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; + }; + F4CE98D71C8AEF7D00D735C1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist; + 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; + }; + name = Debug; + }; + F4CE98D81C8AEF7D00D735C1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SwiftyMarkdownExampleUITests/Info.plist; + 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; + }; + name = Release; + }; +/* 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 = ( + F4CE98CE1C8AEF7D00D735C1 /* Debug */, + F4CE98CF1C8AEF7D00D735C1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4CE98D01C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4CE98D11C8AEF7D00D735C1 /* Debug */, + F4CE98D21C8AEF7D00D735C1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4CE98D31C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4CE98D41C8AEF7D00D735C1 /* Debug */, + F4CE98D51C8AEF7D00D735C1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4CE98D61C8AEF7D00D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4CE98D71C8AEF7D00D735C1 /* Debug */, + F4CE98D81C8AEF7D00D735C1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + 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 */; +} diff --git a/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..6ff7810 --- /dev/null +++ b/Example/SwiftyMarkdownExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/Example/SwiftyMarkdownExample/AppDelegate.swift b/Example/SwiftyMarkdownExample/AppDelegate.swift new file mode 100644 index 0000000..778f14e --- /dev/null +++ b/Example/SwiftyMarkdownExample/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// SwiftyMarkdownExample +// +// Created by Simon Fairbairn on 05/03/2016. +// Copyright © 2016 Voyage Travel Apps. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + 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) { + // 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) { + // 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) { + // 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) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Example/SwiftyMarkdownExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/SwiftyMarkdownExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/Example/SwiftyMarkdownExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/SwiftyMarkdownExample/Assets.xcassets/Contents.json b/Example/SwiftyMarkdownExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/SwiftyMarkdownExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/Contents.json b/Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/Contents.json new file mode 100644 index 0000000..c21b3dc --- /dev/null +++ b/Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/bubble.png b/Example/SwiftyMarkdownExample/Assets.xcassets/bubble.imageset/bubble.png new file mode 100644 index 0000000000000000000000000000000000000000..5c9eb65c1be621db24e59516c5c932232595b4b5 GIT binary patch literal 1675 zcmeHH`7_%I6#quprjFK8q(~$k)z+e-X@WYMNC}BZLJ*~mP_eFNCahqz8n?E#LewI* zx-5&jYI;#^-HcG@k}0KKR3&Pw8+09;o&5{;_kA;;_nFUU-prf%%)FOFrMPRU8>s^T zpyfd%(3N~k2@V9T?2F<+FD0pRa9%h7X#PQCCqzxD5urr7(oZr6fYfvV_^Py0KL9|y z4FG&(0f0j>02p%1p3xiuKm|=D`(98E{9pey0SpGic+;#WHQ<2=H~wXen3D# zS67#}xA*AiC2 zEEZc>SXfk4G&wm527?g@1RM^hQmNhD-Ez5ndwUy&LdC|$uCK2%nM@NClhDvmu~>ZQ z&>;u}GBq^?g+irLshOFXk&%(6rsnYQaBy&Nb#-+{Mn+gzSaWkTgTdI?*s!y+>*?un zaB$#oI0*>}WHNbve!iolBQ-VE$;pX8Ahfr)r=_K_*=&VEfkYzH)6-K@QkzyPqgi~>aTiN{b6%4Qy>2I zed4&xE+Fv7ZyvR(9m)QE%@!7N_$a8{41A#J()~89upno57U!;0`HUciq)N;m z^N16h)^jrvZ=X;7N*}6e+<9W!dEDoOyN9({xe>%Bxf6hFH5gGT4swZ19sp?Q{sYk1 z?6n;L0OLIfINyjl`83C04+qU$@E{^UY{VS|GS#SRhN_+1BY3_!2Zz|8k!Vme-Ir=P zo&4$fuKmNZlOSvdgH?4i3enBGEz3tOgD#cEdO8989_@SVrN@*q_qXT@#gDD zHv$U!v#Vs?N*Vzv(bbDm$t495v6~*+*LH5||LHKws zntAJBj0_cHGr$HE2A-fBM|Ip7$!?5Pmafm{`?K6Z@Fh_X0COD8Aw5Th1l9=t{5(^oPm;#uy-Cf zQ3mj3eq~o$F<#58cndbMlaIEY!Afo`gK%~wW@>$a{N>-!25!2d+Cpp#C|QZmEEqc_ z`63kD;}5p-tHr^y72$CSiPhqwp?S-4P38>9?dkIA{=Mm*@;SuP%NgdYksDR$z%0s7 zq5eAAfSHJ%se3>CV7u~)Tv-nfBIXbzMfd~;Or^d__2#w-ED9WmsY z`o%)=OVfYMFCc!=uun8O%aicG@TnWB8+Ppe9krL^FN!$<{d%sS4Xt`N3@;6lEC&{A zkr|^Ok{2i2d+8m}mT3F+B{{GJ^W*kv(_3}7my9Y2aOIG<~N1{R3_B#EX-Rl?ZAIEI&FsB!G_DLD{w*=zy zf=z4v=yx~JZJR(cqXko*tB||g%S$=)8uz}Ut6j@^(bRsh6rtNJ!=@=x9D>e{g#Yst MJuXlP4S1H|Z{Yu6!~g&Q literal 0 HcmV?d00001 diff --git a/Example/SwiftyMarkdownExample/Base.lproj/LaunchScreen.storyboard b/Example/SwiftyMarkdownExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..2e721e1 --- /dev/null +++ b/Example/SwiftyMarkdownExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/SwiftyMarkdownExample/Base.lproj/Main.storyboard b/Example/SwiftyMarkdownExample/Base.lproj/Main.storyboard new file mode 100644 index 0000000..558d93a --- /dev/null +++ b/Example/SwiftyMarkdownExample/Base.lproj/Main.storyboard @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/SwiftyMarkdownExample/Info.plist b/Example/SwiftyMarkdownExample/Info.plist new file mode 100644 index 0000000..40c6215 --- /dev/null +++ b/Example/SwiftyMarkdownExample/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/SwiftyMarkdownExample/ViewController.swift b/Example/SwiftyMarkdownExample/ViewController.swift new file mode 100644 index 0000000..3752eb2 --- /dev/null +++ b/Example/SwiftyMarkdownExample/ViewController.swift @@ -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") + } + } +} + diff --git a/Example/SwiftyMarkdownExample/example.md b/Example/SwiftyMarkdownExample/example.md new file mode 100644 index 0000000..e7c927d --- /dev/null +++ b/Example/SwiftyMarkdownExample/example.md @@ -0,0 +1,50 @@ +# Swifty Markdown + +SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even with custom fonts. + +Show Images From Your App Bundle! +--- +![Image](bubble) + +Customise fonts and colors easily in a Swift-like way: + + md.code.fontName = "CourierNewPSMT" + + md.h2.fontName = "AvenirNextCondensed-Medium" + md.h2.color = UIColor.redColor() + md.h2.alignment = .center + +It supports the standard Markdown syntax, like *italics*, _underline italics_, **bold**, `backticks for code`, ~~strikethrough~~, and headings. + +It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and \`backticks\`. It also supports inline Markdown [Links](http://voyagetravelapps.com/). + +> It also now supports blockquotes +> and it supports whole-line italic and bold styles so you can go completely wild with styling! Wow! Such styles! Much fun! + +**Lists** + +- It Supports +- Unordered +- Lists + - Indented item with a longer string to make sure indentation is consistent + - Second level indent with a longer string to make sure indentation is consistent +- List item with a longer string to make sure indentation is consistent + +1. And +1. Ordered +1. Lists + 1. Indented item + 1. Second level indent +1. (Use `1.` as the list item identifier) +1. List item +1. List item + - Mix + - List styles +1. List item with a longer string to make sure indentation is consistent +1. List item +1. List item +1. List item +1. List item + + + diff --git a/Example/SwiftyMarkdownExampleTests/Info.plist b/Example/SwiftyMarkdownExampleTests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/Example/SwiftyMarkdownExampleTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Example/SwiftyMarkdownExampleTests/SwiftyMarkdownExampleTests.swift b/Example/SwiftyMarkdownExampleTests/SwiftyMarkdownExampleTests.swift new file mode 100644 index 0000000..e39301e --- /dev/null +++ b/Example/SwiftyMarkdownExampleTests/SwiftyMarkdownExampleTests.swift @@ -0,0 +1,36 @@ +// +// SwiftyMarkdownExampleTests.swift +// SwiftyMarkdownExampleTests +// +// Created by Simon Fairbairn on 05/03/2016. +// Copyright © 2016 Voyage Travel Apps. All rights reserved. +// + +import XCTest +@testable import SwiftyMarkdownExample + +class SwiftyMarkdownExampleTests: 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.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Example/SwiftyMarkdownExampleUITests/Info.plist b/Example/SwiftyMarkdownExampleUITests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/Example/SwiftyMarkdownExampleUITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Example/SwiftyMarkdownExampleUITests/SwiftyMarkdownExampleUITests.swift b/Example/SwiftyMarkdownExampleUITests/SwiftyMarkdownExampleUITests.swift new file mode 100644 index 0000000..dd3844a --- /dev/null +++ b/Example/SwiftyMarkdownExampleUITests/SwiftyMarkdownExampleUITests.swift @@ -0,0 +1,36 @@ +// +// SwiftyMarkdownExampleUITests.swift +// SwiftyMarkdownExampleUITests +// +// Created by Simon Fairbairn on 05/03/2016. +// Copyright © 2016 Voyage Travel Apps. All rights reserved. +// + +import XCTest + +class SwiftyMarkdownExampleUITests: XCTestCase { + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + 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() { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + +} diff --git a/Package.swift b/Package.swift index c547774..a5c5443 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.1 import PackageDescription let package = Package( @@ -13,6 +13,7 @@ let package = Package( .library(name: "SwiftyMarkdown", targets: ["SwiftyMarkdown"]), ], targets: [ - .target(name: "SwiftyMarkdown", path: "SwiftyMarkdown"), + .target(name: "SwiftyMarkdown"), + .testTarget(name: "SwiftyMarkdownTests", dependencies: ["SwiftyMarkdown"]) ] ) diff --git a/Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Contents.swift b/Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..b470d2e --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Contents.swift @@ -0,0 +1,428 @@ +//: [Previous](@previous) + +import UIKit + + +enum CharacterStyle : CharacterStyling { + case none + case bold + case italic + case code + case link + case image +} + +enum MarkdownLineStyle : LineStyling { + var shouldTokeniseLine: Bool { + switch self { + case .codeblock: + return false + default: + return true + } + + } + + case h1 + case h2 + case h3 + case h4 + case h5 + case h6 + case previousH1 + case previousH2 + case body + case blockquote + case codeblock + case unorderedList + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? { + switch self { + case .previousH1: + return MarkdownLineStyle.h1 + case .previousH2: + return MarkdownLineStyle.h2 + default : + return nil + } + } +} + + +@objc public protocol FontProperties { + var fontName : String? { get set } + var color : UIColor { get set } + var fontSize : CGFloat { get set } +} + + +/** +A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied. + +If that is not set, then the system default will be used. +*/ +@objc open class BasicStyles : NSObject, FontProperties { + public var fontName : String? + public var color = UIColor.black + public var fontSize : CGFloat = 0.0 +} + +/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type. +@objc open class SwiftyMarkdown: NSObject { + static let lineRules = [ + LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading), + LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), + LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both), + LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both), + LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both), + LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both), + LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both), + LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both) + ] + + static let characterRules = [ + CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1), + CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1), + CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining), + CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3), + CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) + ] + + let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body) + let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules) + + /// The styles to apply to any H1 headers found in the Markdown + open var h1 = BasicStyles() + + /// The styles to apply to any H2 headers found in the Markdown + open var h2 = BasicStyles() + + /// The styles to apply to any H3 headers found in the Markdown + open var h3 = BasicStyles() + + /// The styles to apply to any H4 headers found in the Markdown + open var h4 = BasicStyles() + + /// The styles to apply to any H5 headers found in the Markdown + open var h5 = BasicStyles() + + /// The styles to apply to any H6 headers found in the Markdown + open var h6 = BasicStyles() + + /// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them. + open var body = BasicStyles() + + /// The styles to apply to any links found in the Markdown + open var link = BasicStyles() + + /// The styles to apply to any bold text found in the Markdown + open var bold = BasicStyles() + + /// The styles to apply to any italic text found in the Markdown + open var italic = BasicStyles() + + /// The styles to apply to any code blocks or inline code text found in the Markdown + open var code = BasicStyles() + + + var currentType : MarkdownLineStyle = .body + + + let string : String + + let tagList = "!\\_*`[]()" + let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()") + + + /** + + - parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString + + - returns: An initialized SwiftyMarkdown object + */ + public init(string : String ) { + self.string = string + } + + /** + A failable initializer that takes a URL and attempts to read it as a UTF-8 string + + - parameter url: The location of the file to read + + - returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read + */ + public init?(url : URL ) { + + do { + self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String + + } catch { + self.string = "" + return nil + } + } + + /** + Set font size for all styles + + - parameter size: size of font + */ + open func setFontSizeForAllStyles(with size: CGFloat) { + h1.fontSize = size + h2.fontSize = size + h3.fontSize = size + h4.fontSize = size + h5.fontSize = size + h6.fontSize = size + body.fontSize = size + italic.fontSize = size + code.fontSize = size + link.fontSize = size + } + + open func setFontColorForAllStyles(with color: UIColor) { + h1.color = color + h2.color = color + h3.color = color + h4.color = color + h5.color = color + h6.color = color + body.color = color + italic.color = color + code.color = color + link.color = color + } + + open func setFontNameForAllStyles(with name: String) { + h1.fontName = name + h2.fontName = name + h3.fontName = name + h4.fontName = name + h5.fontName = name + h6.fontName = name + body.fontName = name + italic.fontName = name + code.fontName = name + link.fontName = name + } + + + + /** + Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called. + + - returns: An NSAttributedString with the styles applied + */ + open func attributedString() -> NSAttributedString { + let attributedString = NSMutableAttributedString(string: "") + let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) + + var strings : [String] = [] + for line in foundAttributes { + let finalTokens = self.tokeniser.process(line.line) + attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) + } + + return attributedString + } + + +} + +extension SwiftyMarkdown { + + func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont { + let textStyle : UIFont.TextStyle + var fontName : String? + var fontSize : CGFloat? + + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1: + fontName = h1.fontName + fontSize = h1.fontSize + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title1 + } else { + textStyle = UIFont.TextStyle.headline + } + case .h2: + fontName = h2.fontName + fontSize = h2.fontSize + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title2 + } else { + textStyle = UIFont.TextStyle.headline + } + case .h3: + fontName = h3.fontName + fontSize = h3.fontSize + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title2 + } else { + textStyle = UIFont.TextStyle.subheadline + } + case .h4: + fontName = h4.fontName + fontSize = h4.fontSize + textStyle = UIFont.TextStyle.headline + case .h5: + fontName = h5.fontName + fontSize = h5.fontSize + textStyle = UIFont.TextStyle.subheadline + case .h6: + fontName = h6.fontName + fontSize = h6.fontSize + textStyle = UIFont.TextStyle.footnote + default: + fontName = body.fontName + fontSize = body.fontSize + textStyle = UIFont.TextStyle.body + } + + if fontName == nil { + fontName = body.fontName + } + + if let characterOverride = characterOverride { + switch characterOverride { + case .code: + fontName = code.fontName ?? fontName + case .link: + fontName = link.fontName ?? fontName + default: + break + } + } + + fontSize = fontSize == 0.0 ? nil : fontSize + var font : UIFont + if let existentFontName = fontName { + font = UIFont.preferredFont(forTextStyle: textStyle) + let finalSize : CGFloat + if let existentFontSize = fontSize { + finalSize = existentFontSize + } else { + let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14) + } + + if let customFont = UIFont(name: existentFontName, size: finalSize) { + let fontMetrics = UIFontMetrics(forTextStyle: textStyle) + font = fontMetrics.scaledFont(for: customFont) + } else { + font = UIFont.preferredFont(forTextStyle: textStyle) + } + } else { + font = UIFont.preferredFont(forTextStyle: textStyle) + } + + return font + + } + + func color( for line : SwiftyLine ) -> UIColor { + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1, .previousH1: + return h1.color + case .h2, .previousH2: + return h2.color + case .h3: + return h3.color + case .h4: + return h4.color + case .h5: + return h5.color + case .h6: + return h6.color + case .body: + return body.color + case .codeblock: + return code.color + case .blockquote: + return body.color + case .unorderedList: + return body.color + } + } + + func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString { + var outputLine = line.line + if let style = line.lineStyle as? MarkdownLineStyle, style == .codeblock { + outputLine = "\t\(outputLine)" + } + + var attributes : [NSAttributedString.Key : AnyObject] = [:] + let finalAttributedString = NSMutableAttributedString() + for token in tokens { + var font = self.font(for: line) + attributes[.foregroundColor] = self.color(for: line) + guard let styles = token.characterStyles as? [CharacterStyle] else { + continue + } + if styles.contains(.italic) { + if let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) { + font = UIFont(descriptor: italicDescriptor, size: 0) + } + } + if styles.contains(.bold) { + if let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) { + font = UIFont(descriptor: boldDescriptor, size: 0) + } + } + attributes[.font] = font + if styles.contains(.link), let url = token.metadataString { + attributes[.foregroundColor] = self.link.color + attributes[.font] = self.font(for: line, characterOverride: .link) + attributes[.link] = url as AnyObject + } + + if styles.contains(.image), let imageName = token.metadataString { + let image1Attachment = NSTextAttachment() + image1Attachment.image = UIImage(named: imageName) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + continue + } + + if styles.contains(.code) { + attributes[.foregroundColor] = self.code.color + attributes[.font] = self.font(for: line, characterOverride: .code) + } else { + // Switch back to previous font + } + let str = NSAttributedString(string: token.outputString, attributes: attributes) + finalAttributedString.append(str) + } + + + return finalAttributedString + } +} + + +let image = UIImage(named: "bubble") +let image1Attachment = NSTextAttachment() +image1Attachment.image = image +let att = NSAttributedString(attachment: image1Attachment) + + + +var str = "# Hello, *playground* `code` **bold** ![Image](bubble)" + +let md = SwiftyMarkdown(string: str) +md.body.color = .red +md.h1.color = .white +md.h1.fontName = "Noteworthy-Light" + +md.link.color = .red + +md.code.fontName = "CourierNewPSMT" + +md.attributedString() + +//: [Next](@next) diff --git a/Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Resources/bubble.png b/Playground/SwiftyMarkdown.playground/Pages/Attributed String.xcplaygroundpage/Resources/bubble.png new file mode 100644 index 0000000000000000000000000000000000000000..5c9eb65c1be621db24e59516c5c932232595b4b5 GIT binary patch literal 1675 zcmeHH`7_%I6#quprjFK8q(~$k)z+e-X@WYMNC}BZLJ*~mP_eFNCahqz8n?E#LewI* zx-5&jYI;#^-HcG@k}0KKR3&Pw8+09;o&5{;_kA;;_nFUU-prf%%)FOFrMPRU8>s^T zpyfd%(3N~k2@V9T?2F<+FD0pRa9%h7X#PQCCqzxD5urr7(oZr6fYfvV_^Py0KL9|y z4FG&(0f0j>02p%1p3xiuKm|=D`(98E{9pey0SpGic+;#WHQ<2=H~wXen3D# zS67#}xA*AiC2 zEEZc>SXfk4G&wm527?g@1RM^hQmNhD-Ez5ndwUy&LdC|$uCK2%nM@NClhDvmu~>ZQ z&>;u}GBq^?g+irLshOFXk&%(6rsnYQaBy&Nb#-+{Mn+gzSaWkTgTdI?*s!y+>*?un zaB$#oI0*>}WHNbve!iolBQ-VE$;pX8Ahfr)r=_K_*=&VEfkYzH)6-K@QkzyPqgi~>aTiN{b6%4Qy>2I zed4&xE+Fv7ZyvR(9m)QE%@!7N_$a8{41A#J()~89upno57U!;0`HUciq)N;m z^N16h)^jrvZ=X;7N*}6e+<9W!dEDoOyN9({xe>%Bxf6hFH5gGT4swZ19sp?Q{sYk1 z?6n;L0OLIfINyjl`83C04+qU$@E{^UY{VS|GS#SRhN_+1BY3_!2Zz|8k!Vme-Ir=P zo&4$fuKmNZlOSvdgH?4i3enBGEz3tOgD#cEdO8989_@SVrN@*q_qXT@#gDD zHv$U!v#Vs?N*Vzv(bbDm$t495v6~*+*LH5||LHKws zntAJBj0_cHGr$HE2A-fBM|Ip7$!?5Pmafm{`?K6Z@Fh_X0COD8Aw5Th1l9=t{5(^oPm;#uy-Cf zQ3mj3eq~o$F<#58cndbMlaIEY!Afo`gK%~wW@>$a{N>-!25!2d+Cpp#C|QZmEEqc_ z`63kD;}5p-tHr^y72$CSiPhqwp?S-4P38>9?dkIA{=Mm*@;SuP%NgdYksDR$z%0s7 zq5eAAfSHJ%se3>CV7u~)Tv-nfBIXbzMfd~;Or^d__2#w-ED9WmsY z`o%)=OVfYMFCc!=uun8O%aicG@TnWB8+Ppe9krL^FN!$<{d%sS4Xt`N3@;6lEC&{A zkr|^Ok{2i2d+8m}mT3F+B{{GJ^W*kv(_3}7my9Y2aOIG<~N1{R3_B#EX-Rl?ZAIEI&FsB!G_DLD{w*=zy zf=z4v=yx~JZJR(cqXko*tB||g%S$=)8uz}Ut6j@^(bRsh6rtNJ!=@=x9D>e{g#Yst MJuXlP4S1H|Z{Yu6!~g&Q literal 0 HcmV?d00001 diff --git a/Playground/SwiftyMarkdown.playground/Pages/Line Processing.xcplaygroundpage/Contents.swift b/Playground/SwiftyMarkdown.playground/Pages/Line Processing.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..c54f6a1 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Pages/Line Processing.xcplaygroundpage/Contents.swift @@ -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 ?? "") + diff --git a/Playground/SwiftyMarkdown.playground/Pages/SKLabelNode.xcplaygroundpage/Contents.swift b/Playground/SwiftyMarkdown.playground/Pages/SKLabelNode.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..094711d --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Pages/SKLabelNode.xcplaygroundpage/Contents.swift @@ -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) diff --git a/Playground/SwiftyMarkdown.playground/Pages/Scanner Tests.xcplaygroundpage/Contents.swift b/Playground/SwiftyMarkdown.playground/Pages/Scanner Tests.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..46bb153 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Pages/Scanner Tests.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + + + +//: [Next](@next) diff --git a/Playground/SwiftyMarkdown.playground/Pages/Tokenising.xcplaygroundpage/Contents.swift b/Playground/SwiftyMarkdown.playground/Pages/Tokenising.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..afc7016 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Pages/Tokenising.xcplaygroundpage/Contents.swift @@ -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.. [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.. [Token] { + var mutableTokens : [Token] = tokens + print( tokens.map( { ( $0.outputString, $0.count )})) + for idx in 0.. 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.. [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).. string.startIndex ) ? String(string[string.index(before: scanLocation).. Bool { + switch rule.spacesAllowed { + case .leadingSide: + guard nextCharacter != nil else { + return true + } + if nextCharacter == " " { + return false + } + case .trailingSide: + guard previousCharacter != nil else { + return true + } + if previousCharacter == " " { + return false + } + case .no: + switch (previousCharacter, nextCharacter) { + case (nil, nil), ( " ", _ ), ( _, " " ): + return false + default: + return true + } + + case .oneSide: + switch (previousCharacter, nextCharacter) { + case (nil, " " ), (" ", nil), (" ", " " ): + return false + default: + return true + } + default: + break + } + return true + } + +} + + + +// Example customisation +public enum CharacterStyle : CharacterStyling { + case none + case bold + case italic + case code + case link + case image +} + + + +var str = "A standard paragraph with an *italic*, * spaced asterisk, \\*escaped asterisks\\*, _underscored italics_, \\_escaped underscores\\_, **bold** \\*\\*escaped double asterisks\\*\\*, __underscored bold__, _ spaced underscore \\_\\_escaped double underscores\\_\\_ and a `code block *with an italic that should be ignored*`." + +//str = "**AAAA*BB\\*BB*AAAAAA**" +str = "*_*Bold and italic*_*" +str = "*Italic* `Code block with *ignored* italic` __Bold__" + +struct TokenTest { + let input : String + let output : String + let tokens : [Token] +} + +let challenge1 = TokenTest(input: "*_*italic*_*", output: "italic", tokens: [ + Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic]) +]) +let challenge2 = TokenTest(input: "*Italic* `Code block with *ignored* italic` __Bold__", output : "Italic `Code block with *ignored* italic` Bold", tokens : [ + Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "Code block with *ignored* italic", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold]) +]) +let challenge3 = TokenTest(input: " * ", output : " * ", tokens : [ + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "*", characterStyles: []), + Token(type: .string, inputString: " ", characterStyles: []) +]) +let challenge4 = TokenTest(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [ + Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: "BB*BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]), + Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]), +]) +let challenge5 = TokenTest(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [ + Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " __Not Bold__ ", characterStyles: []), + Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold]) +]) + +let challenge6 = TokenTest(input: " *\\** ", output : " *** ", tokens : [ + Token(type: .string, inputString: " *** ", characterStyles: []) +]) + +let challenge7 = TokenTest(input: " *\\**Italic*\\** ", output : " *Italic* ", tokens : [ + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "*Italic*", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " ", characterStyles: []), +]) + +let challenge8 = TokenTest(input: "[*Link*](https://www.neverendingvoyage.com/)", output : "Link", tokens : [ + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic]) +]) + +let challenge9 = TokenTest(input: "`Code (should not be indented)`", output: "Code (should not be indented)", tokens: [ + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic]) +]) + +let challenge10 = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " word", characterStyles: []) +]) + +let oneEscapedAsteriskOneNormalAtStart = TokenTest(input: "\\**A normal string\\**", output: "*A normal string*", tokens: [ + Token(type: .string, inputString: "*", characterStyles: []), + Token(type: .string, inputString: "A normal string*", characterStyles: [CharacterStyle.italic]), +]) + + +let escapedBoldAtStart = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [ + Token(type: .string, inputString: "**A normal string**", characterStyles: []) +]) + +let escapedBoldWithin = TokenTest(input: "A string with \\*\\*escaped\\*\\* asterisks", output: "A string with **escaped** asterisks", tokens: [ + Token(type: .string, inputString: "A string with **escaped** asterisks", characterStyles: []) +]) + +let oneEscapedAsteriskOneNormalWithin = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [ + Token(type: .string, inputString: "A string with one *", characterStyles: []), + Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: []) +]) + +let oneEscapedAsteriskTwoNormalWithin = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [ + Token(type: .string, inputString: "A string with randomly **", characterStyles: []), + Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: "** asterisks", characterStyles: []) +]) + +"Given an array of integers, return **indices** of the two numbers such that they add up to a specific target.\n\nYou may assume that each input would have **_exactly_** one solution, and you may not use the _same_ element twice.\n\n**Example:**\n\nGiven nums = [2, 7, 11, 15], target = 9,\n\nBecause nums[**0**] + nums[**1**] = 2 + 7 = 9,\nreturn [**0**, **1**]." + +let challenges = [challenge8] + +var images = CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1) +var links = CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1) +var codeblock = CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1) +codeblock.cancels = .allRemaining +let asterisks = CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) +let underscores = CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) + +let scan = SwiftyTokeniser(with: [ images, links, codeblock, asterisks]) + + +for challenge in challenges { + let finalTokens = scan.process(challenge.input) + let stringTokens = finalTokens.filter({ $0.type == .string }) + + guard stringTokens.count == challenge.tokens.count else { + print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(stringTokens.count)") + print("-------EXPECTED--------") + for token in challenge.tokens { + switch token.type { + case .string: + print("\(token.outputString): \(token.characterStyles)") + default: + print("\(token.outputString)") + } + } + print("-------OUTPUT--------") + for token in finalTokens { + switch token.type { + case .string: + print("\(token.outputString): \(token.characterStyles)") + default: + if !token.outputString.isEmpty { + print("\(token.outputString)") + } + + } + } + continue + } + + print("-----EXPECTATIONS-----") + for (idx, token) in stringTokens.enumerated() { + let expected = challenge.tokens[idx] + + if expected.type != token.type { + print("Failure: Token types are different. Expected: \(expected.type), found: \(token.type)") + } + + switch token.type { + case .string: + print("Expected: \(expected.outputString): \(expected.characterStyles)") + print("Found: \(token.outputString): \(token.characterStyles)") + if token.metadataString != nil { + print("Metadata: \(token.metadataString!)") + } + + default: + break + } + } + + let string = finalTokens.map({ $0.outputString }).joined() + print("-------OUTPUT VS INPUT--------") + print("Input: \(challenge.input)") + print("Expected: \(challenge.output)") + print("Output: \(string)") + +} + + + + + + +//: [Next](@next) diff --git a/Playground/SwiftyMarkdown.playground/Resources/test.md b/Playground/SwiftyMarkdown.playground/Resources/test.md new file mode 100644 index 0000000..0c3b884 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Resources/test.md @@ -0,0 +1,24 @@ +A Markdown Example +======== + +Headings +----- + +# Heading 1 +## Heading 2 +### Heading 3 ### +#### Heading 4 #### +##### Heading 5 ##### +###### Heading 6 ###### + +A simple paragraph with a random * in *the* middle. Now with ** **Added Bold** + +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\_\_. + +This is a very basic implementation of markdown. + +*This whole line is italic* + +**This whole line is bold** + +The End \ No newline at end of file diff --git a/Playground/SwiftyMarkdown.playground/Sources/String+SwiftyMarkdown.swift b/Playground/SwiftyMarkdown.playground/Sources/String+SwiftyMarkdown.swift new file mode 100644 index 0000000..58cfbcf --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Sources/String+SwiftyMarkdown.swift @@ -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(of string: S, options: String.CompareOptions = []) -> Index? { + range(of: string, options: options)?.lowerBound + } + func endIndex(of string: S, options: String.CompareOptions = []) -> Index? { + range(of: string, options: options)?.upperBound + } + func indices(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(of string: S, options: String.CompareOptions = []) -> [Range] { + var result: [Range] = [] + 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 + } +} diff --git a/Playground/SwiftyMarkdown.playground/Sources/Swifty Line Processor.swift b/Playground/SwiftyMarkdown.playground/Sources/Swifty Line Processor.swift new file mode 100644 index 0000000..b48e78b --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Sources/Swifty Line Processor.swift @@ -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.. 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.. 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 + } + +} + + diff --git a/Playground/SwiftyMarkdown.playground/Sources/Swifty Tokeniser.swift b/Playground/SwiftyMarkdown.playground/Sources/Swifty Tokeniser.swift new file mode 100644 index 0000000..256553c --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Sources/Swifty Tokeniser.swift @@ -0,0 +1,496 @@ +// +// SwiftyTokeniser.swift +// SwiftyMarkdown +// +// Created by Simon Fairbairn on 16/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// +import Foundation +import os.log + +extension OSLog { + private static var subsystem = "SwiftyTokeniser" + static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising") + static let styling = OSLog(subsystem: subsystem, category: "Styling") +} + +// Tag definition +public protocol CharacterStyling { + +} + +public enum SpaceAllowed { + case no + case bothSides + case oneSide + case leadingSide + case trailingSide +} + +public enum Cancel { + case none + case allRemaining + case currentSet +} + +public struct CharacterRule { + public let openTag : String + public let intermediateTag : String? + public let closingTag : String? + public let escapeCharacter : Character? + public let styles : [Int : [CharacterStyling]] + public var maxTags : Int = 1 + public var spacesAllowed : SpaceAllowed = .oneSide + public var cancels : Cancel = .none + + public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) { + self.openTag = openTag + self.intermediateTag = intermediateTag + self.closingTag = closingTag + self.escapeCharacter = escapeCharacter + self.styles = styles + self.maxTags = maxTags + self.cancels = cancels + } +} + +// Token definition +public enum TokenType { + case repeatingTag + case openTag + case intermediateTag + case closeTag + case processed + case string + case escape + case metadata +} + + + +public struct Token { + public let id = UUID().uuidString + public var type : TokenType + public let inputString : String + public var metadataString : String? = nil + public var characterStyles : [CharacterStyling] = [] + public var count : Int = 0 + public var shouldSkip : Bool = false + public var outputString : String { + get { + switch self.type { + case .repeatingTag: + if count == 0 { + return "" + } else { + let range = inputString.startIndex.. [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.. [Token] { + var mutableTokens : [Token] = tokens + print( tokens.map( { ( $0.outputString, $0.count )})) + for idx in 0.. 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.. [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).. string.startIndex ) ? String(string[string.index(before: scanLocation).. 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 + } + +} diff --git a/Playground/SwiftyMarkdown.playground/Sources/SwiftyMarkdown.swift b/Playground/SwiftyMarkdown.playground/Sources/SwiftyMarkdown.swift new file mode 100644 index 0000000..4b632b5 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/Sources/SwiftyMarkdown.swift @@ -0,0 +1,519 @@ +import Foundation +import UIKit + +enum CharacterStyle : CharacterStyling { + case none + case bold + case italic + case code + case link + case image +} + +enum MarkdownLineStyle : LineStyling { + var shouldTokeniseLine: Bool { + switch self { + case .codeblock: + return false + default: + return true + } + + } + + case h1 + case h2 + case h3 + case h4 + case h5 + case h6 + case previousH1 + case previousH2 + case body + case blockquote + case codeblock + case unorderedList + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? { + switch self { + case .previousH1: + return MarkdownLineStyle.h1 + case .previousH2: + return MarkdownLineStyle.h2 + default : + return nil + } + } +} + +@objc public enum FontStyle : Int { + case normal + case bold + case italic + case boldItalic +} + +@objc public protocol FontProperties { + var fontName : String? { get set } + var color : UIColor { get set } + var fontSize : CGFloat { get set } + var fontStyle : FontStyle { get set } +} + +@objc public protocol LineProperties { + var alignment : NSTextAlignment { get set } +} + + +/** +A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied. + +If that is not set, then the system default will be used. +*/ +@objc open class BasicStyles : NSObject, FontProperties { + public var fontName : String? + public var color = UIColor.black + public var fontSize : CGFloat = 0.0 + public var fontStyle : FontStyle = .normal +} + +@objc open class LineStyles : NSObject, FontProperties, LineProperties { + public var fontName : String? + public var color = UIColor.black + public var fontSize : CGFloat = 0.0 + public var fontStyle : FontStyle = .normal + public var alignment: NSTextAlignment = .left +} + + + +/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type. +@objc open class SwiftyMarkdown: NSObject { + static let lineRules = [ + LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous), + LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), + LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading), + LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading), + LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both), + LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both), + LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both), + LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both), + LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both), + LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both) + ] + + static let characterRules = [ + CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1), + CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1), + CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining), + CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3), + CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) + ] + + let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body) + let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules) + + /// The styles to apply to any H1 headers found in the Markdown + open var h1 = LineStyles() + + /// The styles to apply to any H2 headers found in the Markdown + open var h2 = LineStyles() + + /// The styles to apply to any H3 headers found in the Markdown + open var h3 = LineStyles() + + /// The styles to apply to any H4 headers found in the Markdown + open var h4 = LineStyles() + + /// The styles to apply to any H5 headers found in the Markdown + open var h5 = LineStyles() + + /// The styles to apply to any H6 headers found in the Markdown + open var h6 = LineStyles() + + /// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them. + open var body = LineStyles() + + /// The styles to apply to any blockquotes found in the Markdown + open var blockquotes = LineStyles() + + /// The styles to apply to any links found in the Markdown + open var link = BasicStyles() + + /// The styles to apply to any bold text found in the Markdown + open var bold = BasicStyles() + + /// The styles to apply to any italic text found in the Markdown + open var italic = BasicStyles() + + /// The styles to apply to any code blocks or inline code text found in the Markdown + open var code = BasicStyles() + + + + public var underlineLinks : Bool = false + + var currentType : MarkdownLineStyle = .body + + + let string : String + + let tagList = "!\\_*`[]()" + let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()") + + + /** + + - parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString + + - returns: An initialized SwiftyMarkdown object + */ + public init(string : String ) { + self.string = string + super.init() + if #available(iOS 13.0, *) { + self.setFontColorForAllStyles(with: .label) + } + } + + /** + A failable initializer that takes a URL and attempts to read it as a UTF-8 string + + - parameter url: The location of the file to read + + - returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read + */ + public init?(url : URL ) { + + do { + self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String + + } catch { + self.string = "" + return nil + } + super.init() + if #available(iOS 13.0, *) { + self.setFontColorForAllStyles(with: .label) + } + } + + /** + Set font size for all styles + + - parameter size: size of font + */ + open func setFontSizeForAllStyles(with size: CGFloat) { + h1.fontSize = size + h2.fontSize = size + h3.fontSize = size + h4.fontSize = size + h5.fontSize = size + h6.fontSize = size + body.fontSize = size + italic.fontSize = size + bold.fontSize = size + code.fontSize = size + link.fontSize = size + link.fontSize = size + } + + open func setFontColorForAllStyles(with color: UIColor) { + h1.color = color + h2.color = color + h3.color = color + h4.color = color + h5.color = color + h6.color = color + body.color = color + italic.color = color + bold.color = color + code.color = color + link.color = color + blockquotes.color = color + } + + open func setFontNameForAllStyles(with name: String) { + h1.fontName = name + h2.fontName = name + h3.fontName = name + h4.fontName = name + h5.fontName = name + h6.fontName = name + body.fontName = name + italic.fontName = name + bold.fontName = name + code.fontName = name + link.fontName = name + blockquotes.fontName = name + } + + + + /** + Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called. + + - returns: An NSAttributedString with the styles applied + */ + open func attributedString() -> NSAttributedString { + let attributedString = NSMutableAttributedString(string: "") + self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body + let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) + + for line in foundAttributes { + let finalTokens = self.tokeniser.process(line.line) + attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) + attributedString.append(NSAttributedString(string: "\n")) + } + return attributedString + } + + +} + +extension SwiftyMarkdown { + + func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont { + let textStyle : UIFont.TextStyle + var fontName : String? + var fontSize : CGFloat? + + var globalBold = false + var globalItalic = false + + let style : FontProperties + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1: + style = self.h1 + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title1 + } else { + textStyle = UIFont.TextStyle.headline + } + case .h2: + style = self.h2 + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title2 + } else { + textStyle = UIFont.TextStyle.headline + } + case .h3: + style = self.h3 + if #available(iOS 9, *) { + textStyle = UIFont.TextStyle.title2 + } else { + textStyle = UIFont.TextStyle.subheadline + } + case .h4: + style = self.h4 + textStyle = UIFont.TextStyle.headline + case .h5: + style = self.h5 + textStyle = UIFont.TextStyle.subheadline + case .h6: + style = self.h6 + textStyle = UIFont.TextStyle.footnote + case .codeblock: + style = self.code + textStyle = UIFont.TextStyle.body + case .blockquote: + style = self.blockquotes + textStyle = UIFont.TextStyle.body + default: + style = self.body + textStyle = UIFont.TextStyle.body + } + + fontName = style.fontName + fontSize = style.fontSize + switch style.fontStyle { + case .bold: + globalBold = true + case .italic: + globalItalic = true + case .boldItalic: + globalItalic = true + globalBold = true + case .normal: + break + } + + if fontName == nil { + fontName = body.fontName + } + + if let characterOverride = characterOverride { + switch characterOverride { + case .code: + fontName = code.fontName ?? fontName + fontSize = code.fontSize + case .link: + fontName = link.fontName ?? fontName + fontSize = link.fontSize + case .bold: + fontName = bold.fontName ?? fontName + fontSize = bold.fontSize + globalBold = true + case .italic: + fontName = italic.fontName ?? fontName + fontSize = italic.fontSize + globalItalic = true + default: + break + } + } + + fontSize = fontSize == 0.0 ? nil : fontSize + var font : UIFont + if let existentFontName = fontName { + font = UIFont.preferredFont(forTextStyle: textStyle) + let finalSize : CGFloat + if let existentFontSize = fontSize { + finalSize = existentFontSize + } else { + let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14) + } + + if let customFont = UIFont(name: existentFontName, size: finalSize) { + let fontMetrics = UIFontMetrics(forTextStyle: textStyle) + font = fontMetrics.scaledFont(for: customFont) + } else { + font = UIFont.preferredFont(forTextStyle: textStyle) + } + } else { + font = UIFont.preferredFont(forTextStyle: textStyle) + } + + if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) { + font = UIFont(descriptor: italicDescriptor, size: 0) + } + if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) { + font = UIFont(descriptor: boldDescriptor, size: 0) + } + + return font + + } + + func color( for line : SwiftyLine ) -> UIColor { + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1, .previousH1: + return h1.color + case .h2, .previousH2: + return h2.color + case .h3: + return h3.color + case .h4: + return h4.color + case .h5: + return h5.color + case .h6: + return h6.color + case .body: + return body.color + case .codeblock: + return code.color + case .blockquote: + return blockquotes.color + case .unorderedList: + return body.color + } + } + + func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString { + + var finalTokens = tokens + let finalAttributedString = NSMutableAttributedString() + var attributes : [NSAttributedString.Key : AnyObject] = [:] + + + let lineProperties : LineProperties + switch line.lineStyle as! MarkdownLineStyle { + case .h1: + lineProperties = self.h1 + case .h2: + lineProperties = self.h2 + case .h3: + lineProperties = self.h3 + case .h4: + lineProperties = self.h4 + case .h5: + lineProperties = self.h5 + case .h6: + lineProperties = self.h6 + + case .codeblock: + lineProperties = body + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 20.0 + attributes[.paragraphStyle] = paragraphStyle + case .blockquote: + lineProperties = self.blockquotes + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 20.0 + attributes[.paragraphStyle] = paragraphStyle + case .unorderedList: + lineProperties = body + finalTokens.insert(Token(type: .string, inputString: "・ "), at: 0) + default: + lineProperties = body + break + } + + if lineProperties.alignment != .left { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = lineProperties.alignment + attributes[.paragraphStyle] = paragraphStyle + } + + + for token in finalTokens { + attributes[.font] = self.font(for: line) + attributes[.foregroundColor] = self.color(for: line) + guard let styles = token.characterStyles as? [CharacterStyle] else { + continue + } + if styles.contains(.italic) { + attributes[.font] = self.font(for: line, characterOverride: .italic) + attributes[.foregroundColor] = self.italic.color + } + if styles.contains(.bold) { + attributes[.font] = self.font(for: line, characterOverride: .bold) + attributes[.foregroundColor] = self.bold.color + } + + if styles.contains(.link), let url = token.metadataString { + attributes[.foregroundColor] = self.link.color + attributes[.font] = self.font(for: line, characterOverride: .link) + attributes[.link] = url as AnyObject + + if underlineLinks { + attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject + } + } + + if styles.contains(.image), let imageName = token.metadataString { + let image1Attachment = NSTextAttachment() + image1Attachment.image = UIImage(named: imageName) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + continue + } + + if styles.contains(.code) { + attributes[.foregroundColor] = self.code.color + attributes[.font] = self.font(for: line, characterOverride: .code) + } else { + // Switch back to previous font + } + let str = NSAttributedString(string: token.outputString, attributes: attributes) + finalAttributedString.append(str) + } + + return finalAttributedString + } +} diff --git a/Playground/SwiftyMarkdown.playground/contents.xcplayground b/Playground/SwiftyMarkdown.playground/contents.xcplayground new file mode 100644 index 0000000..6207e16 --- /dev/null +++ b/Playground/SwiftyMarkdown.playground/contents.xcplayground @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index faf2f27..d34aaad 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,12 @@ label.attributedText = md.attributedString() - Up to three levels - Neat! + 1. Ordered + 1. Lists + 1. Including indented lists + - Up to three levels + + Compound rules also work, for example: diff --git a/Resources/metadataTest.md b/Resources/metadataTest.md new file mode 100644 index 0000000..6d182c0 --- /dev/null +++ b/Resources/metadataTest.md @@ -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. diff --git a/Resources/test.md b/Resources/test.md new file mode 100644 index 0000000..276e9c1 --- /dev/null +++ b/Resources/test.md @@ -0,0 +1,24 @@ +# SwiftyMarkdown 1.0 + +SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use. + +## Fully Rebuilt For 2020! + +SwiftyMarkdown now features a more robust and reliable rules-based line processing and tokenisation engine. It has added support for images stored in the bundle (`![Image]()`), 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. + +1. A List +1. A second item in the list diff --git a/Sources/SwiftyMarkdown/String+SwiftyMarkdown.swift b/Sources/SwiftyMarkdown/String+SwiftyMarkdown.swift new file mode 100644 index 0000000..ca2c300 --- /dev/null +++ b/Sources/SwiftyMarkdown/String+SwiftyMarkdown.swift @@ -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(of string: S, options: String.CompareOptions = []) -> Index? { + range(of: string, options: options)?.lowerBound + } + func endIndex(of string: S, options: String.CompareOptions = []) -> Index? { + range(of: string, options: options)?.upperBound + } + func indices(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(of string: S, options: String.CompareOptions = []) -> [Range] { + var result: [Range] = [] + 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 + } +} diff --git a/Sources/SwiftyMarkdown/SwiftyLineProcessor.swift b/Sources/SwiftyMarkdown/SwiftyLineProcessor.swift new file mode 100644 index 0000000..586e097 --- /dev/null +++ b/Sources/SwiftyMarkdown/SwiftyLineProcessor.swift @@ -0,0 +1,229 @@ +// +// SwiftyLineProcessor.swift +// SwiftyMarkdown +// +// Created by Simon Fairbairn on 16/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// + +import Foundation + +public protocol LineStyling { + var shouldTokeniseLine : Bool { get } + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? +} + +public struct SwiftyLine : CustomStringConvertible { + public let line : String + public let lineStyle : LineStyling + public var description: String { + return self.line + } +} + +extension SwiftyLine : Equatable { + public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool { + return lhs.line == rhs.line + } +} + +public enum Remove { + case leading + case trailing + case both + case entireLine + case none +} + +public enum ChangeApplication { + case current + case previous + case untilClose +} + +public struct 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] + + 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.. 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.. SwiftyLine? { + if text.isEmpty, let style = processEmptyStrings { + return SwiftyLine(line: "", lineStyle: style) + } + let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous }) + + for element in lineRules { + guard element.token.count > 0 else { + continue + } + var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text + let unprocessed = output + + if let hasToken = self.closeToken, unprocessed != hasToken { + return nil + } + + switch element.removeFrom { + case .leading: + output = findLeadingLineElement(element, in: output) + case .trailing: + output = findTrailingLineElement(element, in: output) + case .both: + output = findLeadingLineElement(element, in: output) + output = findTrailingLineElement(element, in: output) + case .entireLine: + let maybeOutput = output.replacingOccurrences(of: element.token, with: "") + output = ( maybeOutput.isEmpty ) ? maybeOutput : output + default: + break + } + // Only if the output has changed in some way + guard unprocessed != output else { + continue + } + if element.changeAppliesTo == .untilClose { + self.closeToken = (self.closeToken == nil) ? element.token : nil + return nil + } + + + + output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output + return SwiftyLine(line: output, lineStyle: element.type) + + } + + for element in previousLines { + let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text + let charSet = CharacterSet(charactersIn: element.token ) + if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) { + return SwiftyLine(line: "", lineStyle: element.type) + } + } + + return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType) + } + + 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] = [] + + var lines = string.components(separatedBy: CharacterSet.newlines) + lines = self.processFrontMatter(lines) + + 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) + } + return foundAttributes + } + +} + + diff --git a/Sources/SwiftyMarkdown/SwiftyMarkdown+iOS.swift b/Sources/SwiftyMarkdown/SwiftyMarkdown+iOS.swift new file mode 100644 index 0000000..6c6a8c0 --- /dev/null +++ b/Sources/SwiftyMarkdown/SwiftyMarkdown+iOS.swift @@ -0,0 +1,173 @@ +// +// 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: 0) + } + if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) { + font = UIFont(descriptor: boldDescriptor, size: 0) + } + + return font + + } + + func color( for line : SwiftyLine ) -> UIColor { + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .yaml: + return body.color + case .h1, .previousH1: + return h1.color + case .h2, .previousH2: + return h2.color + case .h3: + return h3.color + case .h4: + return h4.color + case .h5: + return h5.color + case .h6: + return h6.color + case .body: + return body.color + case .codeblock: + return code.color + case .blockquote: + return blockquotes.color + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: + return body.color + + } + } + +} +#endif diff --git a/Sources/SwiftyMarkdown/SwiftyMarkdown+macOS.swift b/Sources/SwiftyMarkdown/SwiftyMarkdown+macOS.swift new file mode 100644 index 0000000..158b1a7 --- /dev/null +++ b/Sources/SwiftyMarkdown/SwiftyMarkdown+macOS.swift @@ -0,0 +1,144 @@ +// +// SwiftyMarkdown+macOS.swift +// SwiftyMarkdown +// +// Created by Simon Fairbairn on 17/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// + +import Foundation +#if os(macOS) +import AppKit + +extension SwiftyMarkdown { + + func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont { + var fontName : String? + var fontSize : CGFloat? + + var globalBold = false + var globalItalic = false + + let style : FontProperties + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1: + style = self.h1 + case .h2: + style = self.h2 + case .h3: + style = self.h3 + case .h4: + style = self.h4 + case .h5: + style = self.h5 + case .h6: + style = self.h6 + case .codeblock: + style = self.code + case .blockquote: + style = self.blockquotes + default: + style = self.body + } + + fontName = style.fontName + fontSize = style.fontSize + switch style.fontStyle { + case .bold: + globalBold = true + case .italic: + globalItalic = true + case .boldItalic: + globalItalic = true + globalBold = true + case .normal: + break + } + + if fontName == nil { + fontName = body.fontName + } + + if let characterOverride = characterOverride { + switch characterOverride { + case .code: + fontName = code.fontName ?? fontName + fontSize = code.fontSize + case .link: + fontName = link.fontName ?? fontName + fontSize = link.fontSize + case .bold: + fontName = bold.fontName ?? fontName + fontSize = bold.fontSize + globalBold = true + case .italic: + fontName = italic.fontName ?? fontName + fontSize = italic.fontSize + globalItalic = true + default: + break + } + } + + fontSize = fontSize == 0.0 ? nil : fontSize + let finalSize : CGFloat + if let existentFontSize = fontSize { + finalSize = existentFontSize + } else { + finalSize = NSFont.systemFontSize + } + var font : NSFont + if let existentFontName = fontName { + if let customFont = NSFont(name: existentFontName, size: finalSize) { + font = customFont + } else { + font = NSFont.systemFont(ofSize: finalSize) + } + } else { + font = NSFont.systemFont(ofSize: finalSize) + } + + if globalItalic { + let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic) + font = NSFont(descriptor: italicDescriptor, size: 0) ?? font + } + if globalBold { + let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold) + font = NSFont(descriptor: boldDescriptor, size: 0) ?? font + } + + return font + + } + + func color( for line : SwiftyLine ) -> NSColor { + // What type are we and is there a font name set? + switch line.lineStyle as! MarkdownLineStyle { + case .h1, .previousH1: + return h1.color + case .h2, .previousH2: + return h2.color + case .h3: + return h3.color + case .h4: + return h4.color + case .h5: + return h5.color + case .h6: + return h6.color + case .body: + return body.color + case .codeblock: + return code.color + case .blockquote: + return blockquotes.color + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: + return body.color + case .yaml: + return body.color + } + } + +} +#endif diff --git a/Sources/SwiftyMarkdown/SwiftyMarkdown.swift b/Sources/SwiftyMarkdown/SwiftyMarkdown.swift new file mode 100644 index 0000000..7e5c980 --- /dev/null +++ b/Sources/SwiftyMarkdown/SwiftyMarkdown.swift @@ -0,0 +1,545 @@ +// +// SwiftyMarkdown.swift +// SwiftyMarkdown +// +// Created by Simon Fairbairn on 05/03/2016. +// Copyright © 2016 Voyage Travel Apps. All rights reserved. +// + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +enum CharacterStyle : CharacterStyling { + case none + case bold + case italic + case code + case link + case image + case strikethrough + + 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 + + + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? { + switch self { + case .previousH1: + return MarkdownLineStyle.h1 + case .previousH2: + return MarkdownLineStyle.h2 + default : + return nil + } + } +} + +@objc public enum FontStyle : Int { + case normal + case bold + case italic + case boldItalic +} + +#if os(macOS) +@objc public protocol FontProperties { + var fontName : String? { get set } + var color : NSColor { get set } + var fontSize : CGFloat { get set } + var fontStyle : FontStyle { get set } +} +#else +@objc public protocol FontProperties { + var fontName : String? { get set } + var color : UIColor { get set } + var fontSize : CGFloat { get set } + var fontStyle : FontStyle { get set } +} +#endif + + +@objc public protocol LineProperties { + var alignment : NSTextAlignment { get set } +} + + +/** +A 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 +} + + + +/// 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 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(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1), + CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1), + CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining), + CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2), + CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3), + CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) + ] + + static public var frontMatterRules = [ + FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":") + ] + + 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 = 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() + + open var strikethrough = BasicStyles() + + public var bullet : String = "・" + + public var underlineLinks : Bool = false + + public var frontMatterAttributes : [String : String] { + get { + return self.lineProcessor.frontMatterAttributes + } + } + + var currentType : MarkdownLineStyle = .body + + + var string : String + + let tagList = "!\\_*`[]()" + let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()") + + var orderedListCount = 0 + var orderedListIndentFirstOrderCount = 0 + var orderedListIndentSecondOrderCount = 0 + + + /** + + - 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 { + if let existentMarkdownString = markdownString { + self.string = existentMarkdownString + } + let attributedString = NSMutableAttributedString(string: "") + self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body + let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string) + + for (idx, line) in foundAttributes.enumerated() { + if idx > 0 { + attributedString.append(NSAttributedString(string: "\n")) + } + let finalTokens = self.tokeniser.process(line.line) + attributedString.append(attributedStringFor(tokens: finalTokens, in: line)) + + } + 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 + } + + 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[.link] = nil + attributes[.strikethroughStyle] = nil + attributes[.foregroundColor] = self.color(for: line) + guard let styles = token.characterStyles as? [CharacterStyle] else { + continue + } + if styles.contains(.italic) { + attributes[.font] = self.font(for: line, characterOverride: .italic) + attributes[.foregroundColor] = self.italic.color + } + if styles.contains(.bold) { + attributes[.font] = self.font(for: line, characterOverride: .bold) + attributes[.foregroundColor] = self.bold.color + } + + 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(.strikethrough) { + attributes[.font] = self.font(for: line, characterOverride: .strikethrough) + attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject + attributes[.foregroundColor] = self.strikethrough.color + } + + #if !os(watchOS) + if styles.contains(.image), let imageName = token.metadataString { + #if !os(macOS) + let image1Attachment = NSTextAttachment() + image1Attachment.image = UIImage(named: imageName) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + #elseif !os(watchOS) + let image1Attachment = NSTextAttachment() + image1Attachment.image = NSImage(named: imageName) + let str = NSAttributedString(attachment: image1Attachment) + finalAttributedString.append(str) + #endif + continue + } + #endif + + if styles.contains(.code) { + attributes[.foregroundColor] = self.code.color + attributes[.font] = self.font(for: line, characterOverride: .code) + } else { + // Switch back to previous font + } + let str = NSAttributedString(string: token.outputString, attributes: attributes) + finalAttributedString.append(str) + } + + return finalAttributedString + } +} diff --git a/Sources/SwiftyMarkdown/SwiftyTokeniser.swift b/Sources/SwiftyMarkdown/SwiftyTokeniser.swift new file mode 100644 index 0000000..8229f79 --- /dev/null +++ b/Sources/SwiftyMarkdown/SwiftyTokeniser.swift @@ -0,0 +1,821 @@ +// +// SwiftyTokeniser.swift +// SwiftyMarkdown +// +// Created by Simon Fairbairn on 16/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// +import Foundation +import os.log + +extension OSLog { + private static var subsystem = "SwiftyTokeniser" + static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising") + static let styling = OSLog(subsystem: subsystem, category: "Styling") +} + +// Tag definition +public protocol CharacterStyling { + func isEqualTo( _ other : CharacterStyling ) -> Bool +} + +public enum SpaceAllowed { + case no + case bothSides + case oneSide + case leadingSide + case trailingSide +} + +public enum Cancel { + case none + case allRemaining + case currentSet +} + +public struct CharacterRule : CustomStringConvertible { + public let openTag : String + public let intermediateTag : String? + public let closingTag : String? + public let escapeCharacter : Character? + public let styles : [Int : [CharacterStyling]] + public var minTags : Int = 1 + public var maxTags : Int = 1 + public var spacesAllowed : SpaceAllowed = .oneSide + public var cancels : Cancel = .none + + public var description: String { + return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) " + } + + public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], minTags : Int = 1, maxTags : Int = 1, cancels : Cancel = .none) { + self.openTag = openTag + self.intermediateTag = intermediateTag + self.closingTag = closingTag + self.escapeCharacter = escapeCharacter + self.styles = styles + self.minTags = minTags + self.maxTags = maxTags + self.cancels = cancels + } +} + +// Token definition +public enum TokenType { + case repeatingTag + case openTag + case intermediateTag + case closeTag + case string + case escape + case replacement +} + + + +public struct Token { + public let id = UUID().uuidString + public let type : TokenType + public let inputString : String + public fileprivate(set) var metadataString : String? = nil + public fileprivate(set) var characterStyles : [CharacterStyling] = [] + public fileprivate(set) var count : Int = 0 + public fileprivate(set) var shouldSkip : Bool = false + public fileprivate(set) var tokenIndex : Int = -1 + public fileprivate(set) var isProcessed : Bool = false + public fileprivate(set) var isMetadata : Bool = false + public var outputString : String { + get { + switch self.type { + case .repeatingTag: + if count <= 0 { + return "" + } else { + let range = inputString.startIndex.. Token { + var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles) + newToken.metadataString = self.metadataString + newToken.isMetadata = self.isMetadata + newToken.isProcessed = self.isProcessed + return newToken + } +} + +extension Sequence where Iterator.Element == Token { + var oslogDisplay: String { + return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]" + } +} + +public class SwiftyTokeniser { + let rules : [CharacterRule] + var replacements : [String : [Token]] = [:] + + var enableLog = (ProcessInfo.processInfo.environment["SwiftyTokeniserLogging"] != nil) + + public init( with rules : [CharacterRule] ) { + self.rules = rules + } + + + /// This goes through every CharacterRule in order and applies it to the input string, tokenising the string + /// if there are any matches. + /// + /// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to + /// those outside them. + /// + /// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately. + /// + /// This is to prevent situations like **\[link**\](url) from returing a bold string. + /// + /// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to + public func process( _ inputString : String ) -> [Token] { + guard rules.count > 0 else { + return [Token(type: .string, inputString: inputString)] + } + + var currentTokens : [Token] = [] + var mutableRules = self.rules + + + + while !mutableRules.isEmpty { + let nextRule = mutableRules.removeFirst() + + if enableLog { + os_log("------------------------------", log: .tokenising, type: .info) + os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description) + } + + if currentTokens.isEmpty { + // This means it's the first time through + currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule) + continue + } + + var outerStringTokens : [Token] = [] + var innerStringTokens : [Token] = [] + var isOuter = true + for idx in 0.. [Token] { + guard !stringToken.outputString.isEmpty && !replacements.isEmpty else { + return [stringToken] + } + var outputTokens : [Token] = [] + let scanner = Scanner(string: stringToken.outputString) + scanner.charactersToBeSkipped = nil + + // Remove any replacements that don't appear in the incoming string + var repTokens = replacements.filter({ stringToken.outputString.contains($0.inputString) }) + + var testString = "\n" + while !scanner.isAtEnd { + var outputString : String = "" + if repTokens.count > 0 { + testString = repTokens.removeFirst().inputString + } + + if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) { + if let nextString = scanner.scanUpToString(testString) { + outputString = nextString + outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false)) + if let outputToken = scanner.scanString(testString) { + outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true)) + } + } else if let outputToken = scanner.scanString(testString) { + outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true)) + } + } else { + var oldString : NSString? = nil + var tokenString : NSString? = nil + scanner.scanUpTo(testString, into: &oldString) + if let nextString = oldString { + outputString = nextString as String + outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false)) + scanner.scanString(testString, into: &tokenString) + if let outputToken = tokenString as String? { + outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true)) + } + } else { + scanner.scanString(testString, into: &tokenString) + if let outputToken = tokenString as String? { + outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true)) + } + } + } + } + return outputTokens + } + + + /// This function is necessary because a previously tokenised string might have + /// + /// Consider a previously tokenised string, where AAAAA-BBBBB-CCCCC represents a replaced \[link\](url) instance. + /// + /// The incoming tokens will look like this: + /// + /// - `string`: "A \*\*Bold" + /// - `replacement` : "AAAAA-BBBBB-CCCCC" + /// - `string`: " with a trailing string**" + /// + /// However, because the scanner can only tokenise individual strings, passing in the string values + /// of these tokens individually and applying the styles will not correctly detect the starting and + /// ending `repeatingTag` instances. (e.g. the scanner will see "A \*\*Bold", and then "AAAAA-BBBBB-CCCCC", + /// and finally " with a trailing string\*\*") + /// + /// The strings need to be combined, so that they form a single string: + /// A \*\*Bold AAAAA-BBBBB-CCCCC with a trailing string\*\*. + /// This string is then parsed and tokenised so that it looks like this: + /// + /// - `string`: "A " + /// - `repeatingTag`: "\*\*" + /// - `string`: "Bold AAAAA-BBBBB-CCCCC with a trailing string" + /// - `repeatingTag`: "\*\*" + /// + /// Finally, the replacements from the original incoming token array are searched for and pulled out + /// of this new string, so the final result looks like this: + /// + /// - `string`: "A " + /// - `repeatingTag`: "\*\*" + /// - `string`: "Bold " + /// - `replacement`: "AAAAA-BBBBB-CCCCC" + /// - `string`: " with a trailing string" + /// - `repeatingTag`: "\*\*" + /// + /// - Parameters: + /// - tokens: The tokens to be combined, scanned, re-tokenised, and merged + /// - rule: The character rule currently being applied + func scanReplacementTokens( _ tokens : [Token], with rule : CharacterRule ) -> [Token] { + guard tokens.count > 0 else { + return [] + } + + let combinedString = tokens.map({ $0.outputString }).joined() + + let nextTokens = self.scan(combinedString, with: rule) + var replacedTokens = self.applyStyles(to: nextTokens, usingRule: rule) + + /// It's necessary here to check to see if the first token (which will always represent the styles + /// to be applied from previous scans) has any existing metadata or character styles and apply them + /// to *all* the string and replacement tokens found by the new scan. + for idx in 0.. [Token] { + + // Only combine string and replacements that are next to each other. + var newTokenSet : [Token] = [] + var currentTokenSet : [Token] = [] + for i in 0.. 0 else { + return + } + + let startIdx = index + var endIdx : Int? = nil + + let maxCount = (theToken.count > rule.maxTags) ? rule.maxTags : theToken.count + // Try to find exact match first + if let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id && !$0.isProcessed }) { + endIdx = nextTokenIdx + } + + if endIdx == nil, let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count >= 1 && $0.id != theToken.id && !$0.isProcessed }) { + endIdx = nextTokenIdx + } + guard let existentEnd = endIdx else { + return + } + + + let styles : [CharacterStyling] = rule.styles[maxCount] ?? [] + for i in startIdx.. rule.maxTags) ? rule.maxTags : tokens[existentEnd].count + tokens[index].count = theToken.count - maxEnd + tokens[existentEnd].count = tokens[existentEnd].count - maxEnd + if maxEnd < rule.maxTags { + self.handleClosingTagFromRepeatingTag(withIndex: index, in: &tokens, following: rule) + } else { + tokens[existentEnd].isProcessed = true + tokens[index].isProcessed = true + } + + + } + + func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] { + var mutableTokens : [Token] = tokens + + if enableLog { + os_log("Applying styles to tokens: %@", log: .tokenising, type: .info, tokens.oslogDisplay ) + } + for idx in 0.. [Token] { + let scanner = Scanner(string: string) + scanner.charactersToBeSkipped = nil + var tokens : [Token] = [] + var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")") + if let existentEscape = rule.escapeCharacter { + set.insert(charactersIn: String(existentEscape)) + } + + var openTagFound = false + var openingString = "" + while !scanner.isAtEnd { + + if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) { + if let start = scanner.scanUpToCharacters(from: set) { + openingString.append(start) + } + } else { + var string : NSString? + scanner.scanUpToCharacters(from: set, into: &string) + if let existentString = string as String? { + openingString.append(existentString) + } + // Fallback on earlier versions + } + + let lastChar : String? + if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) { + lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex).. string.startIndex ) ? String(string[string.index(before: scanLocation)..= rule.minTags else { + return + } + + if !openingString.isEmpty { + tokens.append(Token(type: .string, inputString: "\(openingString)")) + openingString = "" + } + let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type + + var token = Token(type: actualType, inputString: inputString) + if rule.closingTag == nil { + token.count = inputString.count + } + + tokens.append(token) + + switch type { + case .openTag: + openString = "" + case .intermediateTag: + intermediateString = "" + case .closeTag: + closedString = "" + default: + break + } + } + + // Here I am going through and adding the characters in the found set to a cumulative string. + // If there is an escape character, then the loop stops and any open tags are tokenised. + for char in foundChars { + cumulativeString.append(char) + if maybeEscapeNext { + + var escaped = cumulativeString + if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag { + escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: "")) + } + + openingString.append(escaped) + cumulativeString = "" + maybeEscapeNext = false + } + if let existentEscape = rule.escapeCharacter { + if cumulativeString == String(existentEscape) { + maybeEscapeNext = true + addToken(for: .openTag) + addToken(for: .intermediateTag) + addToken(for: .closeTag) + continue + } + } + + + if cumulativeString == rule.openTag { + openString.append(char) + cumulativeString = "" + openTagFound = true + } else if cumulativeString == rule.intermediateTag, openTagFound { + intermediateString.append(cumulativeString) + cumulativeString = "" + } else if cumulativeString == rule.closingTag, openTagFound { + closedString.append(char) + cumulativeString = "" + openTagFound = false + } + } + // If we're here, it means that an escape character was found but without a corresponding + // tag, which means it might belong to a different rule. + // It should be added to the next group of regular characters + + addToken(for: .openTag) + addToken(for: .intermediateTag) + addToken(for: .closeTag) + openingString.append( cumulativeString ) + } + + if !openingString.isEmpty { + tokens.append(Token(type: .string, inputString: "\(openingString)")) + } + + return tokens + } + + func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool { + switch rule.spacesAllowed { + case .leadingSide: + guard nextCharacter != nil else { + return true + } + if nextCharacter == " " { + return false + } + case .trailingSide: + guard previousCharacter != nil else { + return true + } + if previousCharacter == " " { + return false + } + case .no: + switch (previousCharacter, nextCharacter) { + case (nil, nil), ( " ", _ ), ( _, " " ): + return false + default: + return true + } + + case .oneSide: + switch (previousCharacter, nextCharacter) { + case (nil, " " ), (" ", nil), (" ", " " ): + return false + default: + return true + } + default: + break + } + return true + } + +} diff --git a/SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist b/SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist new file mode 100644 index 0000000..7c23420 --- /dev/null +++ b/SwiftyMarkdown.xcodeproj/SwiftyMarkdownTests_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist b/SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist new file mode 100644 index 0000000..57ada9f --- /dev/null +++ b/SwiftyMarkdown.xcodeproj/SwiftyMarkdown_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/SwiftyMarkdown.xcodeproj/project.pbxproj b/SwiftyMarkdown.xcodeproj/project.pbxproj index 2ca03fc..3ecbae2 100644 --- a/SwiftyMarkdown.xcodeproj/project.pbxproj +++ b/SwiftyMarkdown.xcodeproj/project.pbxproj @@ -1,500 +1,720 @@ // !$*UTF8*$! { - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */; }; - F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */; }; - F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */; }; - F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */; }; - F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */; }; - F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */; }; - F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */; }; - F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */; }; - F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */; }; - F4A48ABB23A8521C00713437 /* test.md in Resources */ = {isa = PBXBuildFile; fileRef = F4A48ABA23A8518900713437 /* test.md */; }; - F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; }; - F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */; }; - F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */; }; - F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E61C8AF01300D735C1 /* LICENSE */; }; - F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F4CE98781C8A921300D735C1 /* Project object */; - proxyType = 1; - remoteGlobalIDString = F4CE98801C8A921300D735C1; - remoteInfo = SwiftyMarkdown; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = ""; }; - F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = ""; }; - F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = ""; }; - F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = ""; }; - F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = ""; }; - F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = ""; }; - F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = ""; }; - F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = ""; }; - F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdownAttributedStringTests copy.swift"; sourceTree = ""; }; - F4A48ABA23A8518900713437 /* test.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = test.md; sourceTree = ""; }; - 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 = ""; }; - F4CE98861C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = ""; }; - F4CE98921C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = ""; }; - F4CE98E61C8AF01300D735C1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - F4CE98E71C8AF01300D735C1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = ""; }; - F4FEF07423E13365007219EF /* metadataTest.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = metadataTest.md; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - F4CE987D1C8A921300D735C1 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CE98881C8A921300D735C1 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - F4CE98771C8A921300D735C1 = { - isa = PBXGroup; - children = ( - F4CE98831C8A921300D735C1 /* SwiftyMarkdown */, - F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */, - F4CE98821C8A921300D735C1 /* Products */, - F4CE98E61C8AF01300D735C1 /* LICENSE */, - F4CE98E71C8AF01300D735C1 /* README.md */, - F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */, - ); - sourceTree = ""; - }; - F4CE98821C8A921300D735C1 /* Products */ = { - isa = PBXGroup; - children = ( - F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */, - F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - F4CE98831C8A921300D735C1 /* SwiftyMarkdown */ = { - isa = PBXGroup; - children = ( - F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */, - F4CE98861C8A921300D735C1 /* Info.plist */, - F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */, - F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */, - F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */, - F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */, - F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */, - F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */, - ); - path = SwiftyMarkdown; - sourceTree = ""; - }; - F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */ = { - isa = PBXGroup; - children = ( - F4A48ABA23A8518900713437 /* test.md */, - F4FEF07423E13365007219EF /* metadataTest.md */, - F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */, - F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */, - F4CE98921C8A921300D735C1 /* Info.plist */, - F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */, - F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */, - F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */, - ); - path = SwiftyMarkdownTests; - sourceTree = ""; - }; -/* 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 */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4CE98951C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */; - buildPhases = ( - F4CE987C1C8A921300D735C1 /* Sources */, - F4CE987D1C8A921300D735C1 /* Frameworks */, - F4CE987E1C8A921300D735C1 /* Headers */, - F4CE987F1C8A921300D735C1 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SwiftyMarkdown; - productName = SwiftyMarkdown; - productReference = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; - productType = "com.apple.product-type.framework"; - }; - F4CE988A1C8A921300D735C1 /* SwiftyMarkdownTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4CE98981C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */; - buildPhases = ( - F4CE98871C8A921300D735C1 /* Sources */, - F4CE98881C8A921300D735C1 /* Frameworks */, - F4CE98891C8A921300D735C1 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F4CE988E1C8A921300D735C1 /* PBXTargetDependency */, - ); - name = SwiftyMarkdownTests; - productName = SwiftyMarkdownTests; - productReference = F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - F4CE98781C8A921300D735C1 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 1120; - ORGANIZATIONNAME = "Voyage Travel Apps"; - TargetAttributes = { - F4CE98801C8A921300D735C1 = { - CreatedOnToolsVersion = 7.2.1; - LastSwiftMigration = 1120; - }; - F4CE988A1C8A921300D735C1 = { - CreatedOnToolsVersion = 7.2.1; - DevelopmentTeam = 52T262DA8V; - LastSwiftMigration = 1120; - }; - }; - }; - buildConfigurationList = F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = F4CE98771C8A921300D735C1; - productRefGroup = F4CE98821C8A921300D735C1 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - F4CE98801C8A921300D735C1 /* SwiftyMarkdown */, - F4CE988A1C8A921300D735C1 /* SwiftyMarkdownTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - F4CE987F1C8A921300D735C1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */, - F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CE98891C8A921300D735C1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4A48ABB23A8521C00713437 /* test.md in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - F4CE987C1C8A921300D735C1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */, - F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */, - F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */, - F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */, - F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */, - F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CE98871C8A921300D735C1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */, - F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */, - F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */, - F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */, - F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - F4CE988E1C8A921300D735C1 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F4CE98801C8A921300D735C1 /* SwiftyMarkdown */; - targetProxy = F4CE988D1C8A921300D735C1 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - F4CE98931C8A921300D735C1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 19; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - F4CE98941C8A921300D735C1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 19; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - F4CE98961C8A921300D735C1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BITCODE_GENERATION_MODE = bitcode; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 19; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SwiftyMarkdown/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_CFLAGS = "-fembed-bitcode"; - PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - F4CE98971C8A921300D735C1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BITCODE_GENERATION_MODE = bitcode; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 19; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = SwiftyMarkdown/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_CFLAGS = "-fembed-bitcode"; - PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - F4CE98991C8A921300D735C1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = 52T262DA8V; - INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - F4CE989A1C8A921300D735C1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = 52T262DA8V; - INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4CE98931C8A921300D735C1 /* Debug */, - F4CE98941C8A921300D735C1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F4CE98951C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdown" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4CE98961C8A921300D735C1 /* Debug */, - F4CE98971C8A921300D735C1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F4CE98981C8A921300D735C1 /* Build configuration list for PBXNativeTarget "SwiftyMarkdownTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4CE98991C8A921300D735C1 /* Debug */, - F4CE989A1C8A921300D735C1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = F4CE98781C8A921300D735C1 /* Project object */; + archiveVersion = "1"; + objectVersion = "46"; + objects = { + "OBJ_1" = { + isa = "PBXProject"; + attributes = { + LastSwiftMigration = "9999"; + LastUpgradeCheck = "9999"; + }; + buildConfigurationList = "OBJ_2"; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = "en"; + hasScannedForEncodings = "0"; + knownRegions = ( + "en" + ); + mainGroup = "OBJ_5"; + productRefGroup = "OBJ_23"; + projectDirPath = "."; + targets = ( + "SwiftyMarkdown::SwiftyMarkdown", + "SwiftyMarkdown::SwiftPMPackageDescription", + "SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget", + "SwiftyMarkdown::SwiftyMarkdownTests" + ); + }; + "OBJ_10" = { + isa = "PBXFileReference"; + path = "SwiftyLineProcessor.swift"; + sourceTree = ""; + }; + "OBJ_11" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdown+iOS.swift"; + sourceTree = ""; + }; + "OBJ_12" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdown+macOS.swift"; + sourceTree = ""; + }; + "OBJ_13" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdown.swift"; + sourceTree = ""; + }; + "OBJ_14" = { + isa = "PBXFileReference"; + path = "SwiftyTokeniser.swift"; + sourceTree = ""; + }; + "OBJ_15" = { + isa = "PBXGroup"; + children = ( + "OBJ_16" + ); + name = "Tests"; + path = ""; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_16" = { + isa = "PBXGroup"; + children = ( + "OBJ_17", + "OBJ_18", + "OBJ_19", + "OBJ_20", + "OBJ_21", + "OBJ_22" + ); + name = "SwiftyMarkdownTests"; + path = "Tests/SwiftyMarkdownTests"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_17" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownAttributedStringTests copy.swift"; + sourceTree = ""; + }; + "OBJ_18" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownCharacterTests.swift"; + sourceTree = ""; + }; + "OBJ_19" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownLineTests.swift"; + sourceTree = ""; + }; + "OBJ_2" = { + isa = "XCConfigurationList"; + buildConfigurations = ( + "OBJ_3", + "OBJ_4" + ); + defaultConfigurationIsVisible = "0"; + defaultConfigurationName = "Release"; + }; + "OBJ_20" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownLinkTests.swift"; + sourceTree = ""; + }; + "OBJ_21" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownPerformanceTests.swift"; + sourceTree = ""; + }; + "OBJ_22" = { + isa = "PBXFileReference"; + path = "XCTest+SwiftyMarkdown.swift"; + sourceTree = ""; + }; + "OBJ_23" = { + isa = "PBXGroup"; + children = ( + "SwiftyMarkdown::SwiftyMarkdown::Product", + "SwiftyMarkdown::SwiftyMarkdownTests::Product" + ); + name = "Products"; + path = ""; + sourceTree = "BUILT_PRODUCTS_DIR"; + }; + "OBJ_26" = { + isa = "PBXFileReference"; + path = "Playground"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_27" = { + isa = "PBXFileReference"; + path = "Example"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_28" = { + isa = "PBXFileReference"; + path = "Resources"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_29" = { + isa = "PBXFileReference"; + path = "fastlane"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_3" = { + isa = "XCBuildConfiguration"; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = "YES"; + COMBINE_HIDPI_IMAGES = "YES"; + COPY_PHASE_STRIP = "NO"; + DEBUG_INFORMATION_FORMAT = "dwarf"; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = "YES"; + GCC_OPTIMIZATION_LEVEL = "0"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "SWIFT_PACKAGE=1", + "DEBUG=1" + ); + MACOSX_DEPLOYMENT_TARGET = "10.10"; + ONLY_ACTIVE_ARCH = "YES"; + 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"; + USE_HEADERMAP = "NO"; + }; + name = "Debug"; + }; + "OBJ_30" = { + isa = "PBXFileReference"; + path = "LICENSE"; + sourceTree = ""; + }; + "OBJ_31" = { + isa = "PBXFileReference"; + path = "README.md"; + sourceTree = ""; + }; + "OBJ_32" = { + isa = "PBXFileReference"; + path = "Gemfile"; + sourceTree = ""; + }; + "OBJ_33" = { + isa = "PBXFileReference"; + path = "Gemfile.lock"; + sourceTree = ""; + }; + "OBJ_34" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdown.podspec"; + sourceTree = ""; + }; + "OBJ_36" = { + isa = "XCConfigurationList"; + buildConfigurations = ( + "OBJ_37", + "OBJ_38" + ); + defaultConfigurationIsVisible = "0"; + defaultConfigurationName = "Release"; + }; + "OBJ_37" = { + isa = "XCBuildConfiguration"; + buildSettings = { + 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" = { + isa = "XCBuildConfiguration"; + buildSettings = { + 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_39" = { + isa = "PBXSourcesBuildPhase"; + files = ( + "OBJ_40", + "OBJ_41", + "OBJ_42", + "OBJ_43", + "OBJ_44", + "OBJ_45" + ); + }; + "OBJ_4" = { + isa = "XCBuildConfiguration"; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = "YES"; + COMBINE_HIDPI_IMAGES = "YES"; + COPY_PHASE_STRIP = "YES"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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"; + }; + "OBJ_40" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_9"; + }; + "OBJ_41" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_10"; + }; + "OBJ_42" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_11"; + }; + "OBJ_43" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_12"; + }; + "OBJ_44" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_13"; + }; + "OBJ_45" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_14"; + }; + "OBJ_46" = { + isa = "PBXFrameworksBuildPhase"; + files = ( + ); + }; + "OBJ_48" = { + isa = "XCConfigurationList"; + buildConfigurations = ( + "OBJ_49", + "OBJ_50" + ); + defaultConfigurationIsVisible = "0"; + defaultConfigurationName = "Release"; + }; + "OBJ_49" = { + isa = "XCBuildConfiguration"; + buildSettings = { + 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"; + }; + "OBJ_5" = { + isa = "PBXGroup"; + children = ( + "OBJ_6", + "OBJ_7", + "OBJ_15", + "OBJ_23", + "OBJ_26", + "OBJ_27", + "OBJ_28", + "OBJ_29", + "OBJ_30", + "OBJ_31", + "OBJ_32", + "OBJ_33", + "OBJ_34" + ); + path = ""; + sourceTree = ""; + }; + "OBJ_50" = { + isa = "XCBuildConfiguration"; + buildSettings = { + 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"; + }; + "OBJ_51" = { + isa = "PBXSourcesBuildPhase"; + files = ( + "OBJ_52" + ); + }; + "OBJ_52" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_6"; + }; + "OBJ_54" = { + isa = "XCConfigurationList"; + buildConfigurations = ( + "OBJ_55", + "OBJ_56" + ); + defaultConfigurationIsVisible = "0"; + defaultConfigurationName = "Release"; + }; + "OBJ_55" = { + isa = "XCBuildConfiguration"; + buildSettings = { + }; + name = "Debug"; + }; + "OBJ_56" = { + isa = "XCBuildConfiguration"; + buildSettings = { + }; + name = "Release"; + }; + "OBJ_57" = { + isa = "PBXTargetDependency"; + target = "SwiftyMarkdown::SwiftyMarkdownTests"; + }; + "OBJ_59" = { + isa = "XCConfigurationList"; + buildConfigurations = ( + "OBJ_60", + "OBJ_61" + ); + defaultConfigurationIsVisible = "0"; + defaultConfigurationName = "Release"; + }; + "OBJ_6" = { + isa = "PBXFileReference"; + explicitFileType = "sourcecode.swift"; + path = "Package.swift"; + sourceTree = ""; + }; + "OBJ_60" = { + 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" = { + 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"; + }; + "OBJ_62" = { + isa = "PBXSourcesBuildPhase"; + files = ( + "OBJ_63", + "OBJ_64", + "OBJ_65", + "OBJ_66", + "OBJ_67", + "OBJ_68" + ); + }; + "OBJ_63" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_17"; + }; + "OBJ_64" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_18"; + }; + "OBJ_65" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_19"; + }; + "OBJ_66" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_20"; + }; + "OBJ_67" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_21"; + }; + "OBJ_68" = { + isa = "PBXBuildFile"; + fileRef = "OBJ_22"; + }; + "OBJ_69" = { + isa = "PBXFrameworksBuildPhase"; + files = ( + "OBJ_70" + ); + }; + "OBJ_7" = { + isa = "PBXGroup"; + children = ( + "OBJ_8" + ); + name = "Sources"; + path = ""; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_70" = { + isa = "PBXBuildFile"; + fileRef = "SwiftyMarkdown::SwiftyMarkdown::Product"; + }; + "OBJ_71" = { + isa = "PBXTargetDependency"; + target = "SwiftyMarkdown::SwiftyMarkdown"; + }; + "OBJ_8" = { + isa = "PBXGroup"; + children = ( + "OBJ_9", + "OBJ_10", + "OBJ_11", + "OBJ_12", + "OBJ_13", + "OBJ_14" + ); + name = "SwiftyMarkdown"; + path = "Sources/SwiftyMarkdown"; + sourceTree = "SOURCE_ROOT"; + }; + "OBJ_9" = { + isa = "PBXFileReference"; + path = "String+SwiftyMarkdown.swift"; + sourceTree = ""; + }; + "SwiftyMarkdown::SwiftPMPackageDescription" = { + isa = "PBXNativeTarget"; + buildConfigurationList = "OBJ_48"; + buildPhases = ( + "OBJ_51" + ); + dependencies = ( + ); + name = "SwiftyMarkdownPackageDescription"; + productName = "SwiftyMarkdownPackageDescription"; + productType = "com.apple.product-type.framework"; + }; + "SwiftyMarkdown::SwiftyMarkdown" = { + isa = "PBXNativeTarget"; + buildConfigurationList = "OBJ_36"; + buildPhases = ( + "OBJ_39", + "OBJ_46" + ); + dependencies = ( + ); + name = "SwiftyMarkdown"; + productName = "SwiftyMarkdown"; + productReference = "SwiftyMarkdown::SwiftyMarkdown::Product"; + productType = "com.apple.product-type.framework"; + }; + "SwiftyMarkdown::SwiftyMarkdown::Product" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdown.framework"; + sourceTree = "BUILT_PRODUCTS_DIR"; + }; + "SwiftyMarkdown::SwiftyMarkdownPackageTests::ProductTarget" = { + isa = "PBXAggregateTarget"; + buildConfigurationList = "OBJ_54"; + buildPhases = ( + ); + dependencies = ( + "OBJ_57" + ); + name = "SwiftyMarkdownPackageTests"; + productName = "SwiftyMarkdownPackageTests"; + }; + "SwiftyMarkdown::SwiftyMarkdownTests" = { + isa = "PBXNativeTarget"; + buildConfigurationList = "OBJ_59"; + buildPhases = ( + "OBJ_62", + "OBJ_69" + ); + dependencies = ( + "OBJ_71" + ); + name = "SwiftyMarkdownTests"; + productName = "SwiftyMarkdownTests"; + productReference = "SwiftyMarkdown::SwiftyMarkdownTests::Product"; + productType = "com.apple.product-type.bundle.unit-test"; + }; + "SwiftyMarkdown::SwiftyMarkdownTests::Product" = { + isa = "PBXFileReference"; + path = "SwiftyMarkdownTests.xctest"; + sourceTree = "BUILT_PRODUCTS_DIR"; + }; + }; + rootObject = "OBJ_1"; } diff --git a/SwiftyMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SwiftyMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 919434a..fe1aa71 100644 --- a/SwiftyMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/SwiftyMarkdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/SwiftyMarkdown.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/SwiftyMarkdown.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..a72dc2b --- /dev/null +++ b/SwiftyMarkdown.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + \ No newline at end of file diff --git a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/3EC61D89-CC18-40D5-9AC6-F4504ECE42B5.plist b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/3EC61D89-CC18-40D5-9AC6-F4504ECE42B5.plist deleted file mode 100644 index 75d5ea9..0000000 --- a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/3EC61D89-CC18-40D5-9AC6-F4504ECE42B5.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - classNames - - SwiftyMarkdownPerformanceTests - - testThatFilesAreProcessedQuickly() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 0.01346 - baselineIntegrationDisplayName - Local Baseline - - - testThatStringsAreProcessedQuickly() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 0.0064581 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/Info.plist b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/Info.plist deleted file mode 100644 index 5a2c60f..0000000 --- a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/Info.plist +++ /dev/null @@ -1,71 +0,0 @@ - - - - - runDestinationsByUUID - - 3EC61D89-CC18-40D5-9AC6-F4504ECE42B5 - - localComputer - - busSpeedInMHz - 400 - cpuCount - 1 - cpuKind - 8-Core Intel Core i9 - cpuSpeedInMHz - 2400 - logicalCPUCoresPerPackage - 16 - modelCode - MacBookPro16,1 - physicalCPUCoresPerPackage - 8 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - x86_64 - targetDevice - - modelCode - iPhone10,6 - platformIdentifier - com.apple.platform.iphonesimulator - - - 8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4 - - localComputer - - busSpeedInMHz - 400 - cpuCount - 1 - cpuKind - 8-Core Intel Core i9 - cpuSpeedInMHz - 2400 - logicalCPUCoresPerPackage - 16 - modelCode - MacBookPro16,1 - physicalCPUCoresPerPackage - 8 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - x86_64 - targetDevice - - modelCode - iPhone11,8 - platformIdentifier - com.apple.platform.iphonesimulator - - - - - diff --git a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4.plist b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/AD1DF83E-20BC-4E7E-8C14-683818ED0A26.plist similarity index 83% rename from SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4.plist rename to SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/AD1DF83E-20BC-4E7E-8C14-683818ED0A26.plist index a7cbb00..d0ef383 100644 --- a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/F4CE988A1C8A921300D735C1.xcbaseline/8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4.plist +++ b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/AD1DF83E-20BC-4E7E-8C14-683818ED0A26.plist @@ -11,11 +11,9 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.1 + 0.0217 baselineIntegrationDisplayName Local Baseline - maxPercentRelativeStandardDeviation - 5 testThatStringsAreProcessedQuickly() @@ -23,11 +21,9 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.01 + 0.016 baselineIntegrationDisplayName Local Baseline - maxPercentRelativeStandardDeviation - 5 testThatVeryLongStringsAreProcessedQuickly() @@ -35,7 +31,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.0392 + 0.016 baselineIntegrationDisplayName Local Baseline diff --git a/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/Info.plist b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/Info.plist new file mode 100644 index 0000000..4b4b577 --- /dev/null +++ b/SwiftyMarkdown.xcodeproj/xcshareddata/xcbaselines/SwiftyMarkdown::SwiftyMarkdownTests.xcbaseline/Info.plist @@ -0,0 +1,33 @@ + + + + + runDestinationsByUUID + + AD1DF83E-20BC-4E7E-8C14-683818ED0A26 + + localComputer + + busSpeedInMHz + 400 + cpuCount + 1 + cpuKind + 8-Core Intel Core i9 + cpuSpeedInMHz + 2400 + logicalCPUCoresPerPackage + 16 + modelCode + MacBookPro16,1 + physicalCPUCoresPerPackage + 8 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + + + + diff --git a/SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown.xcscheme b/SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown-Package.xcscheme similarity index 64% rename from SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown.xcscheme rename to SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown-Package.xcscheme index 34ce90f..3c5c431 100644 --- a/SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown.xcscheme +++ b/SwiftyMarkdown.xcodeproj/xcshareddata/xcschemes/SwiftyMarkdown-Package.xcscheme @@ -1,6 +1,6 @@ @@ -27,21 +27,12 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - @@ -59,15 +50,6 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - - - - - - - - diff --git a/SwiftyMarkdown/SwiftyMarkdown+iOS.swift b/SwiftyMarkdown/SwiftyMarkdown+iOS.swift index 0b616c8..6c6a8c0 100644 --- a/SwiftyMarkdown/SwiftyMarkdown+iOS.swift +++ b/SwiftyMarkdown/SwiftyMarkdown+iOS.swift @@ -163,7 +163,7 @@ extension SwiftyMarkdown { return code.color case .blockquote: return blockquotes.color - case .unorderedList, .indentedUnorderedListFirstOrder, .indentedUnorderedListSecondOrder: + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: return body.color } diff --git a/SwiftyMarkdown/SwiftyMarkdown+macOS.swift b/SwiftyMarkdown/SwiftyMarkdown+macOS.swift index 7fa4eca..158b1a7 100644 --- a/SwiftyMarkdown/SwiftyMarkdown+macOS.swift +++ b/SwiftyMarkdown/SwiftyMarkdown+macOS.swift @@ -133,7 +133,7 @@ extension SwiftyMarkdown { return code.color case .blockquote: return blockquotes.color - case .unorderedList: + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: return body.color case .yaml: return body.color diff --git a/SwiftyMarkdown/SwiftyMarkdown.swift b/SwiftyMarkdown/SwiftyMarkdown.swift index 4df3ca7..353e7f8 100644 --- a/SwiftyMarkdown/SwiftyMarkdown.swift +++ b/SwiftyMarkdown/SwiftyMarkdown.swift @@ -52,8 +52,12 @@ enum MarkdownLineStyle : LineStyling { case blockquote case codeblock case unorderedList - case indentedUnorderedListFirstOrder - case indentedUnorderedListSecondOrder + case unorderedListIndentFirstOrder + case unorderedListIndentSecondOrder + case orderedList + case orderedListIndentFirstOrder + case orderedListIndentSecondOrder + func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? { switch self { @@ -132,11 +136,11 @@ If that is not set, then the system default will be used. LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous), LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous), - LineRule(token: "\t\t- ", type: MarkdownLineStyle.indentedUnorderedListSecondOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t- ", type: MarkdownLineStyle.indentedUnorderedListFirstOrder, removeFrom: .leading, shouldTrim: false), + 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.indentedUnorderedListSecondOrder, removeFrom: .leading, shouldTrim: false), - LineRule(token: "\t* ", type: MarkdownLineStyle.indentedUnorderedListFirstOrder, removeFrom: .leading, shouldTrim: false), + 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: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false), @@ -153,7 +157,7 @@ If that is not set, then the system default will be used. CharacterRule(openTag: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1), CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1), CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining), - CharacterRule(openTag: "~~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.strikethrough]], maxTags: 1, cancels: .allRemaining), + CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2), CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3), CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3) ] @@ -221,6 +225,10 @@ If that is not set, then the system default will be used. let tagList = "!\\_*`[]()" let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()") + var orderedListCount = 0 + var orderedListIndentFirstOrderCount = 0 + var orderedListIndentSecondOrderCount = 0 + /** @@ -371,6 +379,22 @@ extension SwiftyMarkdown { let finalAttributedString = NSMutableAttributedString() var attributes : [NSAttributedString.Key : AnyObject] = [:] + var listItem = self.bullet + switch line.lineStyle as! MarkdownLineStyle { + case .orderedList: + self.orderedListCount += 1 + listItem = "\(self.orderedListCount)" + case .orderedListIndentFirstOrder: + self.orderedListIndentFirstOrderCount += 1 + listItem = "\(self.orderedListIndentFirstOrderCount)" + case .orderedListIndentSecondOrder: + self.orderedListIndentSecondOrderCount += 1 + listItem = "\(self.orderedListIndentSecondOrderCount)" + default: + self.orderedListCount = 0 + self.orderedListIndentFirstOrderCount = 0 + self.orderedListIndentSecondOrderCount = 0 + } let lineProperties : LineProperties switch line.lineStyle as! MarkdownLineStyle { @@ -386,7 +410,6 @@ extension SwiftyMarkdown { lineProperties = self.h5 case .h6: lineProperties = self.h6 - case .codeblock: lineProperties = body let paragraphStyle = NSMutableParagraphStyle() @@ -398,15 +421,19 @@ extension SwiftyMarkdown { paragraphStyle.firstLineHeadIndent = 20.0 paragraphStyle.headIndent = 20.0 attributes[.paragraphStyle] = paragraphStyle - case .unorderedList, .indentedUnorderedListFirstOrder, .indentedUnorderedListSecondOrder: + case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder: + + + + var addition : CGFloat = 20 var indent = "" switch line.lineStyle as! MarkdownLineStyle { - case .indentedUnorderedListFirstOrder: + case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder: addition = 40 indent = "\t" - case .indentedUnorderedListSecondOrder: + case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder: addition = 60 indent = "\t\t" default: @@ -421,7 +448,7 @@ extension SwiftyMarkdown { paragraphStyle.headIndent = addition attributes[.paragraphStyle] = paragraphStyle - finalTokens.insert(Token(type: .string, inputString: "\(indent)\(self.bullet)\t"), at: 0) + finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0) case .yaml: lineProperties = body diff --git a/SwiftyMarkdown/SwiftyTokeniser.swift b/SwiftyMarkdown/SwiftyTokeniser.swift index e84b2fd..05a9554 100644 --- a/SwiftyMarkdown/SwiftyTokeniser.swift +++ b/SwiftyMarkdown/SwiftyTokeniser.swift @@ -40,6 +40,7 @@ public struct CharacterRule : CustomStringConvertible { public let closingTag : String? public let escapeCharacter : Character? public let styles : [Int : [CharacterStyling]] + public var minTags : Int = 1 public var maxTags : Int = 1 public var spacesAllowed : SpaceAllowed = .oneSide public var cancels : Cancel = .none @@ -48,12 +49,13 @@ public struct CharacterRule : CustomStringConvertible { return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) " } - public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) { + public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], minTags : Int = 1, maxTags : Int = 1, cancels : Cancel = .none) { self.openTag = openTag self.intermediateTag = intermediateTag self.closingTag = closingTag self.escapeCharacter = escapeCharacter self.styles = styles + self.minTags = minTags self.maxTags = maxTags self.cancels = cancels } @@ -627,6 +629,11 @@ public class SwiftyTokeniser { continue } + if foundChars == rule.openTag && foundChars.count < rule.minTags { + openingString.append(foundChars) + continue + } + if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) { let escapeString = String("\(rule.escapeCharacter ?? Character(""))") var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag) @@ -663,6 +670,10 @@ public class SwiftyTokeniser { guard !inputString.isEmpty else { return } + guard inputString.count >= rule.minTags else { + return + } + if !openingString.isEmpty { tokens.append(Token(type: .string, inputString: "\(openingString)")) openingString = "" diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/AppDelegate.swift b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/AppDelegate.swift new file mode 100644 index 0000000..0c22e0e --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/AppDelegate.swift @@ -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 + } + + +} + diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2db2b1c --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000..da12ed9 --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Base.lproj/Main.storyboard @@ -0,0 +1,717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Info.plist b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Info.plist new file mode 100644 index 0000000..1d528d1 --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2020 Voyage Travel Apps. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/SwiftyMarkdownExample_macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/ViewController.swift b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/ViewController.swift new file mode 100644 index 0000000..f12b670 --- /dev/null +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample macOS/ViewController.swift @@ -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. + } + } + + +} + diff --git a/SwiftyMarkdownExample/SwiftyMarkdownExample.xcodeproj/project.pbxproj b/SwiftyMarkdownExample/SwiftyMarkdownExample.xcodeproj/project.pbxproj index 526cd7f..1ee9a85 100644 --- a/SwiftyMarkdownExample/SwiftyMarkdownExample.xcodeproj/project.pbxproj +++ b/SwiftyMarkdownExample/SwiftyMarkdownExample.xcodeproj/project.pbxproj @@ -8,6 +8,12 @@ /* 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 */; }; + F4B4A45A23E4E18800550249 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */; }; + F4B4A45B23E4E18800550249 /* SwiftyMarkdown.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98E01C8AEFE200D735C1 /* SwiftyMarkdown.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; @@ -51,6 +57,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + F4B4A45C23E4E18800550249 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + F4B4A45B23E4E18800550249 /* SwiftyMarkdown.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; F4CE98E41C8AEFF000D735C1 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -65,6 +82,13 @@ /* Begin PBXFileReference section */ F421DD951C8AF34F00B86D66 /* example.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = example.md; sourceTree = ""; }; + 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 = ""; }; + F4B4A44D23E4E17400550249 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + F4B4A44F23E4E17400550249 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F4B4A45223E4E17400550249 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F4B4A45423E4E17400550249 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F4B4A45523E4E17400550249 /* SwiftyMarkdownExample_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftyMarkdownExample_macOS.entitlements; sourceTree = ""; }; 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 = ""; }; F4CE98AD1C8AEF7D00D735C1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -82,6 +106,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + F4B4A44623E4E17400550249 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4B4A45A23E4E18800550249 /* SwiftyMarkdown.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F4CE98A51C8AEF7D00D735C1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -107,6 +139,26 @@ /* 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 = ""; + }; + F4B4A45923E4E18800550249 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; F4CE989F1C8AEF7D00D735C1 = { isa = PBXGroup; children = ( @@ -114,7 +166,9 @@ F4CE98AA1C8AEF7D00D735C1 /* SwiftyMarkdownExample */, F4CE98BF1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */, F4CE98CA1C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */, + F4B4A44A23E4E17400550249 /* SwiftyMarkdownExample macOS */, F4CE98A91C8AEF7D00D735C1 /* Products */, + F4B4A45923E4E18800550249 /* Frameworks */, ); sourceTree = ""; }; @@ -124,6 +178,7 @@ F4CE98A81C8AEF7D00D735C1 /* SwiftyMarkdownExample.app */, F4CE98BC1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests.xctest */, F4CE98C71C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests.xctest */, + F4B4A44923E4E17400550249 /* SwiftyMarkdownExample macOS.app */, ); name = Products; sourceTree = ""; @@ -172,6 +227,24 @@ /* 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" */; @@ -232,10 +305,15 @@ F4CE98A01C8AEF7D00D735C1 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0720; + LastSwiftUpdateCheck = 1130; LastUpgradeCheck = 1120; ORGANIZATIONNAME = "Voyage Travel Apps"; TargetAttributes = { + F4B4A44823E4E17400550249 = { + CreatedOnToolsVersion = 11.3.1; + DevelopmentTeam = 52T262DA8V; + ProvisioningStyle = Automatic; + }; F4CE98A71C8AEF7D00D735C1 = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 1120; @@ -274,6 +352,7 @@ F4CE98A71C8AEF7D00D735C1 /* SwiftyMarkdownExample */, F4CE98BB1C8AEF7D00D735C1 /* SwiftyMarkdownExampleTests */, F4CE98C61C8AEF7D00D735C1 /* SwiftyMarkdownExampleUITests */, + F4B4A44823E4E17400550249 /* SwiftyMarkdownExample macOS */, ); }; /* End PBXProject section */ @@ -296,6 +375,15 @@ /* 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; @@ -324,6 +412,15 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + F4B4A44523E4E17400550249 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4B4A44E23E4E17400550249 /* ViewController.swift in Sources */, + F4B4A44C23E4E17400550249 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F4CE98A41C8AEF7D00D735C1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -365,6 +462,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + F4B4A45123E4E17400550249 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F4B4A45223E4E17400550249 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; F4CE98AF1C8AEF7D00D735C1 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -384,6 +489,62 @@ /* 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_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_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 = { @@ -575,6 +736,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 = ( diff --git a/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift b/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift index bfa8099..15cde13 100644 --- a/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift +++ b/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift @@ -13,23 +13,17 @@ import XCTest class SwiftyMarkdownCharacterTests: XCTestCase { func testIsolatedCase() { - let challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [ - Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]), - Token(type: .string, inputString: " ", characterStyles: []), - Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: " ", characterStyles: []), - Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link]) + let 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: []) ]) let results = self.attempt(challenge) XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - } - - func testThatRegularTraitsAreParsedCorrectly() { + } + + func testThatBoldTraitsAreRecognised() { var challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [ Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]) ]) @@ -50,8 +44,8 @@ class SwiftyMarkdownCharacterTests: XCTestCase { XCTAssertEqual(results.foundStyles, results.expectedStyles) XCTAssertEqual(results.attributedString.string, challenge.output) - challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [ - Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code]) + challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [ + Token(type: .string, inputString: "**A normal string**", characterStyles: []) ]) results = self.attempt(challenge) XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) @@ -59,6 +53,47 @@ class SwiftyMarkdownCharacterTests: XCTestCase { XCTAssertEqual(results.foundStyles, results.expectedStyles) XCTAssertEqual(results.attributedString.string, challenge.output) + challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [ + Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [ + Token(type: .string, inputString: "*", characterStyles: []), + Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]), + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [ + Token(type: .string, inputString: "A string with one *", characterStyles: []), + Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatCodeTraitsAreRecognised() { + var challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [ + Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + challenge = TokenTest(input: "A string with `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]), @@ -70,41 +105,6 @@ class SwiftyMarkdownCharacterTests: XCTestCase { XCTAssertEqual(results.foundStyles, results.expectedStyles) XCTAssertEqual(results.attributedString.string, challenge.output) - challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [ - Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic]) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [ - Token(type: .string, inputString: "A string with ", characterStyles: []), - Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]), - Token(type: .string, inputString: " text", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [ - Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: "with a ", characterStyles: []), - Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: " ", characterStyles: []), - Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: " bold ", characterStyles: []), - Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.bold]) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - challenge = TokenTest(input: "`A code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [ Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]), Token(type: .string, inputString: " with multiple ", characterStyles: []), @@ -118,6 +118,57 @@ class SwiftyMarkdownCharacterTests: XCTestCase { XCTAssertEqual(results.foundStyles, results.expectedStyles) XCTAssertEqual(results.attributedString.string, challenge.output) + challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [ + Token(type: .string, inputString: "`A normal string`", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [ + Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A lonely backtick: `", output: "A lonely backtick: `", tokens: [ + Token(type: .string, inputString: "A lonely backtick: `", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatItalicTraitsAreParsedCorrectly() { + + var challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [ + Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [ + Token(type: .string, inputString: "A string with ", characterStyles: []), + Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " text", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "_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: []), @@ -133,6 +184,92 @@ class SwiftyMarkdownCharacterTests: XCTestCase { XCTAssertEqual(results.foundStyles, results.expectedStyles) XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [ + Token(type: .string, inputString: "_A normal string_", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [ + Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: """ + 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(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + } + + func testThatStrikethroughTraitsAreRecognised() { + var challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [ + Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]), + Token(type: .string, inputString: "A crossed-out string", characterStyles: []) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + 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) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + + challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [ + Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + + } + + func testThatMixedTraitsAreRecognised() { + + var challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [ + 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]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + challenge = TokenTest(input: "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [ Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]), Token(type: .string, inputString: ", ", characterStyles: []), @@ -227,84 +364,6 @@ class SwiftyMarkdownCharacterTests: XCTestCase { } - func testThatEscapedCharactersAreEscapedCorrectly() { - var challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [ - Token(type: .string, inputString: "**A normal string**", characterStyles: []) - ]) - var results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [ - Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [ - Token(type: .string, inputString: "_A normal string_", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [ - Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [ - Token(type: .string, inputString: "`A normal string`", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [ - Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [ - Token(type: .string, inputString: "*", characterStyles: []), - Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]), - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [ - Token(type: .string, inputString: "A string with one *", characterStyles: []), - Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]), - Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - } - func offtestAdvancedEscaping() { var challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [ @@ -331,29 +390,24 @@ class SwiftyMarkdownCharacterTests: XCTestCase { } func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() { - let asteriskSpace = """ - An asterisk followed by a space: * - Line break - """ - let backtickSpace = "A backtick followed by a space: `" - let underscoreSpace = "An underscore followed by a space: _" let asteriskFullStop = "Two asterisks followed by a full stop: **." - let backtickFullStop = "Two backticks followed by a full stop: ``." + let 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 backtickFullStop = "Two backticks followed by a full stop: ``." + + let underscoreSpace = "An underscore followed by a space: _" + let backtickComma = "A backtick followed by a space: `, `" let underscoreComma = "An underscore followed by a space: _, _" - let asteriskWithBold = "A **bold** word followed by an asterisk * " let backtickWithCode = "A `code` word followed by a backtick ` " let underscoreWithItalic = "An _italic_ word followed by an underscore _ " - var md = SwiftyMarkdown(string: asteriskSpace) - XCTAssertEqual(md.attributedString().string, asteriskSpace) - - md = SwiftyMarkdown(string: backtickSpace) + var md = SwiftyMarkdown(string: backtickSpace) XCTAssertEqual(md.attributedString().string, backtickSpace) md = SwiftyMarkdown(string: underscoreSpace) @@ -389,191 +443,5 @@ class SwiftyMarkdownCharacterTests: XCTestCase { } - func testForLinks() { - - var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [ - Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link]) - ]) - var results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first { - XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/") - } else { - XCTFail("Failed to find an open link tag") - } - - - challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [ - Token(type: .string, inputString: "A ", characterStyles: []), - Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link]) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - - challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [ - Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]), - Token(type: .string, inputString: ", ", characterStyles: []), - Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link]) - ]) - - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) - XCTAssertEqual(links.count, 2) - XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") - XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/") - - challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [ - Token(type: .string, inputString: "Email us at ", characterStyles: []), - Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]), - Token(type: .string, inputString: " Twitter", characterStyles: []), - Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link]) - ]) - - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) - XCTAssertEqual(links.count, 2) - XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com") - XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp") - challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [ - Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [ - Token(type: .string, inputString: "A ", characterStyles: []), - Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - - challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [ - Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []), - Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [ - Token(type: .string, inputString: "A ", characterStyles: []), - Token(type: .string, inputString: "[Link](", characterStyles: []), - Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [ - Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]), - Token(type: .string, inputString: " ", characterStyles: []), - Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: " ", characterStyles: []), - Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link]) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - - } - - func testLinksWithOtherStyles() { - var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [ - Token(type: .string, inputString: "A ", characterStyles: []), - Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]), - Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold]) - ]) - var results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) -// XCTAssertEqual(results.attributedString.string, challenge.output) - var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) - XCTAssertEqual(links.count, 1) - if links.count == 1 { - XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") - } else { - XCTFail("Incorrect link count. Expecting 1, found \(links.count)") - } - - challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [ - Token(type: .string, inputString: "A Bold ", characterStyles: []), - Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link]) - ]) - results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - XCTAssertEqual(results.attributedString.string, challenge.output) - links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) - XCTAssertEqual(links.count, 1) - XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") - } - - func testForImages() { - let challenge = TokenTest(input: "An ![Image](imageName)", output: "An Image", tokens: [ - Token(type: .string, inputString: "An Image", characterStyles: []), - Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image]) - ]) - let results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) }) - XCTAssertEqual(links.count, 1) - XCTAssertEqual(links[0].metadataString, "imageName") - } - - func testForStrikethrough() { - var challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [ - Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]), - Token(type: .string, inputString: "A crossed-out string", characterStyles: []) - ]) - var results = self.attempt(challenge) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - 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) - XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) - XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) - XCTAssertEqual(results.foundStyles, results.expectedStyles) - - } } diff --git a/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift b/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift index 4caa9fa..8f5123d 100644 --- a/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift +++ b/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift @@ -124,8 +124,20 @@ class SwiftyMarkdownTests: XCTestCase { 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") + let md = SwiftyMarkdown(string: dashBullets.input) + md.bullet = "-" + XCTAssertEqual(md.attributedString().string, dashBullets.expectedOutput) + + + } + + + /* The reason for this test is because the list of items dropped every other item in bullet lists marked with "-" diff --git a/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift b/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift new file mode 100644 index 0000000..393d104 --- /dev/null +++ b/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift @@ -0,0 +1,180 @@ +// +// SwiftyMarkdownCharacterTests.swift +// SwiftyMarkdownTests +// +// Created by Simon Fairbairn on 17/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// + +@testable import SwiftyMarkdown +import UIKit +import XCTest + +class SwiftyMarkdownLinkTests: XCTestCase { + + func testForLinks() { + + var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [ + Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first { + XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/") + } else { + XCTFail("Failed to find an open link tag") + } + + + challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [ + Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: ", ", characterStyles: []), + Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link]) + ]) + + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 2) + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/") + + challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [ + Token(type: .string, inputString: "Email us at ", characterStyles: []), + Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: " Twitter", characterStyles: []), + Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link]) + ]) + + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 2) + XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com") + XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp") + + challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [ + Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [ + Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []), + Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "[Link](", characterStyles: []), + Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [ + Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + } + + func testLinksWithOtherStyles() { + var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) +// XCTAssertEqual(results.attributedString.string, challenge.output) + var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 1) + if links.count == 1 { + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + } else { + XCTFail("Incorrect link count. Expecting 1, found \(links.count)") + } + + challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [ + Token(type: .string, inputString: "A Bold ", characterStyles: []), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 1) + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + } + + func testForImages() { + let challenge = TokenTest(input: "An ![Image](imageName)", output: "An Image", tokens: [ + Token(type: .string, inputString: "An Image", characterStyles: []), + Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image]) + ]) + let results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) }) + XCTAssertEqual(links.count, 1) + XCTAssertEqual(links[0].metadataString, "imageName") + } + + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..1fcb84e --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import AppLibrarianTests + +var tests = [XCTestCaseEntry]() +tests += AppLibrarianTests.allTests() +XCTMain(tests) diff --git a/Tests/SwiftyMarkdownTests/SwiftyMarkdownAttributedStringTests copy.swift b/Tests/SwiftyMarkdownTests/SwiftyMarkdownAttributedStringTests copy.swift new file mode 100644 index 0000000..600bf9b --- /dev/null +++ b/Tests/SwiftyMarkdownTests/SwiftyMarkdownAttributedStringTests copy.swift @@ -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") + + + } + +} diff --git a/Tests/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift b/Tests/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift new file mode 100644 index 0000000..3ca7626 --- /dev/null +++ b/Tests/SwiftyMarkdownTests/SwiftyMarkdownCharacterTests.swift @@ -0,0 +1,446 @@ +// +// 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 SwiftyMarkdownCharacterTests: XCTestCase { + + func testIsolatedCase() { + let challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [ + Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: []) + ]) + let results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + + } + + func testThatBoldTraitsAreRecognised() { + var challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [ + Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " word", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [ + Token(type: .string, inputString: "**A normal string**", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [ + Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [ + Token(type: .string, inputString: "*", characterStyles: []), + Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]), + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [ + Token(type: .string, inputString: "A string with one *", characterStyles: []), + Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatCodeTraitsAreRecognised() { + var challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [ + Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with `code` (should not be indented)", output: "A string with code (should not be indented)", tokens : [ + Token(type: .string, inputString: "A string with ", characterStyles: []), + Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: " (should not be indented)", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "`A code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [ + Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: " with multiple ", characterStyles: []), + Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "instances", characterStyles: [CharacterStyle.code]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [ + Token(type: .string, inputString: "`A normal string`", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [ + Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A lonely backtick: `", output: "A lonely backtick: `", tokens: [ + Token(type: .string, inputString: "A lonely backtick: `", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatItalicTraitsAreParsedCorrectly() { + + var challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [ + Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [ + Token(type: .string, inputString: "A string with ", characterStyles: []), + Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " text", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "_An italic string_ with a *mix* _of_ italic *styles*", output: "An italic string with a mix of italic styles", tokens : [ + Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " with a ", characterStyles: []), + Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " italic ", characterStyles: []), + Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.italic]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [ + Token(type: .string, inputString: "_A normal string_", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [ + Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: """ + 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(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + } + + func testThatStrikethroughTraitsAreRecognised() { + var challenge = TokenTest(input: "~~An~~A crossed-out string", output: "AnA crossed-out string", tokens: [ + Token(type: .string, inputString: "An", characterStyles: [CharacterStyle.strikethrough]), + Token(type: .string, inputString: "A crossed-out string", characterStyles: []) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + 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) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + + challenge = TokenTest(input: "\\~\\~removed\\~\\~crossed-out string. ~This should be ignored~", output: "~~removed~~crossed-out string. ~This should be ignored~", tokens: [ + Token(type: .string, inputString: "~~removed~~crossed-out string. ~This should be ignored~", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + + } + + func testThatMixedTraitsAreRecognised() { + + var challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [ + 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]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [ + Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: ", ", characterStyles: []), + Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: ", ", characterStyles: []), + Token(type: .string, inputString: "with some code", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: ", **and some** _escaped_ `characters`, ", characterStyles: []), + Token(type: .string, inputString: "ending", characterStyles: [CharacterStyle.code]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "with", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "more", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " variety.", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatExtraCharactersAreHandles() { + var challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [ + Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "*bold italic*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]), + Token(type: .string, inputString: " word", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with a ****bold italic*** word", output: "A string with a *bold italic word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "*bold italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]), + Token(type: .string, inputString: " word", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]), + Token(type: .string, inputString: " italic", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: " word", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [ + Token(type: .string, inputString: "A string with a ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " word", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + } + + + // The new version of SwiftyMarkdown is a lot more strict than the old version, although this may change in future + func offtestThatMarkdownMistakesAreHandledAppropriately() { + let mismatchedBoldCharactersAtStart = "**This should be bold*" + let mismatchedBoldCharactersWithin = "A string *that should be italic**" + + var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart) + XCTAssertEqual(md.attributedString().string, "This should be bold") + + md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin) + XCTAssertEqual(md.attributedString().string, "A string that should be italic") + + } + + func offtestAdvancedEscaping() { + + var challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [ + Token(type: .string, inputString: "**", characterStyles: []), + Token(type: .string, inputString: "A normal string", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: "**", characterStyles: []) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [ + Token(type: .string, inputString: "A string with randomly **", characterStyles: []), + Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]), + Token(type: .string, inputString: "** asterisks", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + } + + func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() { + + let 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 backtickFullStop = "Two backticks followed by a full stop: ``." + + let underscoreSpace = "An underscore followed by a space: _" + + let backtickComma = "A backtick followed by a space: `, `" + let underscoreComma = "An underscore followed by a space: _, _" + + let backtickWithCode = "A `code` word followed by a backtick ` " + let underscoreWithItalic = "An _italic_ word followed by an underscore _ " + + var md = SwiftyMarkdown(string: backtickSpace) + XCTAssertEqual(md.attributedString().string, backtickSpace) + + md = SwiftyMarkdown(string: underscoreSpace) + XCTAssertEqual(md.attributedString().string, underscoreSpace) + + md = SwiftyMarkdown(string: asteriskFullStop) + XCTAssertEqual(md.attributedString().string, asteriskFullStop) + + md = SwiftyMarkdown(string: backtickFullStop) + XCTAssertEqual(md.attributedString().string, backtickFullStop) + + md = SwiftyMarkdown(string: underscoreFullStop) + XCTAssertEqual(md.attributedString().string, underscoreFullStop) + + md = SwiftyMarkdown(string: asteriskComma) + XCTAssertEqual(md.attributedString().string, asteriskComma) + + md = SwiftyMarkdown(string: backtickComma) + XCTAssertEqual(md.attributedString().string, backtickComma) + + md = SwiftyMarkdown(string: underscoreComma) + XCTAssertEqual(md.attributedString().string, underscoreComma) + + md = SwiftyMarkdown(string: asteriskWithBold) + XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk *") + + md = SwiftyMarkdown(string: backtickWithCode) + XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick `") + + md = SwiftyMarkdown(string: underscoreWithItalic) + XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _") + + } + + + +} diff --git a/Tests/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift b/Tests/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift new file mode 100644 index 0000000..e4b20a0 --- /dev/null +++ b/Tests/SwiftyMarkdownTests/SwiftyMarkdownLineTests.swift @@ -0,0 +1,206 @@ +// +// 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 testReportedCrashingStrings() { + let text = "[**\\!bang**](https://duckduckgo.com/bang) " + let expected = "\\!bang" + let output = SwiftyMarkdown(string: text).attributedString().string + XCTAssertEqual(output, expected) + } + + func testThatYAMLMetadataIsRemoved() { + let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!\n\nWith A Heading\n---", expectedOutput: "Finally some Markdown!\n\nWith A Heading") + let md = SwiftyMarkdown(string: yaml.input) + XCTAssertEqual(md.attributedString().string, yaml.expectedOutput) + XCTAssertEqual(md.frontMatterAttributes.count, 8) + } + +} diff --git a/Tests/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift b/Tests/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift new file mode 100644 index 0000000..4b12b10 --- /dev/null +++ b/Tests/SwiftyMarkdownTests/SwiftyMarkdownLinkTests.swift @@ -0,0 +1,179 @@ +// +// 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: XCTestCase { + + func testForLinks() { + + var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [ + Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first { + XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/") + } else { + XCTFail("Failed to find an open link tag") + } + + + challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [ + Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: ", ", characterStyles: []), + Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link]) + ]) + + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 2) + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/") + + challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [ + Token(type: .string, inputString: "Email us at ", characterStyles: []), + Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: " Twitter", characterStyles: []), + Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link]) + ]) + + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 2) + XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com") + XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp") + + challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [ + Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + + challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [ + Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []), + Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "[Link](", characterStyles: []), + Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: []) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [ + Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: " ", characterStyles: []), + Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + + } + + func testLinksWithOtherStyles() { + var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [ + Token(type: .string, inputString: "A ", characterStyles: []), + Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold]) + ]) + var results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) +// XCTAssertEqual(results.attributedString.string, challenge.output) + var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 1) + if links.count == 1 { + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + } else { + XCTFail("Incorrect link count. Expecting 1, found \(links.count)") + } + + challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [ + Token(type: .string, inputString: "A Bold ", characterStyles: []), + Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link]) + ]) + results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + XCTAssertEqual(results.attributedString.string, challenge.output) + links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }) + XCTAssertEqual(links.count, 1) + XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/") + } + + func testForImages() { + let challenge = TokenTest(input: "An ![Image](imageName)", output: "An Image", tokens: [ + Token(type: .string, inputString: "An Image", characterStyles: []), + Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image]) + ]) + let results = self.attempt(challenge) + XCTAssertEqual(challenge.tokens.count, results.stringTokens.count) + XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output) + XCTAssertEqual(results.foundStyles, results.expectedStyles) + let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) }) + XCTAssertEqual(links.count, 1) + XCTAssertEqual(links[0].metadataString, "imageName") + } + + +} diff --git a/Tests/SwiftyMarkdownTests/SwiftyMarkdownPerformanceTests.swift b/Tests/SwiftyMarkdownTests/SwiftyMarkdownPerformanceTests.swift new file mode 100644 index 0000000..be774be --- /dev/null +++ b/Tests/SwiftyMarkdownTests/SwiftyMarkdownPerformanceTests.swift @@ -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) + } + } + +} diff --git a/Tests/SwiftyMarkdownTests/XCTest+SwiftyMarkdown.swift b/Tests/SwiftyMarkdownTests/XCTest+SwiftyMarkdown.swift new file mode 100644 index 0000000..e0c6e28 --- /dev/null +++ b/Tests/SwiftyMarkdownTests/XCTest+SwiftyMarkdown.swift @@ -0,0 +1,31 @@ +// +// XCTest+SwiftyMarkdown.swift +// SwiftyMarkdownTests +// +// Created by Simon Fairbairn on 17/12/2019. +// Copyright © 2019 Voyage Travel Apps. All rights reserved. +// + +import XCTest +@testable import SwiftyMarkdown + +extension XCTestCase { + + func resourceURL(for filename : String ) -> URL { + let thisSourceFile = URL(fileURLWithPath: #file) + let thisDirectory = thisSourceFile.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + return thisDirectory.appendingPathComponent("Resources", isDirectory: true).appendingPathComponent(filename) + } + + func attempt( _ challenge : TokenTest ) -> (tokens : [Token], stringTokens: [Token], attributedString : NSAttributedString, foundStyles : [[CharacterStyle]], expectedStyles : [[CharacterStyle]] ) { + let md = SwiftyMarkdown(string: challenge.input) + let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules) + let tokens = tokeniser.process(challenge.input) + let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata }) + + let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] }) + let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] }) + + return (tokens, stringTokens, md.attributedString(), existentTokenStyles, expectedStyles) + } +}