Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36f0c4d9c7 | |||
| 3b9f770ea9 | |||
| a1dfe192af | |||
| 050ca10694 | |||
| 45d03f88b7 | |||
| 586cecff2f | |||
| 124e0f0a48 | |||
| d510f69d10 | |||
| 19c473e81d | |||
| a209277d97 | |||
| 3546711696 | |||
| 3aa40eaa38 | |||
| 0bd12bd5a7 | |||
| d3a820b6e9 | |||
| 56f3c87925 | |||
| aac0c4a74c | |||
| 757e3fd212 | |||
| 2c2da71337 | |||
| d779ce978d | |||
| 0da260626a | |||
| 732a01872f | |||
| e7d60503cd | |||
| 9c9e6c3597 | |||
| ff36dbb07f | |||
| d149b1aa0e | |||
| e117006ad2 | |||
| cb49124b9c | |||
| c2325bac2f | |||
| 81857b5f4d | |||
| 4c1743ab80 | |||
| 35352af0a8 | |||
| 1d25236fec | |||
| d95ee23795 | |||
| e7d6f0ec35 | |||
| c88e81a51f | |||
| 6e6de412c9 | |||
| 6d6f83d4b0 | |||
| 9ab2d38c7d | |||
| e339833936 | |||
| bddcfa43a1 | |||
| 12ce9a365f | |||
| 616ede7202 | |||
| b000ab0be5 | |||
| 5ddb58a95d | |||
| f4520a11b5 | |||
| abaac24b16 | |||
| 57293f4261 | |||
| 1e94e474ed | |||
| 7ffab1963b | |||
| a63fb0dfe4 | |||
| e0e3f9a555 | |||
| 77b51a9592 | |||
| f1ead65825 | |||
| 7bb5c1232d | |||
| 1db338f7da | |||
| 65f1c9a552 | |||
| f6d752dec9 | |||
| 1a594e6841 | |||
| cb2ff2e3ea | |||
| 1fff74b946 | |||
| db1c0f7902 | |||
| 9a2cd21b15 | |||
| fdbfbcda54 | |||
| 04b2f41c00 | |||
| 957ec2b442 | |||
| 3848d54e45 | |||
| 85b1a0b944 | |||
| 289925e34d | |||
| f8567bd974 | |||
| 10ed2abd88 | |||
| 0e7ede23c3 | |||
| d316d83848 | |||
| 3c149a65e7 | |||
| 7a00026c2d | |||
| 5f8a6ba7dd | |||
| 6797288c0e | |||
| 96e6ac313c | |||
| 76f76d8178 | |||
| ea0717903e | |||
| 845985236a | |||
| e3e89715f8 | |||
| b077d32f52 | |||
| fc9d8b2646 | |||
| 43f94723c8 | |||
| ea306673e8 | |||
| 15f2a353be | |||
| e9fed1ced9 | |||
| 67297197e8 | |||
| 6ee2001d43 | |||
| 4bd6bf498f | |||
| 1f5a6d4e50 | |||
| 04de344a43 | |||
| f386286a7e | |||
| 7c07c37f10 | |||
| 07e9cf69a4 | |||
| c86ae343f1 | |||
| 170e4b6fa2 | |||
| d62bf1dee9 | |||
| c8e6bc6b44 | |||
| 18a1f2922a | |||
| 6d1ec33196 | |||
| 63f489eebe | |||
| 01d153b3ca | |||
| 5148e8ad77 | |||
| 994af3ad6d | |||
| 2b7bfc81d7 | |||
| aaf8f3618a | |||
| a030f210b3 | |||
| be2eaf522c | |||
| d48d6b1f90 | |||
| 100bb3df20 | |||
| 29da129a24 | |||
| 9fcdddf484 | |||
| e653548441 | |||
| ff0fce7eb7 | |||
| 99726212be | |||
| 0bfeba8188 | |||
| 7985b57341 | |||
| cb0ecbd67d | |||
| 1d4fb92429 | |||
| 7f276de733 | |||
| 5870b838e7 | |||
| b2dc325510 | |||
| 195f1a5491 | |||
| cac0c00e0f | |||
| e211af53f5 | |||
| 15c6b77288 | |||
| 4c0a60faa0 | |||
| b0bcb587cb | |||
| 02837925b9 | |||
| cc2ea97b4c | |||
| 0ab154977a | |||
| ac99c2efba | |||
| 39073280de | |||
| 0bca66728f | |||
| 38e2c0d733 | |||
| f4f7c7bd55 | |||
| cd30189674 | |||
| b51f13054c | |||
| 6487a98d90 | |||
| b9a01c7f87 | |||
| c70c5671c8 | |||
| 7bdd5d35ad | |||
| 76b27f4571 | |||
| c27ff1d9a5 | |||
| 0ff9fe8c1c | |||
| eb63a4ea5e | |||
| aa01b6bc58 | |||
| 37f08f09ae | |||
| c96f17edd0 | |||
| 6288214d26 | |||
| 66f5db3d74 | |||
| 057cf6a4e9 | |||
| 1a4cfe06aa | |||
| 455a844912 |
@@ -36,6 +36,7 @@ playground.xcworkspace
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
@@ -61,3 +62,6 @@ Carthage/Build
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/screenshots
|
||||
|
||||
# OS X
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
# A sample Gemfile
|
||||
source "https://rubygems.org"
|
||||
|
||||
# gem "rails"
|
||||
gem "cocoapods"
|
||||
gem 'fastlane'
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.1)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
babosa (1.0.3)
|
||||
claide (1.0.3)
|
||||
cocoapods (1.8.4)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.8.4)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.2, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.6.6)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.4)
|
||||
xcodeproj (>= 1.11.1, < 2.0)
|
||||
cocoapods-core (1.8.4)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.4)
|
||||
cocoapods-downloader (1.3.0)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-stats (1.1.0)
|
||||
cocoapods-trunk (1.4.1)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.4.1)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
escape (0.0.4)
|
||||
excon (0.72.0)
|
||||
faraday (0.17.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.7)
|
||||
fastlane (2.140.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 2.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 0.17)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.13.1)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.29.2, < 0.37.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
jwt (~> 2.1.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 2.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.36.4)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-core (1.5.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.3.0)
|
||||
faraday (~> 0.11)
|
||||
google-cloud-errors (1.0.0)
|
||||
google-cloud-storage (1.25.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.10.0)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.12)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.3.0)
|
||||
jwt (2.1.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
minitest (5.14.0)
|
||||
molinillo (0.6.6)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.6)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.0)
|
||||
netrc (0.11.0)
|
||||
os (1.0.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (1.4.0)
|
||||
rubyzip (1.3.0)
|
||||
security (0.1.3)
|
||||
signet (0.12.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.7)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thread_safe (0.3.6)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.7.0)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
tzinfo (1.2.6)
|
||||
thread_safe (~> 0.1)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.14.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
@@ -0,0 +1,18 @@
|
||||
// swift-tools-version:5.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftyMarkdown",
|
||||
platforms: [
|
||||
.iOS(SupportedPlatform.IOSVersion.v11),
|
||||
.tvOS(SupportedPlatform.TVOSVersion.v11),
|
||||
.macOS(.v10_12),
|
||||
.watchOS(.v4)
|
||||
],
|
||||
products: [
|
||||
.library(name: "SwiftyMarkdown", targets: ["SwiftyMarkdown"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "SwiftyMarkdown", path: "SwiftyMarkdown"),
|
||||
]
|
||||
)
|
||||
@@ -1,29 +1,336 @@
|
||||
# SwiftyMarkdown
|
||||
# SwiftyMarkdown 1.0
|
||||
|
||||
SwiftyMarkdown converts Markdown files and strings into NSAttributedString using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use
|
||||
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
|
||||
|
||||
## Usage
|
||||
- [What's New](#fully-rebuilt-for-2020)
|
||||
- [Installation](#installation)
|
||||
- [How to Use](#how-to-use-swiftymarkdown)
|
||||
- [Screenshot](#screenshot)
|
||||
- [Appendix](#appendix)
|
||||
|
||||
Text string
|
||||
## Fully Rebuilt For 2020!
|
||||
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
SwiftyMarkdown now features a more robust and reliable rules-based line processing and character tokenisation engine. It has added support for images stored in the bundle (``), codeblocks, blockquotes, and unordered lists!
|
||||
|
||||
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be optionally underlined by setting `underlineLinks` to `true`.
|
||||
|
||||
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
|
||||
|
||||
Support for all of Apple's platforms has been enabled.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods:
|
||||
|
||||
`pod 'SwiftyMarkdown'`
|
||||
|
||||
### SPM:
|
||||
|
||||
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
|
||||
|
||||
## How To Use SwiftyMarkdown
|
||||
|
||||
Read Markdown from a text string...
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString()
|
||||
```
|
||||
|
||||
...or from a URL.
|
||||
|
||||
```swift
|
||||
if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
|
||||
md.attributedString()
|
||||
}
|
||||
```
|
||||
|
||||
URL
|
||||
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
|
||||
|
||||
if let url = NSBundle.mainBundle().URLForResource("file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
|
||||
md.attributedString()
|
||||
}
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")
|
||||
```
|
||||
|
||||
The attributed string can then be assigned to any label or text control that has support for attributed text.
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
let label = UILabel()
|
||||
label.attributedText = md.attributedString()
|
||||
```
|
||||
|
||||
## Supported Markdown Features
|
||||
|
||||
*italics* or _italics_
|
||||
**bold** or __bold__
|
||||
|
||||
# Header 1
|
||||
|
||||
or
|
||||
|
||||
Header 1
|
||||
====
|
||||
|
||||
## Header 2
|
||||
|
||||
or
|
||||
|
||||
Header 2
|
||||
---
|
||||
|
||||
### Header 3
|
||||
#### Header 4
|
||||
##### Header 5 #####
|
||||
###### Header 6 ######
|
||||
|
||||
`code`
|
||||
|
||||
Indented code blocks (spaces or tabs)
|
||||
|
||||
[Links](http://voyagetravelapps.com/)
|
||||

|
||||
|
||||
> Blockquotes
|
||||
|
||||
- Bulleted
|
||||
- Lists
|
||||
|
||||
Compound rules also work, for example:
|
||||
|
||||
It recognises **[Bold Links](http://voyagetravelapps.com/)**
|
||||
|
||||
Or [**Bold Links**](http://voyagetravelapps.com/)
|
||||
|
||||
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
|
||||
|
||||
## Customisation
|
||||
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
Set the attributes of every paragraph and character style type using straightforward dot syntax:
|
||||
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
```swift
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
|
||||
md.italic.color = UIColor.blueColor()
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h1.fontSize = 16
|
||||
md.h1.alignmnent = .center
|
||||
|
||||
md.italic.color = UIColor.blueColor()
|
||||
|
||||
md.underlineLinks = true
|
||||
```
|
||||
|
||||
On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
There's an example project included in the repository. Open the `.xcworkspace` file to get started.
|
||||
|
||||
## Appendix
|
||||
|
||||
### A) All Customisable Properties
|
||||
|
||||
```swift
|
||||
h1.fontName : String
|
||||
h1.fontSize : CGFloat
|
||||
h1.color : UI/NSColor
|
||||
h1.fontStyle : FontStyle
|
||||
h1.alignment : NSTextAlignment
|
||||
|
||||
h2.fontName : String
|
||||
h2.fontSize : CGFloat
|
||||
h2.color : UI/NSColor
|
||||
h2.fontStyle : FontStyle
|
||||
h2.alignment : NSTextAlignment
|
||||
|
||||
h3.fontName : String
|
||||
h3.fontSize : CGFloat
|
||||
h3.color : UI/NSColor
|
||||
h3.fontStyle : FontStyle
|
||||
h3.alignment : NSTextAlignment
|
||||
|
||||
h4.fontName : String
|
||||
h4.fontSize : CGFloat
|
||||
h4.color : UI/NSColor
|
||||
h4.fontStyle : FontStyle
|
||||
h4.alignment : NSTextAlignment
|
||||
|
||||
h5.fontName : String
|
||||
h5.fontSize : CGFloat
|
||||
h5.color : UI/NSColor
|
||||
h5.fontStyle : FontStyle
|
||||
h5.alignment : NSTextAlignment
|
||||
|
||||
h6.fontName : String
|
||||
h6.fontSize : CGFloat
|
||||
h6.color : UI/NSColor
|
||||
h6.fontStyle : FontStyle
|
||||
h6.alignment : NSTextAlignment
|
||||
|
||||
body.fontName : String
|
||||
body.fontSize : CGFloat
|
||||
body.color : UI/NSColor
|
||||
body.fontStyle : FontStyle
|
||||
body.alignment : NSTextAlignment
|
||||
|
||||
blockquotes.fontName : String
|
||||
blockquotes.fontSize : CGFloat
|
||||
blockquotes.color : UI/NSColor
|
||||
blockquotes.fontStyle : FontStyle
|
||||
blockquotes.alignment : NSTextAlignment
|
||||
|
||||
link.fontName : String
|
||||
link.fontSize : CGFloat
|
||||
link.color : UI/NSColor
|
||||
link.fontStyle : FontStyle
|
||||
|
||||
bold.fontName : String
|
||||
bold.fontSize : CGFloat
|
||||
bold.color : UI/NSColor
|
||||
bold.fontStyle : FontStyle
|
||||
|
||||
italic.fontName : String
|
||||
italic.fontSize : CGFloat
|
||||
italic.color : UI/NSColor
|
||||
italic.fontStyle : FontStyle
|
||||
|
||||
code.fontName : String
|
||||
code.fontSize : CGFloat
|
||||
code.color : UI/NSColor
|
||||
code.fontStyle : FontStyle
|
||||
|
||||
underlineLinks : Bool
|
||||
```
|
||||
|
||||
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look.
|
||||
|
||||
If you like a bit of chaos:
|
||||
|
||||
```swift
|
||||
md.bold.fontStyle = .italic
|
||||
md.italic.fontStyle = .bold
|
||||
```
|
||||
|
||||
### B) Advanced Customisation
|
||||
|
||||
SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules.
|
||||
|
||||
For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:
|
||||
|
||||
```swift
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
case h1
|
||||
case h2
|
||||
case previousH1
|
||||
case codeblock
|
||||
case body
|
||||
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static public var lineRules = [
|
||||
LineRule(token: " ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)
|
||||
```
|
||||
|
||||
Similarly, the character styles all follow rules:
|
||||
|
||||
```swift
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case link, bold, italic, code
|
||||
}
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
```
|
||||
|
||||
#### Rule Subsets
|
||||
|
||||
If you want to only support a small subset of Markdown, it's now easy to do.
|
||||
|
||||
This example would only process strings with `*` and `_` characters, ignoring links, images, code, and all line-level attributes (headings, blockquotes, etc.)
|
||||
```swift
|
||||
SwiftyMarkdown.lineRules = []
|
||||
|
||||
SwiftyMarkdown.characterRules = [
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
```
|
||||
|
||||
#### Custom Rules
|
||||
|
||||
If you wanted to create a rule that applied a style of `Elf` to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:
|
||||
|
||||
```swift
|
||||
enum Characters : CharacterStyling {
|
||||
case elf
|
||||
|
||||
func isEqualTo( _ other : CharacterStyling) -> Bool {
|
||||
if let other = other as? Characters else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
let characterRules = [
|
||||
CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
|
||||
]
|
||||
|
||||
let processor = SwiftyTokeniser( with : characterRules )
|
||||
let string = "The elf will speak now: %Here is my elf speaking%"
|
||||
let tokens = processor.process(string)
|
||||
```
|
||||
|
||||
The output is an array of tokens would be equivalent to:
|
||||
|
||||
```swift
|
||||
[
|
||||
Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: []),
|
||||
Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: [])
|
||||
]
|
||||
```
|
||||
|
||||
### C) SpriteKit Support
|
||||
|
||||
Did you know that `SKLabelNode` supports attributed text? I didn't.
|
||||
|
||||
```swift
|
||||
let smd = SwiftyMarkdown(string: "My Character's **Dialogue**")
|
||||
|
||||
let label = SKLabelNode()
|
||||
label.preferredMaxLayoutWidth = 500
|
||||
label.numberOfLines = 0
|
||||
label.attributedText = smd.attributedString()
|
||||
```
|
||||
@@ -1,313 +0,0 @@
|
||||
//: Playground - noun: a place where people can play
|
||||
|
||||
import UIKit
|
||||
import XCPlayground
|
||||
|
||||
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 400.0, height: 600))
|
||||
|
||||
XCPlaygroundPage.currentPage.liveView = containerView
|
||||
let label = UITextView(frame: containerView.frame)
|
||||
containerView.addSubview(label)
|
||||
|
||||
|
||||
public protocol FontProperties {
|
||||
var fontName : String { get set }
|
||||
var color : UIColor { get set }
|
||||
}
|
||||
|
||||
|
||||
public struct BasicStyles : FontProperties {
|
||||
public var fontName = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
|
||||
public var color = UIColor.blackColor()
|
||||
}
|
||||
|
||||
enum LineType : Int {
|
||||
case H1, H2, H3, H4, H5, H6, Body, Italic, Bold, Code
|
||||
}
|
||||
|
||||
|
||||
public class SwiftyMarkdown {
|
||||
|
||||
public var h1 = BasicStyles()
|
||||
public var h2 = BasicStyles()
|
||||
public var h3 = BasicStyles()
|
||||
public var h4 = BasicStyles()
|
||||
public var h5 = BasicStyles()
|
||||
public var h6 = BasicStyles()
|
||||
|
||||
public var body = BasicStyles()
|
||||
public var link = BasicStyles()
|
||||
public var italic = BasicStyles()
|
||||
public var code = BasicStyles()
|
||||
public var bold = BasicStyles()
|
||||
|
||||
let string : String
|
||||
let instructionSet = NSCharacterSet(charactersInString: "\\*_`")
|
||||
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
public init?(url : NSURL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
fatalError("Couldn't read string")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
|
||||
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
|
||||
|
||||
var lineCount = 0
|
||||
|
||||
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
|
||||
|
||||
|
||||
var skipLine = false
|
||||
for line in lines {
|
||||
lineCount++
|
||||
if skipLine {
|
||||
skipLine = false
|
||||
continue
|
||||
}
|
||||
var headingFound = false
|
||||
for heading in headings {
|
||||
|
||||
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
|
||||
|
||||
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
|
||||
let endHeadingHash = " " + heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
|
||||
let finalHeadingString = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingHash, withString: "")
|
||||
|
||||
// Make Hx where x == current index
|
||||
let string = attributedStringFromString(finalHeadingString, withType: LineType(rawValue: headings.indexOf(heading)!)!)
|
||||
attributedString.appendAttributedString(string)
|
||||
headingFound = true
|
||||
}
|
||||
}
|
||||
if headingFound {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if lineCount < lines.count {
|
||||
let nextLine = lines[lineCount]
|
||||
|
||||
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H1)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
|
||||
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
|
||||
|
||||
|
||||
// Make H1
|
||||
let string = attributedStringFromString(line, withType: .H2)
|
||||
attributedString.appendAttributedString(string)
|
||||
skipLine = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if line.characters.count > 0 {
|
||||
|
||||
let scanner = NSScanner(string: line)
|
||||
|
||||
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
while !scanner.atEnd {
|
||||
|
||||
var followingString : NSString?
|
||||
var string : NSString?
|
||||
// Get all the characters up to the ones we are interested in
|
||||
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
|
||||
if let hasString = string as? String {
|
||||
let bodyString = attributedStringFromString(hasString, withType: .Body)
|
||||
attributedString.appendAttributedString(bodyString)
|
||||
|
||||
var matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
|
||||
let location = scanner.scanLocation
|
||||
// If the next string after the characters is a space, then add it to the final string and continue
|
||||
if !scanner.scanUpToString(" ", intoString: nil) {
|
||||
|
||||
let charAtts = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(charAtts)
|
||||
} else {
|
||||
scanner.scanLocation = location
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
if let hasString = followingString as? String {
|
||||
let attString : NSAttributedString
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
|
||||
} else if matchedCharacters == "**" || matchedCharacters == "__" {
|
||||
attString = attributedStringFromString(hasString, withType: .Bold)
|
||||
} else {
|
||||
attString = attributedStringFromString(hasString, withType: .Italic)
|
||||
}
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
if let hasString = followingString as? String {
|
||||
let attString : NSAttributedString
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
attString = attributedStringFromString(matchedCharacters + hasString, withType: .Body)
|
||||
} else if matchedCharacters == "**" || matchedCharacters == "__" {
|
||||
attString = attributedStringFromString(hasString, withType: .Bold)
|
||||
} else {
|
||||
attString = attributedStringFromString(hasString, withType: .Italic)
|
||||
}
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
matchedCharacters = self.tagFromScanner(scanner)
|
||||
|
||||
if matchedCharacters.containsString("\\") {
|
||||
let attString = attributedStringFromString(matchedCharacters, withType: .Body)
|
||||
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func tagFromScanner( scanner : NSScanner ) -> String {
|
||||
var matchedCharacters : String = ""
|
||||
var tempCharacters : NSString?
|
||||
|
||||
// Scan the ones we are interested in
|
||||
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
|
||||
if let chars = tempCharacters as? String {
|
||||
matchedCharacters = matchedCharacters + chars
|
||||
}
|
||||
}
|
||||
return matchedCharacters
|
||||
}
|
||||
|
||||
|
||||
// Make H1
|
||||
|
||||
func attributedStringFromString(string : String, withType type : LineType ) -> NSAttributedString {
|
||||
var attributes : [String : AnyObject]
|
||||
let textStyle : String
|
||||
let fontName : String
|
||||
|
||||
var appendNewLine = true
|
||||
|
||||
switch type {
|
||||
case .H1:
|
||||
fontName = h1.fontName
|
||||
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle1
|
||||
}
|
||||
attributes = [NSForegroundColorAttributeName : h1.color]
|
||||
case .H2:
|
||||
fontName = h2.fontName
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
attributes = [NSForegroundColorAttributeName : h2.color]
|
||||
case .H3:
|
||||
fontName = h3.fontName
|
||||
textStyle = UIFontTextStyleTitle3
|
||||
attributes = [NSForegroundColorAttributeName : h3.color]
|
||||
case .H4:
|
||||
fontName = h4.fontName
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
attributes = [NSForegroundColorAttributeName : h4.color]
|
||||
case .H5:
|
||||
fontName = h5.fontName
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
attributes = [NSForegroundColorAttributeName : h5.color]
|
||||
case .H6:
|
||||
fontName = h6.fontName
|
||||
textStyle = UIFontTextStyleFootnote
|
||||
attributes = [NSForegroundColorAttributeName : h6.color]
|
||||
case .Italic:
|
||||
fontName = italic.fontName
|
||||
attributes = [NSForegroundColorAttributeName : italic.color]
|
||||
textStyle = UIFontTextStyleBody
|
||||
appendNewLine = false
|
||||
case .Bold:
|
||||
fontName = bold.fontName
|
||||
attributes = [NSForegroundColorAttributeName : bold.color]
|
||||
appendNewLine = false
|
||||
textStyle = UIFontTextStyleBody
|
||||
default:
|
||||
appendNewLine = false
|
||||
fontName = body.fontName
|
||||
textStyle = UIFontTextStyleBody
|
||||
attributes = [NSForegroundColorAttributeName:body.color]
|
||||
break
|
||||
}
|
||||
|
||||
let font = UIFont.preferredFontForTextStyle(textStyle)
|
||||
let styleDescriptor = font.fontDescriptor()
|
||||
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
|
||||
|
||||
var finalFont : UIFont
|
||||
if let font = UIFont(name: fontName, size: styleSize) {
|
||||
finalFont = font
|
||||
} else {
|
||||
finalFont = UIFont.preferredFontForTextStyle(textStyle)
|
||||
}
|
||||
|
||||
let finalFontDescriptor = finalFont.fontDescriptor()
|
||||
if type == .Italic {
|
||||
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
|
||||
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
|
||||
}
|
||||
if type == .Bold {
|
||||
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
|
||||
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
|
||||
}
|
||||
|
||||
|
||||
attributes[NSFontAttributeName] = finalFont
|
||||
|
||||
if appendNewLine {
|
||||
return NSAttributedString(string: string + "\n", attributes: attributes)
|
||||
} else {
|
||||
return NSAttributedString(string: string, attributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let url = NSBundle.mainBundle().URLForResource("test", withExtension: "md"), md = SwiftyMarkdown(url: url) {
|
||||
|
||||
label.attributedText = md.attributedString()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
}
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static let lineRules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static let characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = BasicStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = BasicStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = BasicStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
let string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
}
|
||||
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
}
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
var strings : [String] = []
|
||||
for line in foundAttributes {
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
fontName = h1.fontName
|
||||
fontSize = h1.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
fontName = h2.fontName
|
||||
fontSize = h2.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
fontName = h3.fontName
|
||||
fontSize = h3.fontSize
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
fontName = h4.fontName
|
||||
fontSize = h4.fontSize
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
fontName = h5.fontName
|
||||
fontSize = h5.fontSize
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
fontName = h6.fontName
|
||||
fontSize = h6.fontSize
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
default:
|
||||
fontName = body.fontName
|
||||
fontSize = body.fontSize
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return body.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
var outputLine = line.line
|
||||
if let style = line.lineStyle as? MarkdownLineStyle, style == .codeblock {
|
||||
outputLine = "\t\(outputLine)"
|
||||
}
|
||||
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
for token in tokens {
|
||||
var font = self.font(for: line)
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
if let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
if let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
}
|
||||
attributes[.font] = font
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
}
|
||||
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
continue
|
||||
}
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let image = UIImage(named: "bubble")
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = image
|
||||
let att = NSAttributedString(attachment: image1Attachment)
|
||||
|
||||
|
||||
|
||||
var str = "# Hello, *playground* `code` **bold** "
|
||||
|
||||
let md = SwiftyMarkdown(string: str)
|
||||
md.body.color = .red
|
||||
md.h1.color = .white
|
||||
md.h1.fontName = "Noteworthy-Light"
|
||||
|
||||
md.link.color = .red
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
md.attributedString()
|
||||
|
||||
//: [Next](@next)
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
let lineProcesser = SwiftyLineProcessor(rules: rules, defaultRule: MarkdownLineStyle.body)
|
||||
print(lineProcesser.process("#### Heading 4 ###").first?.line ?? "")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
import SpriteKit
|
||||
import PlaygroundSupport
|
||||
|
||||
|
||||
|
||||
class GameScene : SKScene {
|
||||
var str = "# Text\n## Speaker 1\nHello, **playground**. *I* don't want to be here, you know. *I* want to be somewhere else."
|
||||
override func didMove(to view: SKView) {
|
||||
|
||||
let md = SwiftyMarkdown(string: str)
|
||||
md.h2.alignment = .center
|
||||
md.body.alignment = .center
|
||||
|
||||
let label = SKLabelNode(attributedText: md.attributedString())
|
||||
label.position = CGPoint(x: 100, y: 100)
|
||||
label.numberOfLines = 0
|
||||
label.preferredMaxLayoutWidth = 400
|
||||
label.horizontalAlignmentMode = .left
|
||||
self.addChild(label)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let view = SKView(frame: CGRect(x: 0, y: 0, width: 600, height: 500))
|
||||
let scene = GameScene(size: view.frame.size)
|
||||
scene.scaleMode = .aspectFit
|
||||
view.presentScene(scene)
|
||||
PlaygroundPage.current.liveView = view
|
||||
|
||||
//: [Next](@next)
|
||||
@@ -0,0 +1,7 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
//: [Next](@next)
|
||||
@@ -0,0 +1,680 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case processed
|
||||
case string
|
||||
case escape
|
||||
case metadata
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public var type : TokenType
|
||||
public let inputString : String
|
||||
public var metadataString : String? = nil
|
||||
public var characterStyles : [CharacterStyling] = []
|
||||
public var count : Int = 0
|
||||
public var shouldSkip : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count == 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return inputString
|
||||
case .metadata, .processed:
|
||||
return ""
|
||||
case .escape, .string:
|
||||
return inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
var replacements : [Int : [Token]] = [:]
|
||||
for (idx,token) in currentTokens.enumerated() {
|
||||
switch token.type {
|
||||
case .string:
|
||||
|
||||
if !token.shouldSkip {
|
||||
let nextTokens = self.scan(token.outputString, with: nextRule)
|
||||
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// This replaces the individual string tokens with the new token arrays
|
||||
// making sure to apply any previously found styles to the new tokens.
|
||||
for key in replacements.keys.sorted(by: { $0 > $1 }) {
|
||||
let existingToken = currentTokens[key]
|
||||
var newTokens : [Token] = []
|
||||
for token in replacements[key]! {
|
||||
var newToken = token
|
||||
if existingToken.metadataString != nil {
|
||||
newToken.metadataString = existingToken.metadataString
|
||||
}
|
||||
|
||||
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
|
||||
newTokens.append(newToken)
|
||||
}
|
||||
currentTokens.replaceSubrange(key...key, with: newTokens)
|
||||
}
|
||||
}
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].type = .metadata
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].type = .processed
|
||||
tokens[metadataIndex].type = .processed
|
||||
tokens[index].type = .processed
|
||||
}
|
||||
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
print( tokens.map( { ( $0.outputString, $0.count )}))
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
print( "Found escape (\(token.inputString))" )
|
||||
case .repeatingTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
continue
|
||||
}
|
||||
|
||||
let startIdx = idx
|
||||
var endIdx : Int? = nil
|
||||
|
||||
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
continue
|
||||
}
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
mutableTokens[i].characterStyles.append(style)
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
mutableTokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
mutableTokens[idx].count = 0
|
||||
mutableTokens[existentEnd].count = 0
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found String: \(theToken.inputString)" )
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
print ("With metadata: \(hasMetadata)" )
|
||||
}
|
||||
case .metadata:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found metadata: \(theToken.inputString)" )
|
||||
|
||||
case .processed:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found already processed tag: \(theToken.inputString)" )
|
||||
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Example customisation
|
||||
public enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
|
||||
|
||||
var str = "A standard paragraph with an *italic*, * spaced asterisk, \\*escaped asterisks\\*, _underscored italics_, \\_escaped underscores\\_, **bold** \\*\\*escaped double asterisks\\*\\*, __underscored bold__, _ spaced underscore \\_\\_escaped double underscores\\_\\_ and a `code block *with an italic that should be ignored*`."
|
||||
|
||||
//str = "**AAAA*BB\\*BB*AAAAAA**"
|
||||
str = "*_*Bold and italic*_*"
|
||||
str = "*Italic* `Code block with *ignored* italic` __Bold__"
|
||||
|
||||
struct TokenTest {
|
||||
let input : String
|
||||
let output : String
|
||||
let tokens : [Token]
|
||||
}
|
||||
|
||||
let challenge1 = TokenTest(input: "*_*italic*_*", output: "italic", tokens: [
|
||||
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
let challenge2 = TokenTest(input: "*Italic* `Code block with *ignored* italic` __Bold__", output : "Italic `Code block with *ignored* italic` Bold", tokens : [
|
||||
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Code block with *ignored* italic", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
let challenge3 = TokenTest(input: " * ", output : " * ", tokens : [
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: " ", characterStyles: [])
|
||||
])
|
||||
let challenge4 = TokenTest(input: "**AAAA*BB\\*BB*AAAAAA**", output : "AAAABB*BBAAAAAA", tokens : [
|
||||
Token(type: .string, inputString: "AAAA", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "BB*BB", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "AAAAAA", characterStyles: [CharacterStyle.bold]),
|
||||
])
|
||||
let challenge5 = TokenTest(input: "*Italic* \\_\\_Not Bold\\_\\_ **Bold**", output : "Italic __Not Bold__ Bold", tokens : [
|
||||
Token(type: .string, inputString: "Italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " __Not Bold__ ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
|
||||
let challenge6 = TokenTest(input: " *\\** ", output : " *** ", tokens : [
|
||||
Token(type: .string, inputString: " *** ", characterStyles: [])
|
||||
])
|
||||
|
||||
let challenge7 = TokenTest(input: " *\\**Italic*\\** ", output : " *Italic* ", tokens : [
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*Italic*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
])
|
||||
|
||||
let challenge8 = TokenTest(input: "[*Link*](https://www.neverendingvoyage.com/)", output : "Link", tokens : [
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
|
||||
])
|
||||
|
||||
let challenge9 = TokenTest(input: "`Code (should not be indented)`", output: "Code (should not be indented)", tokens: [
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.italic])
|
||||
])
|
||||
|
||||
let challenge10 = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskOneNormalAtStart = TokenTest(input: "\\**A normal string\\**", output: "*A normal string*", tokens: [
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: "A normal string*", characterStyles: [CharacterStyle.italic]),
|
||||
])
|
||||
|
||||
|
||||
let escapedBoldAtStart = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
|
||||
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
|
||||
])
|
||||
|
||||
let escapedBoldWithin = TokenTest(input: "A string with \\*\\*escaped\\*\\* asterisks", output: "A string with **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with **escaped** asterisks", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskOneNormalWithin = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
|
||||
Token(type: .string, inputString: "A string with one *", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
|
||||
])
|
||||
|
||||
let oneEscapedAsteriskTwoNormalWithin = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "** asterisks", characterStyles: [])
|
||||
])
|
||||
|
||||
"Given an array of integers, return **indices** of the two numbers such that they add up to a specific target.\n\nYou may assume that each input would have **_exactly_** one solution, and you may not use the _same_ element twice.\n\n**Example:**\n\nGiven nums = [2, 7, 11, 15], target = 9,\n\nBecause nums[**0**] + nums[**1**] = 2 + 7 = 9,\nreturn [**0**, **1**]."
|
||||
|
||||
let challenges = [challenge8]
|
||||
|
||||
var images = CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1)
|
||||
var links = CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1)
|
||||
var codeblock = CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1)
|
||||
codeblock.cancels = .allRemaining
|
||||
let asterisks = CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
let underscores = CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [.italic], 2 : [.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
|
||||
let scan = SwiftyTokeniser(with: [ images, links, codeblock, asterisks])
|
||||
|
||||
|
||||
for challenge in challenges {
|
||||
let finalTokens = scan.process(challenge.input)
|
||||
let stringTokens = finalTokens.filter({ $0.type == .string })
|
||||
|
||||
guard stringTokens.count == challenge.tokens.count else {
|
||||
print("Token count check failed. Expected: \(challenge.tokens.count). Found: \(stringTokens.count)")
|
||||
print("-------EXPECTED--------")
|
||||
for token in challenge.tokens {
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("\(token.outputString): \(token.characterStyles)")
|
||||
default:
|
||||
print("\(token.outputString)")
|
||||
}
|
||||
}
|
||||
print("-------OUTPUT--------")
|
||||
for token in finalTokens {
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("\(token.outputString): \(token.characterStyles)")
|
||||
default:
|
||||
if !token.outputString.isEmpty {
|
||||
print("\(token.outputString)")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
print("-----EXPECTATIONS-----")
|
||||
for (idx, token) in stringTokens.enumerated() {
|
||||
let expected = challenge.tokens[idx]
|
||||
|
||||
if expected.type != token.type {
|
||||
print("Failure: Token types are different. Expected: \(expected.type), found: \(token.type)")
|
||||
}
|
||||
|
||||
switch token.type {
|
||||
case .string:
|
||||
print("Expected: \(expected.outputString): \(expected.characterStyles)")
|
||||
print("Found: \(token.outputString): \(token.characterStyles)")
|
||||
if token.metadataString != nil {
|
||||
print("Metadata: \(token.metadataString!)")
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let string = finalTokens.map({ $0.outputString }).joined()
|
||||
print("-------OUTPUT VS INPUT--------")
|
||||
print("Input: \(challenge.input)")
|
||||
print("Expected: \(challenge.output)")
|
||||
print("Output: \(string)")
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//: [Next](@next)
|
||||
@@ -0,0 +1,36 @@
|
||||
// Code inside modules can be shared between pages and other source files.
|
||||
import Foundation
|
||||
|
||||
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
|
||||
public extension StringProtocol {
|
||||
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.upperBound
|
||||
}
|
||||
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
|
||||
var indices: [Index] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
indices.append(range.lowerBound)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return indices
|
||||
}
|
||||
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
|
||||
var result: [Range<Index>] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
result.append(range)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
let defaultType : LineStyling
|
||||
public var processEmptyStrings : LineStyling?
|
||||
let lineRules : [LineRule]
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
|
||||
output.removeSubrange(output.startIndex..<range)
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
let token = element.token.trimmingCharacters(in: .whitespaces)
|
||||
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
|
||||
output.removeSubrange(range..<output.endIndex)
|
||||
return output
|
||||
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func processLineLevelAttributes( _ text : String ) -> SwiftyLine {
|
||||
if text.isEmpty, let style = processEmptyStrings {
|
||||
return SwiftyLine(line: "", lineStyle: style)
|
||||
}
|
||||
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
|
||||
for element in previousLines {
|
||||
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let charSet = CharacterSet(charactersIn: element.token )
|
||||
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
|
||||
return SwiftyLine(line: "", lineStyle: element.type)
|
||||
}
|
||||
}
|
||||
for element in lineRules {
|
||||
guard element.token.count > 0 else {
|
||||
continue
|
||||
}
|
||||
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let unprocessed = output
|
||||
|
||||
switch element.removeFrom {
|
||||
case .leading:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
case .trailing:
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .both:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Only if the output has changed in some way
|
||||
guard unprocessed != output else {
|
||||
continue
|
||||
}
|
||||
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
|
||||
return SwiftyLine(line: output, lineStyle: element.type)
|
||||
|
||||
}
|
||||
|
||||
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
for heading in string.split(separator: "\n") {
|
||||
|
||||
if processEmptyStrings == nil, heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
let input : SwiftyLine
|
||||
input = processLineLevelAttributes(String(heading))
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case processed
|
||||
case string
|
||||
case escape
|
||||
case metadata
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public var type : TokenType
|
||||
public let inputString : String
|
||||
public var metadataString : String? = nil
|
||||
public var characterStyles : [CharacterStyling] = []
|
||||
public var count : Int = 0
|
||||
public var shouldSkip : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count == 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return inputString
|
||||
case .metadata, .processed:
|
||||
return ""
|
||||
case .escape, .string:
|
||||
return inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
var replacements : [Int : [Token]] = [:]
|
||||
for (idx,token) in currentTokens.enumerated() {
|
||||
switch token.type {
|
||||
case .string:
|
||||
|
||||
if !token.shouldSkip {
|
||||
let nextTokens = self.scan(token.outputString, with: nextRule)
|
||||
replacements[idx] = self.applyStyles(to: nextTokens, usingRule: nextRule)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// This replaces the individual string tokens with the new token arrays
|
||||
// making sure to apply any previously found styles to the new tokens.
|
||||
for key in replacements.keys.sorted(by: { $0 > $1 }) {
|
||||
let existingToken = currentTokens[key]
|
||||
var newTokens : [Token] = []
|
||||
for token in replacements[key]! {
|
||||
var newToken = token
|
||||
if existingToken.metadataString != nil {
|
||||
newToken.metadataString = existingToken.metadataString
|
||||
}
|
||||
|
||||
newToken.characterStyles.append(contentsOf: existingToken.characterStyles)
|
||||
newTokens.append(newToken)
|
||||
}
|
||||
currentTokens.replaceSubrange(key...key, with: newTokens)
|
||||
}
|
||||
}
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].type = .metadata
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].type = .processed
|
||||
tokens[metadataIndex].type = .processed
|
||||
tokens[index].type = .processed
|
||||
}
|
||||
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
print( tokens.map( { ( $0.outputString, $0.count )}))
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
print( "Found escape (\(token.inputString))" )
|
||||
case .repeatingTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found repeating tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
continue
|
||||
}
|
||||
|
||||
let startIdx = idx
|
||||
var endIdx : Int? = nil
|
||||
|
||||
if let nextTokenIdx = mutableTokens.firstIndex(where: { $0.inputString == theToken.inputString && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
continue
|
||||
}
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[theToken.count] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
mutableTokens[i].characterStyles.append(style)
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
mutableTokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
mutableTokens[idx].count = 0
|
||||
mutableTokens[existentEnd].count = 0
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found open tag with tag count \(theToken.count) tags: \(theToken.inputString). Current rule open tag = \(rule.openTag)" )
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found intermediate tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found close tag with tag count \(theToken.count) tags: \(theToken.inputString)" )
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found String: \(theToken.inputString)" )
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
print ("With metadata: \(hasMetadata)" )
|
||||
}
|
||||
case .metadata:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found metadata: \(theToken.inputString)" )
|
||||
|
||||
case .processed:
|
||||
let theToken = mutableTokens[idx]
|
||||
print ("Found already processed tag: \(theToken.inputString)" )
|
||||
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation)
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
public var color = UIColor.black
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static let lineRules = [
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static let characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
|
||||
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
let string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
if #available(iOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
if #available(iOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
}
|
||||
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
}
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString() -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
for line in foundAttributes {
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
|
||||
let lineProperties : LineProperties
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList:
|
||||
lineProperties = body
|
||||
finalTokens.insert(Token(type: .string, inputString: "・ "), at: 0)
|
||||
default:
|
||||
lineProperties = body
|
||||
break
|
||||
}
|
||||
|
||||
if lineProperties.alignment != .left {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
|
||||
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
continue
|
||||
}
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
<playground version='6.0' target-platform='ios' last-migration='1120'>
|
||||
<pages>
|
||||
<page name='Scanner Tests'/>
|
||||
<page name='Line Processing'/>
|
||||
<page name='Tokenising'/>
|
||||
<page name='Attributed String'/>
|
||||
</pages>
|
||||
</playground>
|
||||
@@ -1,16 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "SwiftyMarkdown"
|
||||
s.version = "0.2.2"
|
||||
s.version = "1.0.2"
|
||||
s.summary = "Converts Markdown to NSAttributed String"
|
||||
s.homepage = "https://github.com/SimonFairbairn/Stormcloud"
|
||||
s.homepage = "https://github.com/SimonFairbairn/SwiftyMarkdown"
|
||||
s.license = 'MIT'
|
||||
s.author = { "Simon Fairbairn" => "simon@voyagetravelapps.com" }
|
||||
s.source = { :git => "https://github.com/SimonFairbairn/SwiftyMarkdown.git", :tag => s.version }
|
||||
s.social_media_url = 'https://twitter.com/SimonFairbairn'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.tvos.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.12"
|
||||
s.watchos.deployment_target = "4.0"
|
||||
s.requires_arc = true
|
||||
|
||||
s.source_files = 'SwiftyMarkdown/'
|
||||
|
||||
s.swift_version = "5.0"
|
||||
|
||||
end
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */; };
|
||||
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */; };
|
||||
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */; };
|
||||
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */; };
|
||||
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */; };
|
||||
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */; };
|
||||
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */; };
|
||||
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */; };
|
||||
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */; };
|
||||
F4A48ABB23A8521C00713437 /* test.md in Resources */ = {isa = PBXBuildFile; fileRef = F4A48ABA23A8518900713437 /* test.md */; };
|
||||
F4CE98851C8A921300D735C1 /* SwiftyMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
F4CE988C1C8A921300D735C1 /* SwiftyMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */; };
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */; };
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */; };
|
||||
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */; };
|
||||
F4CE98E91C8AF01300D735C1 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E61C8AF01300D735C1 /* LICENSE */; };
|
||||
F4CE98EB1C8AF01300D735C1 /* SwiftyMarkdown.podspec in Resources */ = {isa = PBXBuildFile; fileRef = F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */; };
|
||||
@@ -26,16 +36,27 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownCharacterTests.swift; sourceTree = "<group>"; };
|
||||
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
|
||||
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownPerformanceTests.swift; sourceTree = "<group>"; };
|
||||
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+macOS.swift"; sourceTree = "<group>"; };
|
||||
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdown+iOS.swift"; sourceTree = "<group>"; };
|
||||
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SwiftyMarkdown.swift"; sourceTree = "<group>"; };
|
||||
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyLineProcessor.swift; sourceTree = "<group>"; };
|
||||
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyTokeniser.swift; sourceTree = "<group>"; };
|
||||
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftyMarkdownAttributedStringTests copy.swift"; sourceTree = "<group>"; };
|
||||
F4A48ABA23A8518900713437 /* test.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = test.md; sourceTree = "<group>"; };
|
||||
F4CE98811C8A921300D735C1 /* SwiftyMarkdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyMarkdown.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyMarkdown.h; sourceTree = "<group>"; };
|
||||
F4CE98861C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F4CE988B1C8A921300D735C1 /* SwiftyMarkdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMarkdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownTests.swift; sourceTree = "<group>"; };
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdownLineTests.swift; sourceTree = "<group>"; };
|
||||
F4CE98921C8A921300D735C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyMarkdown.swift; sourceTree = "<group>"; };
|
||||
F4CE98E61C8AF01300D735C1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
F4CE98E71C8AF01300D735C1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
F4CE98E81C8AF01300D735C1 /* SwiftyMarkdown.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SwiftyMarkdown.podspec; sourceTree = "<group>"; };
|
||||
F4FEF07423E13365007219EF /* metadataTest.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = metadataTest.md; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -84,6 +105,11 @@
|
||||
F4CE98841C8A921300D735C1 /* SwiftyMarkdown.h */,
|
||||
F4CE98861C8A921300D735C1 /* Info.plist */,
|
||||
F4CE989B1C8A922E00D735C1 /* SwiftyMarkdown.swift */,
|
||||
F44E3F45239C365A00508290 /* String+SwiftyMarkdown.swift */,
|
||||
F49A0DA523A711910005B8B2 /* SwiftyLineProcessor.swift */,
|
||||
F49A0DA723A71A2C0005B8B2 /* SwiftyTokeniser.swift */,
|
||||
F4440ABD23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift */,
|
||||
F4440ABB23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift */,
|
||||
);
|
||||
path = SwiftyMarkdown;
|
||||
sourceTree = "<group>";
|
||||
@@ -91,8 +117,14 @@
|
||||
F4CE988F1C8A921300D735C1 /* SwiftyMarkdownTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownTests.swift */,
|
||||
F4A48ABA23A8518900713437 /* test.md */,
|
||||
F4FEF07423E13365007219EF /* metadataTest.md */,
|
||||
F4CE98901C8A921300D735C1 /* SwiftyMarkdownLineTests.swift */,
|
||||
F40D3A7E23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift */,
|
||||
F4CE98921C8A921300D735C1 /* Info.plist */,
|
||||
F40D3A8023A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift */,
|
||||
F4A48AB823A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift */,
|
||||
F40D3A8323A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift */,
|
||||
);
|
||||
path = SwiftyMarkdownTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -154,23 +186,27 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0720;
|
||||
LastUpgradeCheck = 0720;
|
||||
LastUpgradeCheck = 1120;
|
||||
ORGANIZATIONNAME = "Voyage Travel Apps";
|
||||
TargetAttributes = {
|
||||
F4CE98801C8A921300D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
F4CE988A1C8A921300D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
DevelopmentTeam = 52T262DA8V;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = F4CE987B1C8A921300D735C1 /* Build configuration list for PBXProject "SwiftyMarkdown" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = F4CE98771C8A921300D735C1;
|
||||
productRefGroup = F4CE98821C8A921300D735C1 /* Products */;
|
||||
@@ -197,6 +233,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F4A48ABB23A8521C00713437 /* test.md in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -207,7 +244,12 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F4440ABC23A8585C00F21DD0 /* SwiftyMarkdown+macOS.swift in Sources */,
|
||||
F49A0DA623A711920005B8B2 /* SwiftyLineProcessor.swift in Sources */,
|
||||
F4440ABE23A8587C00F21DD0 /* SwiftyMarkdown+iOS.swift in Sources */,
|
||||
F4CE989C1C8A922E00D735C1 /* SwiftyMarkdown.swift in Sources */,
|
||||
F49A0DA823A71A2C0005B8B2 /* SwiftyTokeniser.swift in Sources */,
|
||||
F44E3F46239C365A00508290 /* String+SwiftyMarkdown.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -215,7 +257,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownTests.swift in Sources */,
|
||||
F40D3A8123A8085F0085CF6E /* XCTest+SwiftyMarkdown.swift in Sources */,
|
||||
F4A48AB923A8516E00713437 /* SwiftyMarkdownAttributedStringTests copy.swift in Sources */,
|
||||
F40D3A7F23A807F60085CF6E /* SwiftyMarkdownCharacterTests.swift in Sources */,
|
||||
F4CE98911C8A921300D735C1 /* SwiftyMarkdownLineTests.swift in Sources */,
|
||||
F40D3A8423A813240085CF6E /* SwiftyMarkdownPerformanceTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -238,18 +284,28 @@
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -267,7 +323,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -286,18 +342,28 @@
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -309,9 +375,10 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -322,57 +389,78 @@
|
||||
F4CE98961C8A921300D735C1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BITCODE_GENERATION_MODE = bitcode;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 12;
|
||||
DYLIB_CURRENT_VERSION = 19;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_CFLAGS = "-fembed-bitcode";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F4CE98971C8A921300D735C1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BITCODE_GENERATION_MODE = bitcode;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 12;
|
||||
DYLIB_CURRENT_VERSION = 19;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = SwiftyMarkdown/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_CFLAGS = "-fembed-bitcode";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdown;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
F4CE98991C8A921300D735C1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 52T262DA8V;
|
||||
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F4CE989A1C8A921300D735C1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 52T262DA8V;
|
||||
INFOPLIST_FILE = SwiftyMarkdownTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>SwiftyMarkdownPerformanceTests</key>
|
||||
<dict>
|
||||
<key>testThatFilesAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.01346</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.0064581</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>classNames</key>
|
||||
<dict>
|
||||
<key>SwiftyMarkdownPerformanceTests</key>
|
||||
<dict>
|
||||
<key>testThatFilesAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.1</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
<key>maxPercentRelativeStandardDeviation</key>
|
||||
<real>5</real>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.01</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
<key>maxPercentRelativeStandardDeviation</key>
|
||||
<real>5</real>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>testThatVeryLongStringsAreProcessedQuickly()</key>
|
||||
<dict>
|
||||
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||
<dict>
|
||||
<key>baselineAverage</key>
|
||||
<real>0.0392</real>
|
||||
<key>baselineIntegrationDisplayName</key>
|
||||
<string>Local Baseline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>runDestinationsByUUID</key>
|
||||
<dict>
|
||||
<key>3EC61D89-CC18-40D5-9AC6-F4504ECE42B5</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>400</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>8-Core Intel Core i9</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>2400</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>16</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro16,1</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>8</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64</string>
|
||||
<key>targetDevice</key>
|
||||
<dict>
|
||||
<key>modelCode</key>
|
||||
<string>iPhone10,6</string>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.iphonesimulator</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>8451F51C-30BE-4B7B-ACFD-E9C42A8D0DC4</key>
|
||||
<dict>
|
||||
<key>localComputer</key>
|
||||
<dict>
|
||||
<key>busSpeedInMHz</key>
|
||||
<integer>400</integer>
|
||||
<key>cpuCount</key>
|
||||
<integer>1</integer>
|
||||
<key>cpuKind</key>
|
||||
<string>8-Core Intel Core i9</string>
|
||||
<key>cpuSpeedInMHz</key>
|
||||
<integer>2400</integer>
|
||||
<key>logicalCPUCoresPerPackage</key>
|
||||
<integer>16</integer>
|
||||
<key>modelCode</key>
|
||||
<string>MacBookPro16,1</string>
|
||||
<key>physicalCPUCoresPerPackage</key>
|
||||
<integer>8</integer>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.macosx</string>
|
||||
</dict>
|
||||
<key>targetArchitecture</key>
|
||||
<string>x86_64</string>
|
||||
<key>targetDevice</key>
|
||||
<dict>
|
||||
<key>modelCode</key>
|
||||
<string>iPhone11,8</string>
|
||||
<key>platformIdentifier</key>
|
||||
<string>com.apple.platform.iphonesimulator</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1120"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
|
||||
BuildableName = "SwiftyMarkdown.framework"
|
||||
BlueprintName = "SwiftyMarkdown"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
|
||||
BuildableName = "SwiftyMarkdown.framework"
|
||||
BlueprintName = "SwiftyMarkdown"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F4CE988A1C8A921300D735C1"
|
||||
BuildableName = "SwiftyMarkdownTests.xctest"
|
||||
BlueprintName = "SwiftyMarkdownTests"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
|
||||
BuildableName = "SwiftyMarkdown.framework"
|
||||
BlueprintName = "SwiftyMarkdown"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F4CE98801C8A921300D735C1"
|
||||
BuildableName = "SwiftyMarkdown.framework"
|
||||
BlueprintName = "SwiftyMarkdown"
|
||||
ReferencedContainer = "container:SwiftyMarkdown.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -15,11 +15,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.1</string>
|
||||
<string>1.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>12</string>
|
||||
<string>19</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// String+SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 08/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Some helper functions based on this:
|
||||
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
|
||||
extension StringProtocol {
|
||||
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.upperBound
|
||||
}
|
||||
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
|
||||
var indices: [Index] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
indices.append(range.lowerBound)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return indices
|
||||
}
|
||||
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
|
||||
var result: [Range<Index>] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
result.append(range)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
case untilClose
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
var closeToken : String? = nil
|
||||
let defaultType : LineStyling
|
||||
public var processEmptyStrings : LineStyling?
|
||||
let lineRules : [LineRule]
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
|
||||
output.removeSubrange(output.startIndex..<range)
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
let token = element.token.trimmingCharacters(in: .whitespaces)
|
||||
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
|
||||
output.removeSubrange(range..<output.endIndex)
|
||||
return output
|
||||
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func processLineLevelAttributes( _ text : String ) -> SwiftyLine? {
|
||||
if text.isEmpty, let style = processEmptyStrings {
|
||||
return SwiftyLine(line: "", lineStyle: style)
|
||||
}
|
||||
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
|
||||
|
||||
for element in lineRules {
|
||||
guard element.token.count > 0 else {
|
||||
continue
|
||||
}
|
||||
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let unprocessed = output
|
||||
|
||||
if let hasToken = self.closeToken, unprocessed != hasToken {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch element.removeFrom {
|
||||
case .leading:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
case .trailing:
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .both:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .entireLine:
|
||||
let maybeOutput = output.replacingOccurrences(of: element.token, with: "")
|
||||
output = ( maybeOutput.isEmpty ) ? maybeOutput : output
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Only if the output has changed in some way
|
||||
guard unprocessed != output else {
|
||||
continue
|
||||
}
|
||||
if element.changeAppliesTo == .untilClose {
|
||||
self.closeToken = (self.closeToken == nil) ? element.token : nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
|
||||
return SwiftyLine(line: output, lineStyle: element.type)
|
||||
|
||||
}
|
||||
|
||||
for element in previousLines {
|
||||
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let charSet = CharacterSet(charactersIn: element.token )
|
||||
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
|
||||
return SwiftyLine(line: "", lineStyle: element.type)
|
||||
}
|
||||
}
|
||||
|
||||
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
for heading in string.components(separatedBy: CharacterSet.newlines) {
|
||||
|
||||
if processEmptyStrings == nil && heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let input = processLineLevelAttributes(String(heading)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .yaml:
|
||||
return body.color
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont {
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
case .h2:
|
||||
style = self.h2
|
||||
case .h3:
|
||||
style = self.h3
|
||||
case .h4:
|
||||
style = self.h4
|
||||
case .h5:
|
||||
style = self.h5
|
||||
case .h6:
|
||||
style = self.h6
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
default:
|
||||
style = self.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
finalSize = NSFont.systemFontSize
|
||||
}
|
||||
var font : NSFont
|
||||
if let existentFontName = fontName {
|
||||
if let customFont = NSFont(name: existentFontName, size: finalSize) {
|
||||
font = customFont
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
|
||||
if globalItalic {
|
||||
let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic)
|
||||
font = NSFont(descriptor: italicDescriptor, size: 0) ?? font
|
||||
}
|
||||
if globalBold {
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
|
||||
font = NSFont(descriptor: boldDescriptor, size: 0) ?? font
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> NSColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList:
|
||||
return body.color
|
||||
case .yaml:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -6,13 +6,13 @@
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
//! Project version number for SwiftyMarkdown.
|
||||
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
|
||||
//FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
|
||||
|
||||
//! Project version string for SwiftyMarkdown.
|
||||
FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
|
||||
//FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SwiftyMarkdown/PublicHeader.h>
|
||||
|
||||
|
||||
+352
-303
@@ -6,93 +6,199 @@
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
|
||||
func isEqualTo(_ other: CharacterStyling) -> Bool {
|
||||
guard let other = other as? CharacterStyle else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FontProperties {
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
case yaml
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : NSColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#else
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A struct defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
public struct BasicStyles : FontProperties {
|
||||
public var fontName : String? = UIFont.preferredFontForTextStyle(UIFontTextStyleBody).fontName
|
||||
public var color = UIColor.blackColor()
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
enum LineType : Int {
|
||||
case H1, H2, H3, H4, H5, H6, Body
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
}
|
||||
|
||||
enum LineStyle : Int {
|
||||
case None
|
||||
case Italic
|
||||
case Bold
|
||||
case Code
|
||||
case Link
|
||||
|
||||
static func styleFromString(string : String ) -> LineStyle {
|
||||
if string == "**" || string == "__" {
|
||||
return .Bold
|
||||
} else if string == "*" || string == "_" {
|
||||
return .Italic
|
||||
} else if string == "`" {
|
||||
return .Code
|
||||
} else if string == "[" {
|
||||
return .Link
|
||||
} else {
|
||||
return .None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
public class SwiftyMarkdown {
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static public var lineRules = [
|
||||
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
public var h1 = BasicStyles()
|
||||
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
public var h2 = BasicStyles()
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
public var h3 = BasicStyles()
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
public var h4 = BasicStyles()
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
public var h5 = BasicStyles()
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
public var h6 = BasicStyles()
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
public var body = BasicStyles()
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
public var link = BasicStyles()
|
||||
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
public var bold = BasicStyles()
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
public var italic = BasicStyles()
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
public var code = BasicStyles()
|
||||
|
||||
open var code = BasicStyles()
|
||||
|
||||
var currentType : LineType = .Body
|
||||
|
||||
public var bullet : String = "・"
|
||||
|
||||
let string : String
|
||||
let instructionSet = NSCharacterSet(charactersInString: "[\\*_`")
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
var string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,6 +208,14 @@ public class SwiftyMarkdown {
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
#if os(macOS)
|
||||
self.setFontColorForAllStyles(with: .labelColor)
|
||||
#elseif !os(watchOS)
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,297 +225,232 @@ public class SwiftyMarkdown {
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : NSURL ) {
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOfURL: url, encoding: NSUTF8StringEncoding) as String
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
fatalError("Couldn't read string")
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
#if os(macOS)
|
||||
self.setFontColorForAllStyles(with: .labelColor)
|
||||
#elseif !os(watchOS)
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
open func setFontColorForAllStyles(with color: NSColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
}
|
||||
#else
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
}
|
||||
#endif
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
public func attributedString() -> NSAttributedString {
|
||||
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
|
||||
if let existentMarkdownString = markdownString {
|
||||
self.string = existentMarkdownString
|
||||
}
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
|
||||
let lines = self.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
|
||||
|
||||
var lineCount = 0
|
||||
|
||||
let headings = ["# ", "## ", "### ", "#### ", "##### ", "###### "]
|
||||
|
||||
var skipLine = false
|
||||
for theLine in lines {
|
||||
lineCount++
|
||||
if skipLine {
|
||||
skipLine = false
|
||||
continue
|
||||
}
|
||||
var line = theLine
|
||||
for heading in headings {
|
||||
|
||||
if let range = line.rangeOfString(heading) where range.startIndex == line.startIndex {
|
||||
|
||||
let startHeadingString = line.stringByReplacingCharactersInRange(range, withString: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
// Remove ending
|
||||
let endHeadingString = heading.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
line = startHeadingString.stringByReplacingOccurrencesOfString(endHeadingString, withString: "").stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
|
||||
currentType = LineType(rawValue: headings.indexOf(heading)!)!
|
||||
|
||||
// We found a heading so break out of the inner loop
|
||||
break
|
||||
}
|
||||
for (idx, line) in foundAttributes.enumerated() {
|
||||
if idx > 0 {
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
|
||||
// Look for underlined headings
|
||||
if lineCount < lines.count {
|
||||
let nextLine = lines[lineCount]
|
||||
|
||||
if let range = nextLine.rangeOfString("=") where range.startIndex == nextLine.startIndex {
|
||||
// Make H1
|
||||
currentType = .H1
|
||||
// We need to skip the next line
|
||||
skipLine = true
|
||||
}
|
||||
|
||||
if let range = nextLine.rangeOfString("-") where range.startIndex == nextLine.startIndex {
|
||||
// Make H2
|
||||
currentType = .H2
|
||||
// We need to skip the next line
|
||||
skipLine = true
|
||||
}
|
||||
}
|
||||
|
||||
// If this is not an empty line...
|
||||
if line.characters.count > 0 {
|
||||
|
||||
// ...start scanning
|
||||
let scanner = NSScanner(string: line)
|
||||
|
||||
// We want to be aware of spaces
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
while !scanner.atEnd {
|
||||
var string : NSString?
|
||||
|
||||
// Get all the characters up to the ones we are interested in
|
||||
if scanner.scanUpToCharactersFromSet(instructionSet, intoString: &string) {
|
||||
|
||||
if let hasString = string as? String {
|
||||
let bodyString = attributedStringFromString(hasString, withStyle: .None)
|
||||
attributedString.appendAttributedString(bodyString)
|
||||
|
||||
let location = scanner.scanLocation
|
||||
|
||||
let matchedCharacters = tagFromScanner(scanner).foundCharacters
|
||||
// If the next string after the characters is a space, then add it to the final string and continue
|
||||
|
||||
let set = NSMutableCharacterSet.whitespaceCharacterSet()
|
||||
set.formUnionWithCharacterSet(NSCharacterSet.punctuationCharacterSet())
|
||||
if scanner.scanUpToCharactersFromSet(set, intoString: nil) {
|
||||
scanner.scanLocation = location
|
||||
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
|
||||
|
||||
} else if matchedCharacters == "[" {
|
||||
scanner.scanLocation = location
|
||||
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner))
|
||||
} else {
|
||||
let charAtts = attributedStringFromString(matchedCharacters, withStyle: .None)
|
||||
attributedString.appendAttributedString(charAtts)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attributedString.appendAttributedString(self.attributedStringFromScanner(scanner, atStartOfLine: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append a new line character to the end of the processed line
|
||||
attributedString.appendAttributedString(NSAttributedString(string: "\n"))
|
||||
currentType = .Body
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func attributedStringFromScanner( scanner : NSScanner, atStartOfLine start : Bool = false) -> NSAttributedString {
|
||||
var followingString : NSString?
|
||||
}
|
||||
|
||||
let results = self.tagFromScanner(scanner)
|
||||
|
||||
var style = LineStyle.styleFromString(results.foundCharacters)
|
||||
|
||||
var attributes = [String : AnyObject]()
|
||||
if style == .Link {
|
||||
|
||||
var linkText : NSString?
|
||||
var linkURL : NSString?
|
||||
let linkCharacters = NSCharacterSet(charactersInString: "]()")
|
||||
|
||||
scanner.scanUpToCharactersFromSet(linkCharacters, intoString: &linkText)
|
||||
scanner.scanCharactersFromSet(linkCharacters, intoString: nil)
|
||||
scanner.scanUpToCharactersFromSet(linkCharacters, intoString: &linkURL)
|
||||
scanner.scanCharactersFromSet(linkCharacters, intoString: nil)
|
||||
|
||||
|
||||
if let hasLink = linkText, hasURL = linkURL {
|
||||
followingString = hasLink as String
|
||||
attributes[NSLinkAttributeName] = hasURL as String
|
||||
} else {
|
||||
style = .None
|
||||
}
|
||||
} else {
|
||||
scanner.scanUpToCharactersFromSet(instructionSet, intoString: &followingString)
|
||||
}
|
||||
|
||||
let attributedString = attributedStringFromString(results.escapedCharacters, withStyle: style).mutableCopy() as! NSMutableAttributedString
|
||||
if let hasString = followingString as? String {
|
||||
|
||||
|
||||
|
||||
let prefix = ( style == .Code && start ) ? "\t" : ""
|
||||
let attString = attributedStringFromString(prefix + hasString, withStyle: style, attributes: attributes)
|
||||
attributedString.appendAttributedString(attString)
|
||||
}
|
||||
let suffix = self.tagFromScanner(scanner)
|
||||
attributedString.appendAttributedString(attributedStringFromString(suffix.escapedCharacters, withStyle: style))
|
||||
|
||||
return attributedString
|
||||
}
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func tagFromScanner( scanner : NSScanner ) -> (foundCharacters : String, escapedCharacters : String) {
|
||||
var matchedCharacters : String = ""
|
||||
var tempCharacters : NSString?
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
// Scan the ones we are interested in
|
||||
while scanner.scanCharactersFromSet(instructionSet, intoString: &tempCharacters) {
|
||||
if let chars = tempCharacters as? String {
|
||||
matchedCharacters = matchedCharacters + chars
|
||||
}
|
||||
}
|
||||
var foundCharacters : String = ""
|
||||
|
||||
while matchedCharacters.containsString("\\") {
|
||||
if let hasRange = matchedCharacters.rangeOfString("\\") {
|
||||
|
||||
let newRange = Range(start: hasRange.startIndex, end: hasRange.endIndex.advancedBy(1))
|
||||
foundCharacters = foundCharacters + matchedCharacters.substringWithRange(newRange)
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
|
||||
matchedCharacters.removeRange(newRange)
|
||||
}
|
||||
let lineProperties : LineProperties
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
|
||||
}
|
||||
|
||||
|
||||
return (matchedCharacters, foundCharacters.stringByReplacingOccurrencesOfString("\\", withString: ""))
|
||||
}
|
||||
|
||||
|
||||
// Make H1
|
||||
|
||||
func attributedStringFromString(string : String, withStyle style : LineStyle, var attributes : [String : AnyObject] = [:] ) -> NSAttributedString {
|
||||
let textStyle : String
|
||||
var fontName : String?
|
||||
|
||||
// What type are we and is there a font name set?
|
||||
|
||||
|
||||
switch currentType {
|
||||
case .H1:
|
||||
fontName = h1.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle1
|
||||
} else {
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
}
|
||||
attributes[NSForegroundColorAttributeName] = h1.color
|
||||
case .H2:
|
||||
fontName = h2.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
} else {
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
}
|
||||
attributes[NSForegroundColorAttributeName] = h2.color
|
||||
case .H3:
|
||||
fontName = h3.fontName
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFontTextStyleTitle2
|
||||
} else {
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
}
|
||||
attributes[NSForegroundColorAttributeName] = h3.color
|
||||
case .H4:
|
||||
fontName = h4.fontName
|
||||
textStyle = UIFontTextStyleHeadline
|
||||
attributes[NSForegroundColorAttributeName] = h4.color
|
||||
case .H5:
|
||||
fontName = h5.fontName
|
||||
textStyle = UIFontTextStyleSubheadline
|
||||
attributes[NSForegroundColorAttributeName] = h5.color
|
||||
case .H6:
|
||||
fontName = h6.fontName
|
||||
textStyle = UIFontTextStyleFootnote
|
||||
attributes[NSForegroundColorAttributeName] = h6.color
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
paragraphStyle.headIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList:
|
||||
lineProperties = body
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
let nonOptions = [NSTextTab.OptionKey: Any]()
|
||||
paragraphStyle.tabStops = [
|
||||
NSTextTab(textAlignment: .left, location: 20, options: nonOptions)]
|
||||
paragraphStyle.defaultTabInterval = 20
|
||||
paragraphStyle.headIndent = 20
|
||||
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
finalTokens.insert(Token(type: .string, inputString: "\(self.bullet)\t"), at: 0)
|
||||
default:
|
||||
fontName = body.fontName
|
||||
textStyle = UIFontTextStyleBody
|
||||
attributes[NSForegroundColorAttributeName] = body.color
|
||||
lineProperties = body
|
||||
break
|
||||
}
|
||||
|
||||
// Check for code
|
||||
|
||||
if style == .Code {
|
||||
fontName = code.fontName
|
||||
attributes[NSForegroundColorAttributeName] = code.color
|
||||
if lineProperties.alignment != .left {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
|
||||
if style == .Link {
|
||||
fontName = link.fontName
|
||||
attributes[NSForegroundColorAttributeName] = link.color
|
||||
}
|
||||
|
||||
// Fallback to body
|
||||
if let _ = fontName {
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.link] = nil
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
} else {
|
||||
fontName = body.fontName
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
#if !os(macOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#elseif !os(watchOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = NSImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
#endif
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
let font = UIFont.preferredFontForTextStyle(textStyle)
|
||||
let styleDescriptor = font.fontDescriptor()
|
||||
let styleSize = styleDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as? CGFloat ?? CGFloat(14)
|
||||
|
||||
var finalFont : UIFont
|
||||
if let finalFontName = fontName, font = UIFont(name: finalFontName, size: styleSize) {
|
||||
finalFont = font
|
||||
} else {
|
||||
finalFont = UIFont.preferredFontForTextStyle(textStyle)
|
||||
}
|
||||
|
||||
let finalFontDescriptor = finalFont.fontDescriptor()
|
||||
if style == .Italic {
|
||||
let italicDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitItalic)
|
||||
finalFont = UIFont(descriptor: italicDescriptor, size: styleSize)
|
||||
}
|
||||
if style == .Bold {
|
||||
let boldDescriptor = finalFontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
|
||||
finalFont = UIFont(descriptor: boldDescriptor, size: styleSize)
|
||||
}
|
||||
|
||||
|
||||
attributes[NSFontAttributeName] = finalFont
|
||||
|
||||
return NSAttributedString(string: string, attributes: attributes)
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
func isEqualTo( _ other : CharacterStyling ) -> Bool
|
||||
}
|
||||
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule : CustomStringConvertible {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public var description: String {
|
||||
return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) "
|
||||
}
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case string
|
||||
case escape
|
||||
case replacement
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public let type : TokenType
|
||||
public let inputString : String
|
||||
public fileprivate(set) var metadataString : String? = nil
|
||||
public fileprivate(set) var characterStyles : [CharacterStyling] = []
|
||||
public fileprivate(set) var count : Int = 0
|
||||
public fileprivate(set) var shouldSkip : Bool = false
|
||||
public fileprivate(set) var tokenIndex : Int = -1
|
||||
public fileprivate(set) var isProcessed : Bool = false
|
||||
public fileprivate(set) var isMetadata : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count <= 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .escape, .string:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .replacement:
|
||||
return self.inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
|
||||
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
|
||||
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
|
||||
newToken.metadataString = self.metadataString
|
||||
newToken.isMetadata = self.isMetadata
|
||||
newToken.isProcessed = self.isProcessed
|
||||
return newToken
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Iterator.Element == Token {
|
||||
var oslogDisplay: String {
|
||||
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
var replacements : [String : [Token]] = [:]
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
|
||||
/// This goes through every CharacterRule in order and applies it to the input string, tokenising the string
|
||||
/// if there are any matches.
|
||||
///
|
||||
/// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to
|
||||
/// those outside them.
|
||||
///
|
||||
/// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately.
|
||||
///
|
||||
/// This is to prevent situations like **\[link**\](url) from returing a bold string.
|
||||
///
|
||||
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
|
||||
|
||||
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
os_log("------------------------------", log: .tokenising, type: .info)
|
||||
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
|
||||
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
|
||||
var outerStringTokens : [Token] = []
|
||||
var innerStringTokens : [Token] = []
|
||||
var isOuter = true
|
||||
for idx in 0..<currentTokens.count {
|
||||
let nextToken = currentTokens[idx]
|
||||
if nextToken.type == .openTag && nextToken.isProcessed {
|
||||
isOuter = false
|
||||
}
|
||||
if nextToken.type == .closeTag {
|
||||
let ref = UUID().uuidString
|
||||
outerStringTokens.append(Token(type: .replacement, inputString: ref))
|
||||
innerStringTokens.append(nextToken)
|
||||
self.replacements[ref] = self.handleReplacementTokens(innerStringTokens, with: nextRule)
|
||||
innerStringTokens.removeAll()
|
||||
isOuter = true
|
||||
continue
|
||||
}
|
||||
(isOuter) ? outerStringTokens.append(nextToken) : innerStringTokens.append(nextToken)
|
||||
}
|
||||
|
||||
currentTokens = self.handleReplacementTokens(outerStringTokens, with: nextRule)
|
||||
|
||||
var finalTokens : [Token] = []
|
||||
for token in currentTokens {
|
||||
guard token.type == .replacement else {
|
||||
finalTokens.append(token)
|
||||
continue
|
||||
}
|
||||
if let hasReplacement = self.replacements[token.inputString] {
|
||||
os_log("Found replacement for %@", log: .tokenising, type: .info, token.inputString)
|
||||
for var repToken in hasReplacement {
|
||||
guard repToken.type == .string else {
|
||||
finalTokens.append(repToken)
|
||||
continue
|
||||
}
|
||||
for style in token.characterStyles {
|
||||
if !repToken.characterStyles.contains(where: { $0.isEqualTo(style)}) {
|
||||
repToken.characterStyles.append(contentsOf: token.characterStyles)
|
||||
}
|
||||
}
|
||||
|
||||
finalTokens.append(repToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
currentTokens = finalTokens
|
||||
|
||||
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
}
|
||||
|
||||
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
|
||||
os_log("==================================", log: .tokenising, type: .info)
|
||||
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// In order to reinsert the original replacements into the new string token, the replacements
|
||||
/// need to be searched for in the incoming string one by one.
|
||||
///
|
||||
/// Using the `newToken(fromSubstring:isReplacement:)` function ensures that any metadata and character styles
|
||||
/// are passed over into the newly created tokens.
|
||||
///
|
||||
/// E.g. A string token that has an `outputString` of "This string AAAAA-BBBBB-CCCCC replacements", with
|
||||
/// a characterStyle of `bold` for the entire string, needs to be separated into the following tokens:
|
||||
///
|
||||
/// - `string`: "This string "
|
||||
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " replacements"
|
||||
///
|
||||
/// Each of these need to have a character style of `bold`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - replacements: An array of `replacement` tokens
|
||||
/// - token: The new `string` token that may contain replacement IDs contained in the `replacements` array
|
||||
func reinsertReplacements(_ replacements : [Token], from stringToken : Token ) -> [Token] {
|
||||
guard !stringToken.outputString.isEmpty && !replacements.isEmpty else {
|
||||
return [stringToken]
|
||||
}
|
||||
var outputTokens : [Token] = []
|
||||
let scanner = Scanner(string: stringToken.outputString)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
// Remove any replacements that don't appear in the incoming string
|
||||
var repTokens = replacements.filter({ stringToken.outputString.contains($0.inputString) })
|
||||
|
||||
var testString = "\n"
|
||||
while !scanner.isAtEnd {
|
||||
var outputString : String = ""
|
||||
if repTokens.count > 0 {
|
||||
testString = repTokens.removeFirst().inputString
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
if let nextString = scanner.scanUpToString(testString) {
|
||||
outputString = nextString
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
|
||||
if let outputToken = scanner.scanString(testString) {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else if let outputToken = scanner.scanString(testString) {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else {
|
||||
var oldString : NSString? = nil
|
||||
var tokenString : NSString? = nil
|
||||
scanner.scanUpTo(testString, into: &oldString)
|
||||
if let nextString = oldString {
|
||||
outputString = nextString as String
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
|
||||
scanner.scanString(testString, into: &tokenString)
|
||||
if let outputToken = tokenString as String? {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else {
|
||||
scanner.scanString(testString, into: &tokenString)
|
||||
if let outputToken = tokenString as String? {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputTokens
|
||||
}
|
||||
|
||||
|
||||
/// This function is necessary because a previously tokenised string might have
|
||||
///
|
||||
/// Consider a previously tokenised string, where AAAAA-BBBBB-CCCCC represents a replaced \[link\](url) instance.
|
||||
///
|
||||
/// The incoming tokens will look like this:
|
||||
///
|
||||
/// - `string`: "A \*\*Bold"
|
||||
/// - `replacement` : "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " with a trailing string**"
|
||||
///
|
||||
/// However, because the scanner can only tokenise individual strings, passing in the string values
|
||||
/// of these tokens individually and applying the styles will not correctly detect the starting and
|
||||
/// ending `repeatingTag` instances. (e.g. the scanner will see "A \*\*Bold", and then "AAAAA-BBBBB-CCCCC",
|
||||
/// and finally " with a trailing string\*\*")
|
||||
///
|
||||
/// The strings need to be combined, so that they form a single string:
|
||||
/// A \*\*Bold AAAAA-BBBBB-CCCCC with a trailing string\*\*.
|
||||
/// This string is then parsed and tokenised so that it looks like this:
|
||||
///
|
||||
/// - `string`: "A "
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
/// - `string`: "Bold AAAAA-BBBBB-CCCCC with a trailing string"
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
///
|
||||
/// Finally, the replacements from the original incoming token array are searched for and pulled out
|
||||
/// of this new string, so the final result looks like this:
|
||||
///
|
||||
/// - `string`: "A "
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
/// - `string`: "Bold "
|
||||
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " with a trailing string"
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tokens: The tokens to be combined, scanned, re-tokenised, and merged
|
||||
/// - rule: The character rule currently being applied
|
||||
func scanReplacementTokens( _ tokens : [Token], with rule : CharacterRule ) -> [Token] {
|
||||
guard tokens.count > 0 else {
|
||||
return []
|
||||
}
|
||||
|
||||
let combinedString = tokens.map({ $0.outputString }).joined()
|
||||
|
||||
let nextTokens = self.scan(combinedString, with: rule)
|
||||
var replacedTokens = self.applyStyles(to: nextTokens, usingRule: rule)
|
||||
|
||||
/// It's necessary here to check to see if the first token (which will always represent the styles
|
||||
/// to be applied from previous scans) has any existing metadata or character styles and apply them
|
||||
/// to *all* the string and replacement tokens found by the new scan.
|
||||
for idx in 0..<replacedTokens.count {
|
||||
guard replacedTokens[idx].type == .string || replacedTokens[idx].type == .replacement else {
|
||||
continue
|
||||
}
|
||||
if tokens.first!.metadataString != nil && replacedTokens[idx].metadataString == nil {
|
||||
replacedTokens[idx].metadataString = tokens.first!.metadataString
|
||||
}
|
||||
replacedTokens[idx].characterStyles.append(contentsOf: tokens.first!.characterStyles)
|
||||
}
|
||||
|
||||
// Swap the original replacement tokens back in
|
||||
let replacements = tokens.filter({ $0.type == .replacement })
|
||||
var outputTokens : [Token] = []
|
||||
for token in replacedTokens {
|
||||
guard token.type == .string else {
|
||||
outputTokens.append(token)
|
||||
continue
|
||||
}
|
||||
outputTokens.append(contentsOf: self.reinsertReplacements(replacements, from: token))
|
||||
}
|
||||
|
||||
return outputTokens
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// This function ensures that only concurrent `string` and `replacement` tokens are processed together.
|
||||
///
|
||||
/// i.e. If there is an existing `repeatingTag` token between two strings, then those strings will be
|
||||
/// processed individually. This prevents incorrect parsing of strings like "\*\*\_Should only be bold\*\*\_"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - incomingTokens: A group of tokens whose string tokens and replacement tokens should be combined and re-tokenised
|
||||
/// - rule: The current rule being processed
|
||||
func handleReplacementTokens( _ incomingTokens : [Token], with rule : CharacterRule) -> [Token] {
|
||||
|
||||
// Only combine string and replacements that are next to each other.
|
||||
var newTokenSet : [Token] = []
|
||||
var currentTokenSet : [Token] = []
|
||||
for i in 0..<incomingTokens.count {
|
||||
guard incomingTokens[i].type == .string || incomingTokens[i].type == .replacement else {
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
newTokenSet.append(incomingTokens[i])
|
||||
currentTokenSet.removeAll()
|
||||
continue
|
||||
}
|
||||
guard !incomingTokens[i].isProcessed && !incomingTokens[i].isMetadata && !incomingTokens[i].shouldSkip else {
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
newTokenSet.append(incomingTokens[i])
|
||||
currentTokenSet.removeAll()
|
||||
continue
|
||||
}
|
||||
currentTokenSet.append(incomingTokens[i])
|
||||
}
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
|
||||
return newTokenSet
|
||||
}
|
||||
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag && !$0.isProcessed }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag && !$0.isProcessed }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].isMetadata = true
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].isProcessed = true
|
||||
tokens[metadataIndex].isProcessed = true
|
||||
tokens[index].isProcessed = true
|
||||
}
|
||||
|
||||
|
||||
/// This is here to manage how opening tags are matched with closing tags when they're all the same
|
||||
/// character.
|
||||
///
|
||||
/// Of course, because Markdown is about as loose as a spec can be while still being considered any
|
||||
/// kind of spec, the number of times this character repeats causes different effects. Then there
|
||||
/// is the ill-defined way it should work if the number of opening and closing tags are different.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - index: The index of the current token in the loop
|
||||
/// - tokens: An inout variable of the loop tokens of interest
|
||||
/// - rule: The character rule being applied
|
||||
func handleClosingTagFromRepeatingTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule) {
|
||||
let theToken = tokens[index]
|
||||
os_log("Found repeating tag with tag count: %i, tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString, rule.openTag )
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let startIdx = index
|
||||
var endIdx : Int? = nil
|
||||
|
||||
let maxCount = (theToken.count > rule.maxTags) ? rule.maxTags : theToken.count
|
||||
// Try to find exact match first
|
||||
if let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id && !$0.isProcessed }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
|
||||
if endIdx == nil, let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count >= 1 && $0.id != theToken.id && !$0.isProcessed }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[maxCount] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
tokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
|
||||
let maxEnd = (tokens[existentEnd].count > rule.maxTags) ? rule.maxTags : tokens[existentEnd].count
|
||||
tokens[index].count = theToken.count - maxEnd
|
||||
tokens[existentEnd].count = tokens[existentEnd].count - maxEnd
|
||||
if maxEnd < rule.maxTags {
|
||||
self.handleClosingTagFromRepeatingTag(withIndex: index, in: &tokens, following: rule)
|
||||
} else {
|
||||
tokens[existentEnd].isProcessed = true
|
||||
tokens[index].isProcessed = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
|
||||
os_log("Applying styles to tokens: %@", log: .tokenising, type: .info, tokens.oslogDisplay )
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
os_log("Found escape: %@", log: .tokenising, type: .info, token.inputString )
|
||||
case .repeatingTag:
|
||||
self.handleClosingTagFromRepeatingTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
os_log("Found repeating tag with tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.inputString, rule.openTag )
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
os_log("Found close tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
if theToken.isMetadata {
|
||||
os_log("Found Metadata: %@", log: .tokenising, type: .info, theToken.inputString )
|
||||
} else {
|
||||
os_log("Found String: %@", log: .tokenising, type: .info, theToken.inputString )
|
||||
}
|
||||
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
os_log("...with metadata: %@", log: .tokenising, type: .info, hasMetadata )
|
||||
}
|
||||
case .replacement:
|
||||
os_log("Found replacement with ID: %@", log: .tokenising, type: .info, mutableTokens[idx].inputString )
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
} else {
|
||||
lastChar = nil
|
||||
}
|
||||
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0,tvOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
} else {
|
||||
nextChar = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -233,25 +233,28 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0720;
|
||||
LastUpgradeCheck = 0720;
|
||||
LastUpgradeCheck = 1120;
|
||||
ORGANIZATIONNAME = "Voyage Travel Apps";
|
||||
TargetAttributes = {
|
||||
F4CE98A71C8AEF7D00D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
F4CE98BB1C8AEF7D00D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
TestTargetID = F4CE98A71C8AEF7D00D735C1;
|
||||
};
|
||||
F4CE98C61C8AEF7D00D735C1 = {
|
||||
CreatedOnToolsVersion = 7.2.1;
|
||||
LastSwiftMigration = 1120;
|
||||
TestTargetID = F4CE98A71C8AEF7D00D735C1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = F4CE98A31C8AEF7D00D735C1 /* Build configuration list for PBXProject "SwiftyMarkdownExample" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -385,17 +388,28 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
@@ -417,7 +431,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -430,17 +444,28 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
@@ -456,9 +481,10 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
@@ -472,6 +498,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -483,6 +511,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -494,6 +524,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -506,6 +538,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMarkdownExample.app/SwiftyMarkdownExample";
|
||||
};
|
||||
name = Release;
|
||||
@@ -517,6 +551,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SwiftyMarkdownExample;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
@@ -529,6 +565,8 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.voyagetravelapps.SwiftyMarkdownExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SwiftyMarkdownExample;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
|
||||
@@ -14,30 +14,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
|
||||
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(application: UIApplication) {
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(application: UIApplication) {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(application: UIApplication) {
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(application: UIApplication) {
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(application: UIApplication) {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
@@ -30,6 +40,16 @@
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
@@ -59,6 +79,16 @@
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+21
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait" appearance="dark"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
@@ -14,32 +15,65 @@
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" usesAttributedText="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qZP-CU-74n">
|
||||
<rect key="frame" x="20" y="20" width="560" height="580"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="583"/>
|
||||
<attributedString key="attributedText"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<dataDetectorType key="dataDetectorTypes" link="YES"/>
|
||||
</textView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="je8-G1-qej">
|
||||
<rect key="frame" x="24" y="591" width="327" height="68"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="EOv-1r-ixy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="327" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
</textField>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="XzJ-SY-mRx">
|
||||
<rect key="frame" x="0.0" y="34" width="327" height="34"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ksq-jO-8Gt">
|
||||
<rect key="frame" x="0.0" y="0.0" width="163.5" height="34"/>
|
||||
<state key="normal" title="Reload File"/>
|
||||
<connections>
|
||||
<action selector="reloadText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="dg3-xi-352"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JK2-Lo-xRa">
|
||||
<rect key="frame" x="163.5" y="0.0" width="163.5" height="34"/>
|
||||
<state key="normal" title="Process Text"/>
|
||||
<connections>
|
||||
<action selector="processText:" destination="BYZ-38-t0r" eventType="touchUpInside" id="e9I-rJ-ifc"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" id="1yU-8N-26a"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" id="7lJ-OQ-4eh"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="qZP-CU-74n" secondAttribute="trailing" id="F5p-iG-zTB"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="je8-G1-qej" secondAttribute="trailing" constant="8" id="JKP-2n-p24"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="je8-G1-qej" secondAttribute="bottom" constant="8" id="cvL-k7-iKp"/>
|
||||
<constraint firstItem="je8-G1-qej" firstAttribute="top" secondItem="qZP-CU-74n" secondAttribute="bottom" constant="8" id="gnq-BF-hFJ"/>
|
||||
<constraint firstItem="je8-G1-qej" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="8" id="kJ0-Hf-RwN"/>
|
||||
<constraint firstItem="qZP-CU-74n" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="srf-u0-j0n"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="textField" destination="EOv-1r-ixy" id="k2p-vH-CR0"/>
|
||||
<outlet property="textView" destination="qZP-CU-74n" id="VO1-kx-Lpd"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="24.800000000000001" y="34.632683658170919"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -11,31 +11,46 @@ import SwiftyMarkdown
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
|
||||
@IBOutlet weak var textField : UITextField!
|
||||
@IBOutlet weak var textView : UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
// This is to help debugging.
|
||||
reloadText(nil)
|
||||
|
||||
self.textView.dataDetectorTypes = UIDataDetectorTypes.All
|
||||
if let url = NSBundle.mainBundle().URLForResource("example", withExtension: "md"), md = SwiftyMarkdown(url: url) {
|
||||
self.textField.text = "Yo I'm a *single* line **string**. How do I look?"
|
||||
}
|
||||
|
||||
@IBAction func processText( _ sender : UIButton? ) {
|
||||
guard let existentText = self.textField.text else {
|
||||
return
|
||||
}
|
||||
self.textView.attributedText = SwiftyMarkdown(string: existentText).attributedString()
|
||||
}
|
||||
|
||||
@IBAction func reloadText( _ sender : UIButton? ) {
|
||||
|
||||
self.textView.dataDetectorTypes = UIDataDetectorTypes.all
|
||||
|
||||
if let url = Bundle.main.url(forResource: "example", withExtension: "md"), let md = SwiftyMarkdown(url: url) {
|
||||
md.h2.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h2.color = UIColor.redColor()
|
||||
md.h2.color = UIColor.red
|
||||
md.h2.alignment = .center
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
md.blockquotes.fontStyle = .italic
|
||||
|
||||
md.underlineLinks = true
|
||||
|
||||
self.textView.attributedText = md.attributedString()
|
||||
|
||||
} else {
|
||||
fatalError("Error loading file")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
# Swifty Markdown
|
||||
|
||||
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even if you use custom fonts.
|
||||
SwiftyMarkdown is a Swift-based *Markdown* parser that converts *Markdown* files or strings into **NSAttributedStrings**. It uses sensible defaults and supports dynamic type, even with custom fonts.
|
||||
|
||||
## Features
|
||||
Show Images From Your App Bundle!
|
||||
---
|
||||

|
||||
|
||||
Customise fonts and colors easily in a Swift-like way:
|
||||
|
||||
md.code.fontName = "CourierNewPSMT"
|
||||
|
||||
md.h2.fontName = "AvenirNextCondensed-Medium"
|
||||
md.h2.color = UIColor.redColor()
|
||||
md.h2.alignment = .center
|
||||
|
||||
It supports the standard Markdown syntax, like *italics*, _underline italics_, **bold**, `backticks for code` and headings.
|
||||
|
||||
It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and \`backticks\`. It also supports inline Markdown [Links](http://voyagetravelapps.com/).
|
||||
|
||||
> It also now supports blockquotes
|
||||
> and it supports whole-line italic and bold styles so you can go completely wild with styling! Wow! Such styles! Much fun!
|
||||
|
||||
**List**
|
||||
|
||||
- And
|
||||
- Unordered
|
||||
- Lists
|
||||
- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
Customise fonts and colours easily in a Swift-like way:
|
||||
|
||||
`md.code.fontName = "CourierNewPSMT"`
|
||||
|
||||
`md.h2.fontName = "AvenirNextCondensed-Medium"`
|
||||
`md.h2.color = UIColor.redColor()`
|
||||
|
||||
*An italic line*
|
||||
|
||||
It ignores random * and correctly handles escaped \*asterisks\* and \_underlines\_ and has error handling for mismatched tags (\*\*bold\* == **bold*). It also supports inline Markdown [Links](http://voyagetravelapps.com/)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class SwiftyMarkdownExampleTests: XCTestCase {
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock {
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.1</string>
|
||||
<string>1.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>12</string>
|
||||
<string>19</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// SwiftyMarkdownAttributedStringTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownAttributedStringTests: XCTestCase {
|
||||
|
||||
func testThatAttributesAreAppliedCorrectly() {
|
||||
|
||||
let string = """
|
||||
# Heading 1
|
||||
|
||||
A more *complicated* example. This one has **it all**. Here is a [link](http://voyagetravelapps.com/).
|
||||
|
||||
## Heading 2
|
||||
|
||||
## Heading 3
|
||||
|
||||
> This one is a blockquote
|
||||
"""
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
let attributedString = md.attributedString()
|
||||
|
||||
XCTAssertNotNil(attributedString)
|
||||
|
||||
XCTAssertEqual(attributedString.string, "Heading 1\n\nA more complicated example. This one has it all. Here is a link.\n\nHeading 2\n\nHeading 3\n\nThis one is a blockquote")
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
//
|
||||
// SwiftyMarkdownCharacterTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import SwiftyMarkdown
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class SwiftyMarkdownCharacterTests: XCTestCase {
|
||||
|
||||
func testIsolatedCase() {
|
||||
let challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
|
||||
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
let results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
|
||||
func testThatRegularTraitsAreParsedCorrectly() {
|
||||
|
||||
var challenge = TokenTest(input: "**A bold string**", output: "A bold string", tokens: [
|
||||
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a **bold** word", output: "A string with a bold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "`Code (**should** not process internal tags)`", output: "Code (**should** not process internal tags)", tokens: [
|
||||
Token(type: .string, inputString: "Code (**should** not process internal tags) ", characterStyles: [CharacterStyle.code])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with `code` (should not be indented)", output: "A string with code (should not be indented)", tokens : [
|
||||
Token(type: .string, inputString: "A string with ", characterStyles: []),
|
||||
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " (should not be indented)", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "*An italicised string*", output: "An italicised string", tokens : [
|
||||
Token(type: .string, inputString: "An italicised string", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with *italicised* text", output: "A string with italicised text", tokens : [
|
||||
Token(type: .string, inputString: "A string with ", characterStyles: []),
|
||||
Token(type: .string, inputString: "italicised", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " text", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "__A bold string__ with a **mix** **of** bold __styles__", output: "A bold string with a mix of bold styles", tokens : [
|
||||
Token(type: .string, inputString: "A bold string", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " bold ", characterStyles: []),
|
||||
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.bold])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "`A code string` with multiple `code` `instances`", output: "A code string with multiple code instances", tokens : [
|
||||
Token(type: .string, inputString: "A code string", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " with multiple ", characterStyles: []),
|
||||
Token(type: .string, inputString: "code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "instances", characterStyles: [CharacterStyle.code])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "_An italic string_ with a *mix* _of_ italic *styles*", output: "An italic string with a mix of italic styles", tokens : [
|
||||
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "mix", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "of", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " italic ", characterStyles: []),
|
||||
Token(type: .string, inputString: "styles", characterStyles: [CharacterStyle.italic])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety.", output: "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.", tokens : [
|
||||
Token(type: .string, inputString: "An italic string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: ", ", characterStyles: []),
|
||||
Token(type: .string, inputString: "followed by a bold one", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: ", ", characterStyles: []),
|
||||
Token(type: .string, inputString: "with some code", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: ", **and some** _escaped_ `characters`, ", characterStyles: []),
|
||||
Token(type: .string, inputString: "ending", characterStyles: [CharacterStyle.code]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "with", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "more", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " variety.", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatExtraCharactersAreHandles() {
|
||||
var challenge = TokenTest(input: "***A bold italic string***", output: "A bold italic string", tokens: [
|
||||
Token(type: .string, inputString: "A bold italic string", characterStyles: [CharacterStyle.bold, CharacterStyle.italic])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ****bold italic**** word", output: "A string with a *bold italic* word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*bold italic*", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ****bold italic*** word", output: "A string with a *bold italic word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "*bold italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a ***bold** italic* word", output: "A string with a bold italic word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " italic", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with a **bold*italic*bold** word", output: "A string with a bolditalicbold word", tokens: [
|
||||
Token(type: .string, inputString: "A string with a ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "italic", characterStyles: [CharacterStyle.bold, CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " word", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// The new version of SwiftyMarkdown is a lot more strict than the old version, although this may change in future
|
||||
func offtestThatMarkdownMistakesAreHandledAppropriately() {
|
||||
let mismatchedBoldCharactersAtStart = "**This should be bold*"
|
||||
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
|
||||
|
||||
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "This should be bold")
|
||||
|
||||
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string that should be italic")
|
||||
|
||||
}
|
||||
|
||||
func testThatEscapedCharactersAreEscapedCorrectly() {
|
||||
var challenge = TokenTest(input: "\\*\\*A normal string\\*\\*", output: "**A normal string**", tokens: [
|
||||
Token(type: .string, inputString: "**A normal string**", characterStyles: [])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with double \\*\\*escaped\\*\\* asterisks", output: "A string with double **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with double **escaped** asterisks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\_A normal string\\_", output: "_A normal string_", tokens: [
|
||||
Token(type: .string, inputString: "_A normal string_", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with \\_escaped\\_ underscores", output: "A string with _escaped_ underscores", tokens: [
|
||||
Token(type: .string, inputString: "A string with _escaped_ underscores", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\`A normal string\\`", output: "`A normal string`", tokens: [
|
||||
Token(type: .string, inputString: "`A normal string`", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with \\`escaped\\` backticks", output: "A string with `escaped` backticks", tokens: [
|
||||
Token(type: .string, inputString: "A string with `escaped` backticks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "\\**One escaped, one not at either end\\**", output: "*One escaped, one not at either end*", tokens: [
|
||||
Token(type: .string, inputString: "*", characterStyles: []),
|
||||
Token(type: .string, inputString: "One escaped, one not at either end*", characterStyles: [CharacterStyle.italic]),
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with one \\**escaped\\** asterisk, one not at either end", output: "A string with one *escaped* asterisk, one not at either end", tokens: [
|
||||
Token(type: .string, inputString: "A string with one *", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped*", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: " asterisk, one not at either end", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
func offtestAdvancedEscaping() {
|
||||
|
||||
var challenge = TokenTest(input: "\\***A normal string*\\**", output: "**A normal string*", tokens: [
|
||||
Token(type: .string, inputString: "**", characterStyles: []),
|
||||
Token(type: .string, inputString: "A normal string", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "**", characterStyles: [])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A string with randomly *\\**escaped**\\* asterisks", output: "A string with randomly **escaped** asterisks", tokens: [
|
||||
Token(type: .string, inputString: "A string with randomly **", characterStyles: []),
|
||||
Token(type: .string, inputString: "escaped", characterStyles: [CharacterStyle.italic]),
|
||||
Token(type: .string, inputString: "** asterisks", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
}
|
||||
|
||||
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
|
||||
let asteriskSpace = """
|
||||
An asterisk followed by a space: *
|
||||
Line break
|
||||
"""
|
||||
let backtickSpace = "A backtick followed by a space: `"
|
||||
let underscoreSpace = "An underscore followed by a space: _"
|
||||
|
||||
let asteriskFullStop = "Two asterisks followed by a full stop: **."
|
||||
let backtickFullStop = "Two backticks followed by a full stop: ``."
|
||||
let underscoreFullStop = "Two underscores followed by a full stop: __."
|
||||
|
||||
let asteriskComma = "An asterisk followed by a full stop: *, *"
|
||||
let backtickComma = "A backtick followed by a space: `, `"
|
||||
let underscoreComma = "An underscore followed by a space: _, _"
|
||||
|
||||
let asteriskWithBold = "A **bold** word followed by an asterisk * "
|
||||
let backtickWithCode = "A `code` word followed by a backtick ` "
|
||||
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
|
||||
|
||||
var md = SwiftyMarkdown(string: asteriskSpace)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskSpace)
|
||||
|
||||
md = SwiftyMarkdown(string: backtickSpace)
|
||||
XCTAssertEqual(md.attributedString().string, backtickSpace)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreSpace)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreSpace)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskFullStop)
|
||||
|
||||
md = SwiftyMarkdown(string: backtickFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, backtickFullStop)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreFullStop)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskComma)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskComma)
|
||||
|
||||
md = SwiftyMarkdown(string: backtickComma)
|
||||
XCTAssertEqual(md.attributedString().string, backtickComma)
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreComma)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreComma)
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskWithBold)
|
||||
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk *")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickWithCode)
|
||||
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick `")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreWithItalic)
|
||||
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _")
|
||||
|
||||
}
|
||||
|
||||
|
||||
func testForLinks() {
|
||||
|
||||
var challenge = TokenTest(input: "[Link at start](http://voyagetravelapps.com/)", output: "Link at start", tokens: [
|
||||
Token(type: .string, inputString: "Link at start", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
if let existentOpen = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) }).first {
|
||||
XCTAssertEqual(existentOpen.metadataString, "http://voyagetravelapps.com/")
|
||||
} else {
|
||||
XCTFail("Failed to find an open link tag")
|
||||
}
|
||||
|
||||
|
||||
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/)", output: "A Link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
|
||||
challenge = TokenTest(input: "[Link 1](http://voyagetravelapps.com/), [Link 2](https://www.neverendingvoyage.com/)", output: "Link 1, Link 2", tokens: [
|
||||
Token(type: .string, inputString: "Link 1", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: ", ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Link 2", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
|
||||
XCTAssertEqual(links.count, 2)
|
||||
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
|
||||
XCTAssertEqual(links[1].metadataString, "https://www.neverendingvoyage.com/")
|
||||
|
||||
challenge = TokenTest(input: "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)", output: "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp", tokens: [
|
||||
Token(type: .string, inputString: "Email us at ", characterStyles: []),
|
||||
Token(type: .string, inputString: "simon@voyagetravelapps.com", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " Twitter", characterStyles: []),
|
||||
Token(type: .string, inputString: "@VoyageTravelApp", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
|
||||
XCTAssertEqual(links.count, 2)
|
||||
XCTAssertEqual(links[0].metadataString, "mailto:simon@voyagetravelapps.com")
|
||||
XCTAssertEqual(links[1].metadataString, "twitter://user?screen_name=VoyageTravelApp")
|
||||
|
||||
challenge = TokenTest(input: "[Link with missing square(http://voyagetravelapps.com/)", output: "[Link with missing square(http://voyagetravelapps.com/)", tokens: [
|
||||
Token(type: .string, inputString: "Link with missing square(http://voyagetravelapps.com/)", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A [Link(http://voyagetravelapps.com/)", output: "A [Link(http://voyagetravelapps.com/)", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "[Link(http://voyagetravelapps.com/)", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
|
||||
challenge = TokenTest(input: "[Link with missing parenthesis](http://voyagetravelapps.com/", output: "[Link with missing parenthesis](http://voyagetravelapps.com/", tokens: [
|
||||
Token(type: .string, inputString: "[Link with missing parenthesis](", characterStyles: []),
|
||||
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "A [Link](http://voyagetravelapps.com/", output: "A [Link](http://voyagetravelapps.com/", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "[Link](", characterStyles: []),
|
||||
Token(type: .string, inputString: "http://voyagetravelapps.com/", characterStyles: [])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
challenge = TokenTest(input: "[Link1](http://voyagetravelapps.com/) **bold** [Link2](http://voyagetravelapps.com/)", output: "Link1 bold Link2", tokens: [
|
||||
Token(type: .string, inputString: "Link1", characterStyles: [CharacterStyle.link]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "bold", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: " ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Link2", characterStyles: [CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
|
||||
}
|
||||
|
||||
func testLinksWithOtherStyles() {
|
||||
var challenge = TokenTest(input: "A **Bold [Link](http://voyagetravelapps.com/)**", output: "A Bold Link", tokens: [
|
||||
Token(type: .string, inputString: "A ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Bold ", characterStyles: [CharacterStyle.bold]),
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.link, CharacterStyle.bold])
|
||||
])
|
||||
var results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
// XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
var links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
|
||||
XCTAssertEqual(links.count, 1)
|
||||
if links.count == 1 {
|
||||
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
|
||||
} else {
|
||||
XCTFail("Incorrect link count. Expecting 1, found \(links.count)")
|
||||
}
|
||||
|
||||
|
||||
challenge = TokenTest(input: "A Bold [**Link**](http://voyagetravelapps.com/)", output: "A Bold Link", tokens: [
|
||||
Token(type: .string, inputString: "A Bold ", characterStyles: []),
|
||||
Token(type: .string, inputString: "Link", characterStyles: [CharacterStyle.bold, CharacterStyle.link])
|
||||
])
|
||||
results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
XCTAssertEqual(results.attributedString.string, challenge.output)
|
||||
links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.link) ?? false) })
|
||||
XCTAssertEqual(links.count, 1)
|
||||
XCTAssertEqual(links[0].metadataString, "http://voyagetravelapps.com/")
|
||||
}
|
||||
|
||||
func testForImages() {
|
||||
let challenge = TokenTest(input: "An ", output: "An Image", tokens: [
|
||||
Token(type: .string, inputString: "An Image", characterStyles: []),
|
||||
Token(type: .string, inputString: "", characterStyles: [CharacterStyle.image])
|
||||
])
|
||||
let results = self.attempt(challenge)
|
||||
XCTAssertEqual(challenge.tokens.count, results.stringTokens.count)
|
||||
XCTAssertEqual(results.tokens.map({ $0.outputString }).joined(), challenge.output)
|
||||
XCTAssertEqual(results.foundStyles, results.expectedStyles)
|
||||
let links = results.tokens.filter({ $0.type == .string && (($0.characterStyles as? [CharacterStyle])?.contains(.image) ?? false) })
|
||||
XCTAssertEqual(links.count, 1)
|
||||
XCTAssertEqual(links[0].metadataString, "imageName")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// SwiftyMarkdownTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
struct StringTest {
|
||||
let input : String
|
||||
let expectedOutput : String
|
||||
var acutalOutput : String = ""
|
||||
}
|
||||
|
||||
struct TokenTest {
|
||||
let input : String
|
||||
let output : String
|
||||
let tokens : [Token]
|
||||
}
|
||||
|
||||
class SwiftyMarkdownTests: XCTestCase {
|
||||
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testThatOctothorpeHeadersAreHandledCorrectly() {
|
||||
|
||||
let heading1 = StringTest(input: "# Heading 1", expectedOutput: "Heading 1")
|
||||
var smd = SwiftyMarkdown(string:heading1.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading1.expectedOutput)
|
||||
|
||||
let heading2 = StringTest(input: "## Heading 2", expectedOutput: "Heading 2")
|
||||
smd = SwiftyMarkdown(string:heading2.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading2.expectedOutput)
|
||||
|
||||
let heading3 = StringTest(input: "### #Heading #3", expectedOutput: "#Heading #3")
|
||||
smd = SwiftyMarkdown(string:heading3.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading3.expectedOutput)
|
||||
|
||||
let heading4 = StringTest(input: " #### #Heading 4 ####", expectedOutput: "#Heading 4")
|
||||
smd = SwiftyMarkdown(string:heading4.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading4.expectedOutput)
|
||||
|
||||
let heading5 = StringTest(input: " ##### Heading 5 #### ", expectedOutput: "Heading 5 ####")
|
||||
smd = SwiftyMarkdown(string:heading5.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading5.expectedOutput)
|
||||
|
||||
let heading6 = StringTest(input: " ##### Heading 5 #### More ", expectedOutput: "Heading 5 #### More")
|
||||
smd = SwiftyMarkdown(string:heading6.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading6.expectedOutput)
|
||||
|
||||
let heading7 = StringTest(input: "# **Bold Header 1** ", expectedOutput: "Bold Header 1")
|
||||
smd = SwiftyMarkdown(string:heading7.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading7.expectedOutput)
|
||||
|
||||
let heading8 = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
|
||||
smd = SwiftyMarkdown(string:heading8.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading8.expectedOutput)
|
||||
|
||||
let heading9 = StringTest(input: " # Heading 1", expectedOutput: "# Heading 1")
|
||||
smd = SwiftyMarkdown(string:heading9.input )
|
||||
XCTAssertEqual(smd.attributedString().string, heading9.expectedOutput)
|
||||
|
||||
let allHeaders = [heading1, heading2, heading3, heading4, heading5, heading6, heading7, heading8, heading9]
|
||||
smd = SwiftyMarkdown(string: allHeaders.map({ $0.input }).joined(separator: "\n"))
|
||||
XCTAssertEqual(smd.attributedString().string, allHeaders.map({ $0.expectedOutput}).joined(separator: "\n"))
|
||||
|
||||
let headerString = StringTest(input: "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6", expectedOutput: "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6")
|
||||
smd = SwiftyMarkdown(string: headerString.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerString.expectedOutput)
|
||||
|
||||
let headerStringWithBold = StringTest(input: "# **Bold Header 1**", expectedOutput: "Bold Header 1")
|
||||
smd = SwiftyMarkdown(string: headerStringWithBold.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerStringWithBold.expectedOutput )
|
||||
|
||||
let headerStringWithItalic = StringTest(input: "## Header 2 _With Italics_", expectedOutput: "Header 2 With Italics")
|
||||
smd = SwiftyMarkdown(string: headerStringWithItalic.input)
|
||||
XCTAssertEqual(smd.attributedString().string, headerStringWithItalic.expectedOutput)
|
||||
|
||||
}
|
||||
|
||||
|
||||
func testThatUndelinedHeadersAreHandledCorrectly() {
|
||||
|
||||
let h1String = StringTest(input: "Header 1\n===\nSome following text", expectedOutput: "Header 1\nSome following text")
|
||||
var md = SwiftyMarkdown(string: h1String.input)
|
||||
XCTAssertEqual(md.attributedString().string, h1String.expectedOutput)
|
||||
|
||||
let h2String = StringTest(input: "Header 2\n---\nSome following text", expectedOutput: "Header 2\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2String.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2String.expectedOutput)
|
||||
|
||||
let h1StringWithBold = StringTest(input: "Header 1 **With Bold**\n===\nSome following text", expectedOutput: "Header 1 With Bold\nSome following text")
|
||||
md = SwiftyMarkdown(string: h1StringWithBold.input)
|
||||
XCTAssertEqual(md.attributedString().string, h1StringWithBold.expectedOutput)
|
||||
|
||||
let h2StringWithItalic = StringTest(input: "Header 2 _With Italic_\n---\nSome following text", expectedOutput: "Header 2 With Italic\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2StringWithItalic.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2StringWithItalic.expectedOutput)
|
||||
|
||||
let h2StringWithCode = StringTest(input: "Header 2 `With Code`\n---\nSome following text", expectedOutput: "Header 2 With Code\nSome following text")
|
||||
md = SwiftyMarkdown(string: h2StringWithCode.input)
|
||||
XCTAssertEqual(md.attributedString().string, h2StringWithCode.expectedOutput)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
The reason for this test is because the list of items dropped every other item in bullet lists marked with "-"
|
||||
The faulty result was: "A cool title\n \n- Här har vi svenska ÅÄÖåäö tecken\n \nA Link"
|
||||
As you can see, "- Point number one" and "- Point number two" are mysteriously missing.
|
||||
It incorrectly identified rows as `Alt-H2`
|
||||
*/
|
||||
func offtestInternationalCharactersInList() {
|
||||
|
||||
let expected = "A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n \nA Link"
|
||||
let input = "# A cool title\n\n- Point number one\n- Här har vi svenska ÅÄÖåäö tecken\n- Point number two\n\n[A Link](http://dooer.com)"
|
||||
let output = SwiftyMarkdown(string: input).attributedString().string
|
||||
|
||||
XCTAssertEqual(output, expected)
|
||||
|
||||
}
|
||||
|
||||
func testReportedCrashingStrings() {
|
||||
let text = "[**\\!bang**](https://duckduckgo.com/bang) "
|
||||
let expected = "\\!bang"
|
||||
let output = SwiftyMarkdown(string: text).attributedString().string
|
||||
XCTAssertEqual(output, expected)
|
||||
}
|
||||
|
||||
func testThatYAMLMetadataIsRemoved() {
|
||||
// let yaml = StringTest(input: "---\nlayout: page\ntitle: \"Trail Wallet FAQ\"\ndate: 2015-04-22 10:59\ncomments: true\nsharing: true\nliking: false\nfooter: true\nsidebar: false\n---\n# Finally some Markdown!", expectedOutput: "Finally some Markdown!")
|
||||
// let md = SwiftyMarkdown(string: yaml.input)
|
||||
// XCTAssertEqual(md.attributedString().string, yaml.expectedOutput)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// SwiftyMarkdownAttributedStringTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownPerformanceTests: XCTestCase {
|
||||
|
||||
func testThatFilesAreProcessedQuickly() {
|
||||
|
||||
guard let url = Bundle(for: SwiftyMarkdownPerformanceTests.self).url(forResource: "test", withExtension: "md") else {
|
||||
XCTFail("Failed to load test.md in test bundle")
|
||||
return
|
||||
}
|
||||
measure {
|
||||
let md = SwiftyMarkdown(url: url)
|
||||
_ = md?.attributedString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testThatStringsAreProcessedQuickly() {
|
||||
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
measure {
|
||||
_ = md.attributedString(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func testThatVeryLongStringsAreProcessedQuickly() {
|
||||
let string = "SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use. SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a *Swift*-style syntax. It uses **dynamic type** to set the font size correctly with [whatever](https://www.neverendingvoyage.com/) font you'd like to use."
|
||||
let md = SwiftyMarkdown(string: string)
|
||||
measure {
|
||||
_ = md.attributedString(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdownTests.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
class SwiftyMarkdownTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testThatOctothorpeHeadersAreHandledCorrectly() {
|
||||
|
||||
let headerString = "# Header 1\n## Header 2 ##\n### Header 3 ### \n#### Header 4#### \n##### Header 5\n###### Header 6"
|
||||
let headerStringWithBold = "# **Bold Header 1**"
|
||||
let headerStringWithItalic = "## Header 2 _With Italics_"
|
||||
|
||||
var md = SwiftyMarkdown(string: headerString)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 1\nHeader 2\nHeader 3\nHeader 4\nHeader 5\nHeader 6\n")
|
||||
|
||||
md = SwiftyMarkdown(string: headerStringWithBold)
|
||||
XCTAssertEqual(md.attributedString().string, "Bold Header 1\n")
|
||||
|
||||
md = SwiftyMarkdown(string: headerStringWithItalic)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 2 With Italics\n")
|
||||
|
||||
}
|
||||
|
||||
func testThatUndelinedHeadersAreHandledCorrectly() {
|
||||
let h1String = "Header 1\n===\nSome following text"
|
||||
let h2String = "Header 2\n---\nSome following text"
|
||||
|
||||
let h1StringWithBold = "Header 1 **With Bold**\n===\nSome following text"
|
||||
let h2StringWithItalic = "Header 2 _With Italic_\n---\nSome following text"
|
||||
let h2StringWithCode = "Header 2 `With Code`\n---\nSome following text"
|
||||
|
||||
var md = SwiftyMarkdown(string: h1String)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 1\nSome following text\n")
|
||||
|
||||
md = SwiftyMarkdown(string: h2String)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 2\nSome following text\n")
|
||||
|
||||
md = SwiftyMarkdown(string: h1StringWithBold)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 1 With Bold\nSome following text\n")
|
||||
|
||||
md = SwiftyMarkdown(string: h2StringWithItalic)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 2 With Italic\nSome following text\n")
|
||||
|
||||
md = SwiftyMarkdown(string: h2StringWithCode)
|
||||
XCTAssertEqual(md.attributedString().string, "Header 2 With Code\nSome following text\n")
|
||||
}
|
||||
|
||||
func testThatRegularTraitsAreParsedCorrectly() {
|
||||
let boldAtStartOfString = "**A bold string**"
|
||||
let boldWithinString = "A string with a **bold** word"
|
||||
let codeAtStartOfString = "`Code (should be indented)`"
|
||||
let codeWithinString = "A string with `code` (should not be indented)"
|
||||
let italicAtStartOfString = "*An italicised string*"
|
||||
let italicWithinString = "A string with *italicised* text"
|
||||
|
||||
let multipleBoldWords = "__A bold string__ with a **mix** **of** bold __styles__"
|
||||
let multipleCodeWords = "`A code string` with multiple `code` `instances`"
|
||||
let multipleItalicWords = "_An italic string_ with a *mix* _of_ italic *styles*"
|
||||
|
||||
let longMixedString = "_An italic string_, **follwed by a bold one**, `with some code`, \\*\\*and some\\*\\* \\_escaped\\_ \\`characters\\`, `ending` *with* __more__ variety."
|
||||
|
||||
|
||||
var md = SwiftyMarkdown(string: boldAtStartOfString)
|
||||
XCTAssertEqual(md.attributedString().string, "A bold string\n")
|
||||
|
||||
md = SwiftyMarkdown(string: boldWithinString)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with a bold word\n")
|
||||
|
||||
md = SwiftyMarkdown(string: codeAtStartOfString)
|
||||
XCTAssertEqual(md.attributedString().string, "\tCode (should be indented)\n")
|
||||
|
||||
md = SwiftyMarkdown(string: codeWithinString)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with code (should not be indented)\n")
|
||||
|
||||
md = SwiftyMarkdown(string: italicAtStartOfString)
|
||||
XCTAssertEqual(md.attributedString().string, "An italicised string\n")
|
||||
|
||||
md = SwiftyMarkdown(string: italicWithinString)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with italicised text\n")
|
||||
|
||||
md = SwiftyMarkdown(string: multipleBoldWords)
|
||||
XCTAssertEqual(md.attributedString().string, "A bold string with a mix of bold styles\n")
|
||||
|
||||
md = SwiftyMarkdown(string: multipleCodeWords)
|
||||
XCTAssertEqual(md.attributedString().string, "\tA code string with multiple code instances\n")
|
||||
|
||||
md = SwiftyMarkdown(string: multipleItalicWords)
|
||||
XCTAssertEqual(md.attributedString().string, "An italic string with a mix of italic styles\n")
|
||||
|
||||
md = SwiftyMarkdown(string: longMixedString)
|
||||
XCTAssertEqual(md.attributedString().string, "An italic string, follwed by a bold one, with some code, **and some** _escaped_ `characters`, ending with more variety.\n")
|
||||
|
||||
}
|
||||
|
||||
func testThatMarkdownMistakesAreHandledAppropriately() {
|
||||
let mismatchedBoldCharactersAtStart = "**This should be bold*"
|
||||
let mismatchedBoldCharactersWithin = "A string *that should be italic**"
|
||||
|
||||
var md = SwiftyMarkdown(string: mismatchedBoldCharactersAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "This should be bold\n")
|
||||
|
||||
md = SwiftyMarkdown(string: mismatchedBoldCharactersWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string that should be italic\n")
|
||||
|
||||
}
|
||||
|
||||
func testThatEscapedCharactersAreEscapedCorrectly() {
|
||||
let escapedBoldAtStart = "\\*\\*A normal string\\*\\*"
|
||||
let escapedBoldWithin = "A string with \\*\\*escaped\\*\\* asterisks"
|
||||
|
||||
let escapedItalicAtStart = "\\_A normal string\\_"
|
||||
let escapedItalicWithin = "A string with \\_escaped\\_ underscores"
|
||||
|
||||
let escapedBackticksAtStart = "\\`A normal string\\`"
|
||||
let escapedBacktickWithin = "A string with \\`escaped\\` backticks"
|
||||
|
||||
let oneEscapedAsteriskOneNormalAtStart = "\\**A normal string\\**"
|
||||
let oneEscapedAsteriskOneNormalWithin = "A string with \\**escaped\\** asterisks"
|
||||
|
||||
let oneEscapedAsteriskTwoNormalAtStart = "\\***A normal string*\\**"
|
||||
let oneEscapedAsteriskTwoNormalWithin = "A string with *\\**escaped**\\* asterisks"
|
||||
|
||||
var md = SwiftyMarkdown(string: escapedBoldAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "**A normal string**\n")
|
||||
|
||||
md = SwiftyMarkdown(string: escapedBoldWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with **escaped** asterisks\n")
|
||||
|
||||
md = SwiftyMarkdown(string: escapedItalicAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "_A normal string_\n")
|
||||
|
||||
md = SwiftyMarkdown(string: escapedItalicWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with _escaped_ underscores\n")
|
||||
|
||||
md = SwiftyMarkdown(string: escapedBackticksAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "`A normal string`\n")
|
||||
|
||||
md = SwiftyMarkdown(string: escapedBacktickWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with `escaped` backticks\n")
|
||||
|
||||
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "*A normal string*\n")
|
||||
|
||||
md = SwiftyMarkdown(string: oneEscapedAsteriskOneNormalWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks\n")
|
||||
|
||||
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "*A normal string*\n")
|
||||
|
||||
md = SwiftyMarkdown(string: oneEscapedAsteriskTwoNormalWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A string with *escaped* asterisks\n")
|
||||
|
||||
}
|
||||
|
||||
func testThatAsterisksAndUnderscoresNotAttachedToWordsAreNotRemoved() {
|
||||
let asteriskSpace = "An asterisk followed by a space: * "
|
||||
let backtickSpace = "A backtick followed by a space: ` "
|
||||
let underscoreSpace = "An underscore followed by a space: _ "
|
||||
|
||||
let asteriskFullStop = "Two asterisks followed by a full stop: **."
|
||||
let backtickFullStop = "Two backticks followed by a full stop: ``."
|
||||
let underscoreFullStop = "Two underscores followed by a full stop: __."
|
||||
|
||||
let asteriskComma = "An asterisk followed by a full stop: *, *"
|
||||
let backtickComma = "A backtick followed by a space: `, `"
|
||||
let underscoreComma = "An underscore followed by a space: _, _"
|
||||
|
||||
let asteriskWithBold = "A **bold** word followed by an asterisk * "
|
||||
let backtickWithCode = "A `code` word followed by a backtick ` "
|
||||
let underscoreWithItalic = "An _italic_ word followed by an underscore _ "
|
||||
|
||||
var md = SwiftyMarkdown(string: asteriskSpace)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskSpace + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickSpace)
|
||||
XCTAssertEqual(md.attributedString().string, backtickSpace + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreSpace)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreSpace + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskFullStop + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, backtickFullStop + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreFullStop)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreFullStop + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskComma)
|
||||
XCTAssertEqual(md.attributedString().string, asteriskComma + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickComma)
|
||||
XCTAssertEqual(md.attributedString().string, backtickComma + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreComma)
|
||||
XCTAssertEqual(md.attributedString().string, underscoreComma + "\n")
|
||||
|
||||
md = SwiftyMarkdown(string: asteriskWithBold)
|
||||
XCTAssertEqual(md.attributedString().string, "A bold word followed by an asterisk * \n")
|
||||
|
||||
md = SwiftyMarkdown(string: backtickWithCode)
|
||||
XCTAssertEqual(md.attributedString().string, "A code word followed by a backtick ` \n")
|
||||
|
||||
md = SwiftyMarkdown(string: underscoreWithItalic)
|
||||
XCTAssertEqual(md.attributedString().string, "An italic word followed by an underscore _ \n")
|
||||
|
||||
}
|
||||
|
||||
|
||||
func testForLinks() {
|
||||
|
||||
let linkAtStart = "[Link at start](http://voyagetravelapps.com/)"
|
||||
let linkWithin = "A [Link](http://voyagetravelapps.com/)"
|
||||
let headerLink = "## [Header link](http://voyagetravelapps.com/)"
|
||||
|
||||
let multipleLinks = "[Link 1](http://voyagetravelapps.com/), [Link 2](http://voyagetravelapps.com/)"
|
||||
|
||||
let mailtoAndTwitterLinks = "Email us at [simon@voyagetravelapps.com](mailto:simon@voyagetravelapps.com) Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)"
|
||||
|
||||
let syntaxErrorSquareBracketAtStart = "[Link with missing square(http://voyagetravelapps.com/)"
|
||||
let syntaxErrorSquareBracketWithin = "A [Link(http://voyagetravelapps.com/)"
|
||||
|
||||
let syntaxErrorParenthesisAtStart = "[Link with missing parenthesis](http://voyagetravelapps.com/"
|
||||
let syntaxErrorParenthesisWithin = "A [Link](http://voyagetravelapps.com/"
|
||||
|
||||
var md = SwiftyMarkdown(string: linkAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "Link at start\n")
|
||||
|
||||
md = SwiftyMarkdown(string: linkWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A Link\n")
|
||||
|
||||
md = SwiftyMarkdown(string: headerLink)
|
||||
XCTAssertEqual(md.attributedString().string, "Header link\n")
|
||||
|
||||
md = SwiftyMarkdown(string: multipleLinks)
|
||||
XCTAssertEqual(md.attributedString().string, "Link 1, Link 2\n")
|
||||
|
||||
md = SwiftyMarkdown(string: syntaxErrorSquareBracketAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "Link with missing square\n")
|
||||
|
||||
md = SwiftyMarkdown(string: syntaxErrorSquareBracketWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A Link\n")
|
||||
|
||||
md = SwiftyMarkdown(string: syntaxErrorParenthesisAtStart)
|
||||
XCTAssertEqual(md.attributedString().string, "Link with missing parenthesis\n")
|
||||
|
||||
md = SwiftyMarkdown(string: syntaxErrorParenthesisWithin)
|
||||
XCTAssertEqual(md.attributedString().string, "A Link\n")
|
||||
|
||||
md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
|
||||
XCTAssertEqual(md.attributedString().string, "Email us at simon@voyagetravelapps.com Twitter @VoyageTravelApp\n")
|
||||
|
||||
|
||||
|
||||
// let mailtoAndTwitterLinks = "Twitter [@VoyageTravelApp](twitter://user?screen_name=VoyageTravelApp)"
|
||||
// let md = SwiftyMarkdown(string: mailtoAndTwitterLinks)
|
||||
// XCTAssertEqual(md.attributedString().string, "Twitter @VoyageTravelApp\n")
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// XCTest+SwiftyMarkdown.swift
|
||||
// SwiftyMarkdownTests
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftyMarkdown
|
||||
|
||||
extension XCTestCase {
|
||||
func attempt( _ challenge : TokenTest ) -> (tokens : [Token], stringTokens: [Token], attributedString : NSAttributedString, foundStyles : [[CharacterStyle]], expectedStyles : [[CharacterStyle]] ) {
|
||||
let md = SwiftyMarkdown(string: challenge.input)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
let tokens = tokeniser.process(challenge.input)
|
||||
let stringTokens = tokens.filter({ $0.type == .string && !$0.isMetadata })
|
||||
|
||||
let existentTokenStyles = stringTokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
|
||||
let expectedStyles = challenge.tokens.compactMap({ $0.characterStyles as? [CharacterStyle] })
|
||||
|
||||
return (tokens, stringTokens, md.attributedString(), existentTokenStyles, expectedStyles)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
layout: page
|
||||
title: "Trail Wallet FAQ"
|
||||
date: 2015-04-22 10:59
|
||||
comments: true
|
||||
sharing: true
|
||||
liking: false
|
||||
footer: true
|
||||
sidebar: false
|
||||
---
|
||||
|
||||
# Good Day To You, Walleteer!
|
||||
|
||||
We are Erin and Simon from [Never Ending Voyage][1] and we want to thank you for trying out our app. We have been travelling non-stop for seven years and part of how we support ourselves is through Trail Wallet.
|
||||
@@ -0,0 +1,21 @@
|
||||
# SwiftyMarkdown 1.0
|
||||
|
||||
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
|
||||
|
||||
## Fully Rebuilt For 2020!
|
||||
|
||||
SwiftyMarkdown now features a more robust and reliable rules-based line processing and tokenisation engine. It has added support for images stored in the bundle (``), codeblocks, blockquotes, and unordered lists!
|
||||
|
||||
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be underlined by setting underlineLinks to `true`.
|
||||
|
||||
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods:
|
||||
|
||||
`pod 'SwiftyMarkdown'`
|
||||
|
||||
### SPM:
|
||||
|
||||
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
|
||||
+4
-69
@@ -16,73 +16,8 @@
|
||||
|
||||
# This is the minimum version number required.
|
||||
# Update this, if you use features of a newer version
|
||||
fastlane_version "1.36.1"
|
||||
$scheme = "SwiftyMarkdown"
|
||||
$spec = "SwiftyMarkdown.podspec"
|
||||
$project = './SwiftyMarkdown.xcodeproj'
|
||||
|
||||
default_platform :ios
|
||||
|
||||
|
||||
|
||||
platform :ios do
|
||||
|
||||
spec = "SwiftyMarkdown.podspec"
|
||||
|
||||
before_all do
|
||||
ensure_git_branch
|
||||
ensure_git_status_clean
|
||||
increment_build_number
|
||||
commit_version_bump(xcodeproj: './SwiftyMarkdown.xcodeproj')
|
||||
end
|
||||
|
||||
desc "Bumps the patch version, increments it, and pushes it to both the remote and spec repos"
|
||||
lane :patch do
|
||||
# snapshot
|
||||
update(type: "patch")
|
||||
end
|
||||
|
||||
desc "Bumps the minor version, increments it, and pushes it to both the remote and spec repos"
|
||||
lane :minor do
|
||||
# snapshot
|
||||
update(type: "minor")
|
||||
|
||||
end
|
||||
|
||||
lane :initial do
|
||||
version = version_get_podspec
|
||||
bump(version: version)
|
||||
end
|
||||
|
||||
private_lane :update do |options|
|
||||
|
||||
type = options[:type]
|
||||
version = version_bump_podspec(path: spec, bump_type: type)
|
||||
increment_version_number( bump_type: type, version_number: version )
|
||||
git_commit(path: spec, message: "Updating podspec")
|
||||
|
||||
bump(version: version)
|
||||
|
||||
end
|
||||
|
||||
private_lane :bump do |options|
|
||||
version = options[:version]
|
||||
add_git_tag( tag: version)
|
||||
push_to_git_remote
|
||||
pod_push(path: spec)
|
||||
|
||||
end
|
||||
|
||||
# You can define as many lanes as you want
|
||||
|
||||
after_all do |lane|
|
||||
# This block is called, only if the executed lane was successful
|
||||
notification(subtitle: "SwiftyMarkdown fastlane finished", message: "Completed '#{lane}' successfully")
|
||||
end
|
||||
|
||||
error do |lane, exception|
|
||||
notification(subtitle: "Error: SwiftyMarkdown fastlane", message: "'#{exception.message}'")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# More information about multiple platforms in fastlane: https://github.com/KrauseFx/fastlane/blob/master/docs/Platforms.md
|
||||
# All available actions: https://github.com/KrauseFx/fastlane/blob/master/docs/Actions.md
|
||||
import "../../Fastlane/FastfilePods"
|
||||
+52
-8
@@ -1,29 +1,73 @@
|
||||
fastlane documentation
|
||||
================
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```
|
||||
sudo gem install fastlane
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
Install _fastlane_ using
|
||||
```
|
||||
[sudo] gem install fastlane -NV
|
||||
```
|
||||
or alternatively using `brew cask install fastlane`
|
||||
|
||||
# Available Actions
|
||||
## iOS
|
||||
### ios patch
|
||||
```
|
||||
fastlane ios patch
|
||||
```
|
||||
Bumps the patch version, increments it, and pushes it to both the remote and spec repos
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the patch version
|
||||
### ios minor
|
||||
```
|
||||
fastlane ios minor
|
||||
```
|
||||
Bumps the minor version, increments it, and pushes it to both the remote and spec repos
|
||||
### ios initial
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the minor version
|
||||
### ios major
|
||||
```
|
||||
fastlane ios initial
|
||||
fastlane ios major
|
||||
```
|
||||
This does the following:
|
||||
|
||||
|
||||
|
||||
- Runs the unit tests
|
||||
|
||||
- Ensures Cocoapods compatibility
|
||||
|
||||
- Bumps the major version
|
||||
### ios test
|
||||
```
|
||||
fastlane ios test
|
||||
```
|
||||
|
||||
### ios submit_pod
|
||||
```
|
||||
fastlane ios submit_pod
|
||||
```
|
||||
Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to run tests, bump versions, and commit changes.
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time to run [fastlane](https://fastlane.tools).
|
||||
More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools).
|
||||
The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane).
|
||||
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
|
||||
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
|
||||
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
|
||||
Reference in New Issue
Block a user