feat: acknowledgments tab was slow to display (closes #3957)
This commit is contained in:
1 parent
5601a556c9
commit
41ddd7c1a6
28 files changed
+697
-3332
No files matched your search
@@ -5,7 +5,6 @@ target 'alt-tab-macos' do
|
||||
pod 'LetsMove', :git => 'https://github.com/lwouis/LetsMove.git', :commit => '7abf4daed1a25218f2b52f2dfd190aee5a50071c'
|
||||
pod 'Sparkle', :podspec => 'https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec'
|
||||
pod 'ShortcutRecorder', :git => 'https://github.com/lwouis/ShortcutRecorder.git', :branch => 'alt-tab-current'
|
||||
pod 'SwiftyMarkdown', '1.1.0'
|
||||
pod 'AppCenter/Crashes', '4.3.0'
|
||||
pod 'SwiftyBeaver', '1.9.0'
|
||||
end
|
||||
|
||||
+1
-5
@@ -6,7 +6,6 @@ PODS:
|
||||
- ShortcutRecorder (3.1)
|
||||
- Sparkle (1.24.0)
|
||||
- SwiftyBeaver (1.9.0)
|
||||
- SwiftyMarkdown (1.1.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AppCenter/Crashes (= 4.3.0)
|
||||
@@ -14,13 +13,11 @@ DEPENDENCIES:
|
||||
- ShortcutRecorder (from `https://github.com/lwouis/ShortcutRecorder.git`, branch `alt-tab-current`)
|
||||
- Sparkle (from `https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec`)
|
||||
- SwiftyBeaver (= 1.9.0)
|
||||
- SwiftyMarkdown (= 1.1.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppCenter
|
||||
- SwiftyBeaver
|
||||
- SwiftyMarkdown
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
LetsMove:
|
||||
@@ -46,8 +43,7 @@ SPEC CHECKSUMS:
|
||||
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
|
||||
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
|
||||
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
|
||||
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
|
||||
|
||||
PODFILE CHECKSUM: 29e9c8f00c3b594083545d6218fdfecdcd6e0037
|
||||
PODFILE CHECKSUM: aa75d205f0ad11974024e111d7fff313b55e6f93
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
Generated
+1
-5
@@ -6,7 +6,6 @@ PODS:
|
||||
- ShortcutRecorder (3.1)
|
||||
- Sparkle (1.24.0)
|
||||
- SwiftyBeaver (1.9.0)
|
||||
- SwiftyMarkdown (1.1.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AppCenter/Crashes (= 4.3.0)
|
||||
@@ -14,13 +13,11 @@ DEPENDENCIES:
|
||||
- ShortcutRecorder (from `https://github.com/lwouis/ShortcutRecorder.git`, branch `alt-tab-current`)
|
||||
- Sparkle (from `https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec`)
|
||||
- SwiftyBeaver (= 1.9.0)
|
||||
- SwiftyMarkdown (= 1.1.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppCenter
|
||||
- SwiftyBeaver
|
||||
- SwiftyMarkdown
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
LetsMove:
|
||||
@@ -46,8 +43,7 @@ SPEC CHECKSUMS:
|
||||
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
|
||||
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
|
||||
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
|
||||
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
|
||||
|
||||
PODFILE CHECKSUM: 29e9c8f00c3b594083545d6218fdfecdcd6e0037
|
||||
PODFILE CHECKSUM: aa75d205f0ad11974024e111d7fff313b55e6f93
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
Generated
+597
-807
File diff suppressed because it is too large.
Load diff
Generated
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Generated
-360
@@ -1,360 +0,0 @@
|
||||
# SwiftyMarkdown 1.0
|
||||
|
||||
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
|
||||
|
||||
- [What's New](#fully-rebuilt-for-2020)
|
||||
- [Installation](#installation)
|
||||
- [How to Use](#how-to-use-swiftymarkdown)
|
||||
- [Screenshot](#screenshot)
|
||||
- [Front Matter](#front-matter)
|
||||
- [Appendix](#appendix)
|
||||
|
||||
## Fully Rebuilt For 2020!
|
||||
|
||||
SwiftyMarkdown now features a more robust and reliable rules-based line processing and character tokenisation engine. It has added support for images stored in the bundle (``), 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()
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")
|
||||
```
|
||||
|
||||
The attributed string can then be assigned to any label or text control that has support for attributed text.
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
let label = UILabel()
|
||||
label.attributedText = md.attributedString()
|
||||
```
|
||||
|
||||
## Supported Markdown Features
|
||||
|
||||
*italics* or _italics_
|
||||
**bold** or __bold__
|
||||
~~Linethrough~~Strikethroughs.
|
||||
`code`
|
||||
|
||||
# Header 1
|
||||
|
||||
or
|
||||
|
||||
Header 1
|
||||
====
|
||||
|
||||
## Header 2
|
||||
|
||||
or
|
||||
|
||||
Header 2
|
||||
---
|
||||
|
||||
### Header 3
|
||||
#### Header 4
|
||||
##### Header 5 #####
|
||||
###### Header 6 ######
|
||||
|
||||
Indented code blocks (spaces or tabs)
|
||||
|
||||
[Links](http://voyagetravelapps.com/)
|
||||

|
||||
|
||||
> Blockquotes
|
||||
|
||||
- Bulleted
|
||||
- Lists
|
||||
- Including indented lists
|
||||
- Up to three levels
|
||||
- Neat!
|
||||
|
||||
1. Ordered
|
||||
1. Lists
|
||||
1. Including indented lists
|
||||
- Up to three levels
|
||||
|
||||
|
||||
|
||||
Compound rules also work, for example:
|
||||
|
||||
It recognises **[Bold Links](http://voyagetravelapps.com/)**
|
||||
|
||||
Or [**Bold Links**](http://voyagetravelapps.com/)
|
||||
|
||||
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
|
||||
|
||||
## Customisation
|
||||
|
||||
Set the attributes of every paragraph and character style type using straightforward dot syntax:
|
||||
|
||||
```swift
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h1.fontSize = 16
|
||||
md.h1.alignmnent = .center
|
||||
|
||||
md.italic.color = UIColor.blueColor()
|
||||
|
||||
md.underlineLinks = true
|
||||
|
||||
md.bullet = "🍏"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Front Matter
|
||||
|
||||
SwiftyMarkdown recognises YAML front matter and will populate the `frontMatterAttributes` property with the key-value pairs that it fines.
|
||||
|
||||
## Appendix
|
||||
|
||||
### A) All Customisable Properties
|
||||
|
||||
```swift
|
||||
h1.fontName : String
|
||||
h1.fontSize : CGFloat
|
||||
h1.color : UI/NSColor
|
||||
h1.fontStyle : FontStyle
|
||||
h1.alignment : NSTextAlignment
|
||||
|
||||
h2.fontName : String
|
||||
h2.fontSize : CGFloat
|
||||
h2.color : UI/NSColor
|
||||
h2.fontStyle : FontStyle
|
||||
h2.alignment : NSTextAlignment
|
||||
|
||||
h3.fontName : String
|
||||
h3.fontSize : CGFloat
|
||||
h3.color : UI/NSColor
|
||||
h3.fontStyle : FontStyle
|
||||
h3.alignment : NSTextAlignment
|
||||
|
||||
h4.fontName : String
|
||||
h4.fontSize : CGFloat
|
||||
h4.color : UI/NSColor
|
||||
h4.fontStyle : FontStyle
|
||||
h4.alignment : NSTextAlignment
|
||||
|
||||
h5.fontName : String
|
||||
h5.fontSize : CGFloat
|
||||
h5.color : UI/NSColor
|
||||
h5.fontStyle : FontStyle
|
||||
h5.alignment : NSTextAlignment
|
||||
|
||||
h6.fontName : String
|
||||
h6.fontSize : CGFloat
|
||||
h6.color : UI/NSColor
|
||||
h6.fontStyle : FontStyle
|
||||
h6.alignment : NSTextAlignment
|
||||
|
||||
body.fontName : String
|
||||
body.fontSize : CGFloat
|
||||
body.color : UI/NSColor
|
||||
body.fontStyle : FontStyle
|
||||
body.alignment : NSTextAlignment
|
||||
|
||||
blockquotes.fontName : String
|
||||
blockquotes.fontSize : CGFloat
|
||||
blockquotes.color : UI/NSColor
|
||||
blockquotes.fontStyle : FontStyle
|
||||
blockquotes.alignment : NSTextAlignment
|
||||
|
||||
link.fontName : String
|
||||
link.fontSize : CGFloat
|
||||
link.color : UI/NSColor
|
||||
link.fontStyle : FontStyle
|
||||
|
||||
bold.fontName : String
|
||||
bold.fontSize : CGFloat
|
||||
bold.color : UI/NSColor
|
||||
bold.fontStyle : FontStyle
|
||||
|
||||
italic.fontName : String
|
||||
italic.fontSize : CGFloat
|
||||
italic.color : UI/NSColor
|
||||
italic.fontStyle : FontStyle
|
||||
|
||||
code.fontName : String
|
||||
code.fontSize : CGFloat
|
||||
code.color : UI/NSColor
|
||||
code.fontStyle : FontStyle
|
||||
|
||||
strikethrough.fontName : String
|
||||
strikethrough.fontSize : CGFloat
|
||||
strikethrough.color : UI/NSColor
|
||||
strikethrough.fontStyle : FontStyle
|
||||
|
||||
underlineLinks : Bool
|
||||
|
||||
bullet : String
|
||||
```
|
||||
|
||||
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look.
|
||||
|
||||
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,44 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
case untilClose
|
||||
}
|
||||
|
||||
public struct FrontMatterRule {
|
||||
let openTag : String
|
||||
let closeTag : String
|
||||
let keyValueSeparator : Character
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
public var processEmptyStrings : LineStyling?
|
||||
public internal(set) var frontMatterAttributes : [String : String] = [:]
|
||||
|
||||
var closeToken : String? = nil
|
||||
let defaultType : LineStyling
|
||||
|
||||
let lineRules : [LineRule]
|
||||
let frontMatterRules : [FrontMatterRule]
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling, frontMatterRules : [FrontMatterRule] = []) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
self.frontMatterRules = frontMatterRules
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<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)
|
||||
}
|
||||
|
||||
func processFrontMatter( _ strings : [String] ) -> [String] {
|
||||
guard let firstString = strings.first?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
return strings
|
||||
}
|
||||
var rulesToApply : FrontMatterRule? = nil
|
||||
for matter in self.frontMatterRules {
|
||||
if firstString == matter.openTag {
|
||||
rulesToApply = matter
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let existentRules = rulesToApply else {
|
||||
return strings
|
||||
}
|
||||
var outputString = strings
|
||||
// Remove the first line, which is the front matter opening tag
|
||||
let _ = outputString.removeFirst()
|
||||
var closeFound = false
|
||||
while !closeFound {
|
||||
let nextString = outputString.removeFirst()
|
||||
if nextString == existentRules.closeTag {
|
||||
closeFound = true
|
||||
continue
|
||||
}
|
||||
var keyValue = nextString.components(separatedBy: "\(existentRules.keyValueSeparator)")
|
||||
if keyValue.count < 2 {
|
||||
continue
|
||||
}
|
||||
let key = keyValue.removeFirst()
|
||||
let value = keyValue.joined()
|
||||
self.frontMatterAttributes[key] = value
|
||||
}
|
||||
while outputString.first?.isEmpty ?? false {
|
||||
outputString.removeFirst()
|
||||
}
|
||||
return outputString
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
|
||||
var lines = string.components(separatedBy: CharacterSet.newlines)
|
||||
lines = self.processFrontMatter(lines)
|
||||
|
||||
for heading in lines {
|
||||
|
||||
if processEmptyStrings == nil && heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let input = processLineLevelAttributes(String(heading)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
case .strikethrough:
|
||||
fontName = strikethrough.fontName ?? fontName
|
||||
fontSize = strikethrough.fontSize
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .yaml:
|
||||
return body.color
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont {
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
case .h2:
|
||||
style = self.h2
|
||||
case .h3:
|
||||
style = self.h3
|
||||
case .h4:
|
||||
style = self.h4
|
||||
case .h5:
|
||||
style = self.h5
|
||||
case .h6:
|
||||
style = self.h6
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
default:
|
||||
style = self.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
finalSize = NSFont.systemFontSize
|
||||
}
|
||||
var font : NSFont
|
||||
if let existentFontName = fontName {
|
||||
if let customFont = NSFont(name: existentFontName, size: finalSize) {
|
||||
font = customFont
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
|
||||
if globalItalic {
|
||||
let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic)
|
||||
font = NSFont(descriptor: italicDescriptor, size: 0) ?? font
|
||||
}
|
||||
if globalBold {
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
|
||||
font = NSFont(descriptor: boldDescriptor, size: 0) ?? font
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> NSColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
case .yaml:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,545 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
case strikethrough
|
||||
|
||||
func isEqualTo(_ other: CharacterStyling) -> Bool {
|
||||
guard let other = other as? CharacterStyle else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
case yaml
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
case unorderedListIndentFirstOrder
|
||||
case unorderedListIndentSecondOrder
|
||||
case orderedList
|
||||
case orderedListIndentFirstOrder
|
||||
case orderedListIndentSecondOrder
|
||||
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : NSColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#else
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static public var lineRules = [
|
||||
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading),
|
||||
LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
static public var frontMatterRules = [
|
||||
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
open var strikethrough = BasicStyles()
|
||||
|
||||
public var bullet : String = "・"
|
||||
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
public var frontMatterAttributes : [String : String] {
|
||||
get {
|
||||
return self.lineProcessor.frontMatterAttributes
|
||||
}
|
||||
}
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
var string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
var orderedListCount = 0
|
||||
var orderedListIndentFirstOrderCount = 0
|
||||
var orderedListIndentSecondOrderCount = 0
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
#if os(macOS)
|
||||
self.setFontColorForAllStyles(with: .labelColor)
|
||||
#elseif !os(watchOS)
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
strikethrough.fontSize = size
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
open func setFontColorForAllStyles(with color: NSColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#else
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#endif
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
strikethrough.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
|
||||
if let existentMarkdownString = markdownString {
|
||||
self.string = existentMarkdownString
|
||||
}
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
for (idx, line) in foundAttributes.enumerated() {
|
||||
if idx > 0 {
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else {
|
||||
preconditionFailure("The passed line style is not a valid Markdown Line Style")
|
||||
}
|
||||
|
||||
var listItem = self.bullet
|
||||
switch markdownLineStyle {
|
||||
case .orderedList:
|
||||
self.orderedListCount += 1
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
listItem = "\(self.orderedListCount)."
|
||||
case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder:
|
||||
self.orderedListIndentFirstOrderCount += 1
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
if markdownLineStyle == .orderedListIndentFirstOrder {
|
||||
listItem = "\(self.orderedListIndentFirstOrderCount)."
|
||||
}
|
||||
|
||||
case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder:
|
||||
self.orderedListIndentSecondOrderCount += 1
|
||||
if markdownLineStyle == .orderedListIndentSecondOrder {
|
||||
listItem = "\(self.orderedListIndentSecondOrderCount)."
|
||||
}
|
||||
|
||||
default:
|
||||
self.orderedListCount = 0
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
}
|
||||
|
||||
let lineProperties : LineProperties
|
||||
switch markdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
paragraphStyle.headIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
|
||||
let interval : CGFloat = 30
|
||||
var addition = interval
|
||||
var indent = ""
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder:
|
||||
addition = interval * 2
|
||||
indent = "\t"
|
||||
case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder:
|
||||
addition = interval * 3
|
||||
indent = "\t\t"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
lineProperties = body
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])]
|
||||
paragraphStyle.defaultTabInterval = interval
|
||||
paragraphStyle.headIndent = addition
|
||||
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0)
|
||||
|
||||
case .yaml:
|
||||
lineProperties = body
|
||||
case .previousH1:
|
||||
lineProperties = body
|
||||
case .previousH2:
|
||||
lineProperties = body
|
||||
case .body:
|
||||
lineProperties = body
|
||||
}
|
||||
|
||||
if lineProperties.alignment != .left {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
|
||||
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.link] = nil
|
||||
attributes[.strikethroughStyle] = nil
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
if styles.contains(.strikethrough) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
|
||||
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
attributes[.foregroundColor] = self.strikethrough.color
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
#if !os(macOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#elseif !os(watchOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = NSImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
#endif
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
@@ -1,821 +0,0 @@
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
func isEqualTo( _ other : CharacterStyling ) -> Bool
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule : CustomStringConvertible {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var minTags : Int = 1
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public var description: String {
|
||||
return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) "
|
||||
}
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], minTags : Int = 1, maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.minTags = minTags
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case string
|
||||
case escape
|
||||
case replacement
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public let type : TokenType
|
||||
public let inputString : String
|
||||
public fileprivate(set) var metadataString : String? = nil
|
||||
public fileprivate(set) var characterStyles : [CharacterStyling] = []
|
||||
public fileprivate(set) var count : Int = 0
|
||||
public fileprivate(set) var shouldSkip : Bool = false
|
||||
public fileprivate(set) var tokenIndex : Int = -1
|
||||
public fileprivate(set) var isProcessed : Bool = false
|
||||
public fileprivate(set) var isMetadata : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count <= 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<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]] = [:]
|
||||
|
||||
var enableLog = (ProcessInfo.processInfo.environment["SwiftyTokeniserLogging"] != nil)
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
|
||||
/// This goes through every CharacterRule in order and applies it to the input string, tokenising the string
|
||||
/// if there are any matches.
|
||||
///
|
||||
/// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to
|
||||
/// those outside them.
|
||||
///
|
||||
/// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately.
|
||||
///
|
||||
/// This is to prevent situations like **\[link**\](url) from returing a bold string.
|
||||
///
|
||||
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
|
||||
|
||||
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
|
||||
if enableLog {
|
||||
os_log("------------------------------", log: .tokenising, type: .info)
|
||||
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
|
||||
}
|
||||
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
|
||||
var outerStringTokens : [Token] = []
|
||||
var innerStringTokens : [Token] = []
|
||||
var isOuter = true
|
||||
for idx in 0..<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] {
|
||||
if enableLog {
|
||||
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
|
||||
}
|
||||
|
||||
if enableLog {
|
||||
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]
|
||||
|
||||
if enableLog {
|
||||
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
|
||||
|
||||
if enableLog {
|
||||
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:
|
||||
if enableLog {
|
||||
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]
|
||||
if enableLog {
|
||||
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]
|
||||
if enableLog {
|
||||
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
|
||||
}
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
if enableLog {
|
||||
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 enableLog {
|
||||
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:
|
||||
if enableLog {
|
||||
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 foundChars == rule.openTag && foundChars.count < rule.minTags {
|
||||
openingString.append(foundChars)
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
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
|
||||
}
|
||||
guard inputString.count >= rule.minTags else {
|
||||
return
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
-25
@@ -459,29 +459,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
## SwiftyMarkdown
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Generated by CocoaPods - https://cocoapods.org
|
||||
-31
@@ -495,37 +495,6 @@ SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT</string>
|
||||
<key>Title</key>
|
||||
<string>SwiftyMarkdown</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Generated by CocoaPods - https://cocoapods.org</string>
|
||||
|
||||
-2
@@ -180,14 +180,12 @@ if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework"
|
||||
fi
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
wait
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SwiftyMarkdown" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SwiftyMarkdown" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown"
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,5 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_SwiftyMarkdown : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_SwiftyMarkdown
|
||||
@end
|
||||
@@ -1,12 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyMarkdown
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,6 +0,0 @@
|
||||
framework module SwiftyMarkdown {
|
||||
umbrella header "SwiftyMarkdown-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyMarkdown
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -129,6 +129,7 @@
|
||||
BF0C894325A54CF0B6B53464 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8B8B31D2F421AC258351 /* Localizable.strings */; };
|
||||
BF0C896B5D08B7DA8F2024AC /* KeyRepeatTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8D606B53D315F2D9927B /* KeyRepeatTimer.swift */; };
|
||||
BF0C898511686611E4D7D81E /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B2C057099EDABE46779 /* Button.swift */; };
|
||||
BF0C89B4B17D7843C32A102E /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8353097840D7DB1E0E2E /* Markdown.swift */; };
|
||||
BF0C89C49CFBAAB4646BB8CB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82A8848224E76B880A83 /* InfoPlist.strings */; };
|
||||
BF0C89F0632E0A2EC52D9168 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C87749F1A64E2D3305181 /* InfoPlist.strings */; };
|
||||
BF0C8A3F32B2177AF407DC7E /* DockEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C870C14C20C936BA4AF40 /* DockEvents.swift */; };
|
||||
@@ -360,6 +361,7 @@
|
||||
BF0C832DEED422648209CCD2 /* unit-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "unit-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF0C833D471343CEA3DEFE56 /* icon_512x512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_512x512@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8343C1F6FCA82552DCB7 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
|
||||
BF0C8353097840D7DB1E0E2E /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; };
|
||||
BF0C83D645B6BB523E50670A /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C83E1E5C670A8B03E3612 /* hr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C83F32C3AB0531459F69E /* ku */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ku; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -1362,6 +1364,7 @@
|
||||
D04BA514F1C4B475B3CA6EB6 /* Bash.swift */,
|
||||
BF0C82E4DE5A28800F1DA6E6 /* MissionControl.swift */,
|
||||
BF0C8B841E890CA0D68FFD86 /* Logger.swift */,
|
||||
BF0C8353097840D7DB1E0E2E /* Markdown.swift */,
|
||||
);
|
||||
path = "api-wrappers";
|
||||
sourceTree = "<group>";
|
||||
@@ -2109,7 +2112,6 @@
|
||||
"${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework",
|
||||
"${PODS_ROOT}/Sparkle/Sparkle.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
@@ -2117,7 +2119,6 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShortcutRecorder.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyBeaver.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyMarkdown.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
@@ -2264,6 +2265,7 @@
|
||||
BF0C8846AFEE2FFF93FAB36F /* AppearanceTestable.swift in Sources */,
|
||||
BF0C88449007A09F1D175029 /* KeyboardEventsTestable.swift in Sources */,
|
||||
BF0C85DA2D285400801C514A /* Logger.swift in Sources */,
|
||||
BF0C89B4B17D7843C32A102E /* Markdown.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -8,9 +8,6 @@ These third-party libraries are used:
|
||||
**Sparkle**
|
||||
[Website](https://github.com/sparkle-project/Sparkle) - [License](https://github.com/sparkle-project/Sparkle/blob/master/LICENSE)
|
||||
|
||||
**SwiftyMarkdown**
|
||||
[Website](https://github.com/SimonFairbairn/SwiftyMarkdown) - [MIT License](https://github.com/SimonFairbairn/SwiftyMarkdown/blob/master/LICENSE)
|
||||
|
||||
**LetsMove**
|
||||
[Website](https://github.com/potionfactory/LetsMove) - [Public domain](https://github.com/potionfactory/LetsMove#license)
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import Cocoa
|
||||
|
||||
class Markdown {
|
||||
private static let titleAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.boldSystemFont(ofSize: 15)
|
||||
]
|
||||
private static let listItemParagraphStyle = {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.firstLineHeadIndent = 20
|
||||
return style
|
||||
}()
|
||||
private static let listItemAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 13),
|
||||
.paragraphStyle: listItemParagraphStyle
|
||||
]
|
||||
private static let boldAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.boldSystemFont(ofSize: 13)
|
||||
]
|
||||
private static let linkAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 13),
|
||||
.foregroundColor: NSColor.blue,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
private static let baseAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 13)
|
||||
]
|
||||
private static let linkRegex = try! NSRegularExpression(pattern: "\\[(.*?)\\]\\((.*?)\\)")
|
||||
private static let boldRegex = try! NSRegularExpression(pattern: "\\*\\*(.*?)\\*\\*")
|
||||
|
||||
static func toAttributedString(_ markdown: String) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
for line in markdown.split(separator: "\n", omittingEmptySubsequences: false) {
|
||||
let processedLine = NSMutableAttributedString(string: String(line) + "\n", attributes: baseAttributes)
|
||||
// titles
|
||||
if processedLine.string.starts(with: "# ") {
|
||||
processedLine.replaceCharacters(in: NSRange(location: 0, length: 2), with: "")
|
||||
processedLine.addAttributes(titleAttributes, range: NSRange(location: 0, length: processedLine.length - 1)) // Exclude the newline
|
||||
attributedString.append(processedLine)
|
||||
continue
|
||||
}
|
||||
// bold
|
||||
let boldMatches = boldRegex.matches(in: processedLine.string, range: NSRange(location: 0, length: processedLine.string.utf16.count))
|
||||
for match in boldMatches.reversed() {
|
||||
let boldTextRange = match.range(at: 1)
|
||||
let boldText = processedLine.attributedSubstring(from: boldTextRange).string
|
||||
processedLine.replaceCharacters(in: match.range, with: boldText)
|
||||
processedLine.addAttributes(boldAttributes, range: NSRange(location: match.range.location, length: boldText.utf16.count))
|
||||
}
|
||||
// list items
|
||||
if processedLine.string.trimmingCharacters(in: .whitespaces).starts(with: "*") {
|
||||
processedLine.replaceCharacters(in: NSRange(location: 0, length: 1), with: "•")
|
||||
processedLine.addAttributes(listItemAttributes, range: NSRange(location: 0, length: processedLine.length - 1))
|
||||
}
|
||||
// links
|
||||
let linkMatches = linkRegex.matches(in: processedLine.string, range: NSRange(location: 0, length: processedLine.string.utf16.count))
|
||||
for match in linkMatches.reversed() {
|
||||
let linkTextRange = match.range(at: 1)
|
||||
let linkURLRange = match.range(at: 2)
|
||||
let linkText = processedLine.attributedSubstring(from: linkTextRange).string
|
||||
let linkURL = processedLine.attributedSubstring(from: linkURLRange).string
|
||||
processedLine.replaceCharacters(in: match.range, with: linkText)
|
||||
processedLine.addAttributes(linkAttributes, range: NSRange(location: match.range.location, length: linkText.utf16.count))
|
||||
processedLine.addAttribute(.link, value: linkURL, range: NSRange(location: match.range.location, length: linkText.utf16.count))
|
||||
}
|
||||
attributedString.append(processedLine)
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import Cocoa
|
||||
import SwiftyMarkdown
|
||||
|
||||
class AcknowledgmentsTab {
|
||||
// going taller than this will crop the view on small screens or low scaling modes on retina displays
|
||||
static let maxTabHeight = CGFloat(450)
|
||||
|
||||
static func initTab() -> NSView {
|
||||
let textViews: [TextField] = ["Contributors", "Acknowledgments"].map {
|
||||
let textViews: [NSTextView] = ["Contributors", "Acknowledgments"].map {
|
||||
let markdownFileUrl = Bundle.main.url(forResource: $0, withExtension: "md")!
|
||||
let md = SwiftyMarkdown(url: markdownFileUrl)!
|
||||
md.h1.fontSize = 16
|
||||
md.underlineLinks = false
|
||||
md.bullet = "•"
|
||||
let textView = TextField(md.attributedString())
|
||||
textView.allowsEditingTextAttributes = true
|
||||
let content = try! String(contentsOf: markdownFileUrl, encoding: .utf8)
|
||||
let attributedString = Markdown.toAttributedString(content)
|
||||
let textView = NSTextView()
|
||||
textView.textContainer!.widthTracksTextView = true
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
textView.drawsBackground = true
|
||||
textView.backgroundColor = .clear
|
||||
textView.isSelectable = true
|
||||
textView.isEditable = false
|
||||
textView.enabledTextCheckingTypes = 0
|
||||
textView.frame.size.width = 230
|
||||
textView.textStorage!.setAttributedString(attributedString)
|
||||
textView.layoutManager!.ensureLayout(for: textView.textContainer!)
|
||||
textView.frame = textView.layoutManager!.usedRect(for: textView.textContainer!)
|
||||
textView.fit(textView.frame.width, textView.frame.height)
|
||||
return textView
|
||||
}
|
||||
let subGrid = GridView([textViews])
|
||||
@@ -27,11 +34,10 @@ class AcknowledgmentsTab {
|
||||
scrollView.scrollerKnobStyle = .default
|
||||
scrollView.documentView!.subviews = [subGrid]
|
||||
let totalWidth = subGrid.fittingSize.width
|
||||
let totalHeight = min(subGrid.fittingSize.height, AcknowledgmentsTab.maxTabHeight)
|
||||
scrollView.frame.size = NSSize(width: totalWidth, height: totalHeight)
|
||||
scrollView.frame.size = NSSize(width: totalWidth, height: maxTabHeight)
|
||||
scrollView.contentView.frame.size = scrollView.frame.size
|
||||
scrollView.documentView!.frame.size = subGrid.fittingSize
|
||||
scrollView.fit(totalWidth, totalHeight)
|
||||
scrollView.fit(totalWidth, maxTabHeight)
|
||||
|
||||
return scrollView
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user