feat: acknowledgments tab was slow to display (closes #3957)

This commit is contained in:
lwouis committed 2024-12-10 23:17:01 +01:00
1 parent 5601a556c9
commit 41ddd7c1a6
28 files changed
+697 -3332

No files matched your search

-1
View File
@@ -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
View File
@@ -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
+1 -5
View File
@@ -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
+597 -807
View File
File diff suppressed because it is too large. Load diff
-21
View File
@@ -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.
-360
View File
@@ -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 (`![Image](<Name In bundle>)`), codeblocks, blockquotes, and unordered lists!
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be 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/)
![Images](<Name of asset in bundle>)
> Blockquotes
- Bulleted
- Lists
- Including indented lists
- Up to three levels
- Neat!
1. Ordered
1. Lists
1. Including indented lists
- Up to three levels
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
![Screenshot](https://cl.ly/779e6964257a/swiftymarkdown-2020.png)
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: "![", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2),
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
]
static public var frontMatterRules = [
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
]
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
/// The styles to apply to any H1 headers found in the Markdown
open var h1 = LineStyles()
/// The styles to apply to any H2 headers found in the Markdown
open var h2 = LineStyles()
/// The styles to apply to any H3 headers found in the Markdown
open var h3 = LineStyles()
/// The styles to apply to any H4 headers found in the Markdown
open var h4 = LineStyles()
/// The styles to apply to any H5 headers found in the Markdown
open var h5 = LineStyles()
/// The styles to apply to any H6 headers found in the Markdown
open var h6 = LineStyles()
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
open var body = LineStyles()
/// The styles to apply to any blockquotes found in the Markdown
open var blockquotes = LineStyles()
/// The styles to apply to any links found in the Markdown
open var link = BasicStyles()
/// The styles to apply to any bold text found in the Markdown
open var bold = BasicStyles()
/// The styles to apply to any italic text found in the Markdown
open var italic = BasicStyles()
/// The styles to apply to any code blocks or inline code text found in the Markdown
open var code = BasicStyles()
open var strikethrough = BasicStyles()
public var bullet : String = "・"
public var underlineLinks : Bool = false
public var frontMatterAttributes : [String : String] {
get {
return self.lineProcessor.frontMatterAttributes
}
}
var currentType : MarkdownLineStyle = .body
var string : String
let tagList = "!\\_*`[]()"
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
var orderedListCount = 0
var orderedListIndentFirstOrderCount = 0
var orderedListIndentSecondOrderCount = 0
/**
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
- returns: An initialized SwiftyMarkdown object
*/
public init(string : String ) {
self.string = string
super.init()
self.setup()
}
/**
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
- parameter url: The location of the file to read
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
*/
public init?(url : URL ) {
do {
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
} catch {
self.string = ""
return nil
}
super.init()
self.setup()
}
func setup() {
#if os(macOS)
self.setFontColorForAllStyles(with: .labelColor)
#elseif !os(watchOS)
if #available(iOS 13.0, tvOS 13.0, *) {
self.setFontColorForAllStyles(with: .label)
}
#endif
}
/**
Set font size for all styles
- parameter size: size of font
*/
open func setFontSizeForAllStyles(with size: CGFloat) {
h1.fontSize = size
h2.fontSize = size
h3.fontSize = size
h4.fontSize = size
h5.fontSize = size
h6.fontSize = size
body.fontSize = size
italic.fontSize = size
bold.fontSize = size
code.fontSize = size
link.fontSize = size
link.fontSize = size
strikethrough.fontSize = size
}
#if os(macOS)
open func setFontColorForAllStyles(with color: NSColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
bold.color = color
code.color = color
link.color = color
blockquotes.color = color
strikethrough.color = color
}
#else
open func setFontColorForAllStyles(with color: UIColor) {
h1.color = color
h2.color = color
h3.color = color
h4.color = color
h5.color = color
h6.color = color
body.color = color
italic.color = color
bold.color = color
code.color = color
link.color = color
blockquotes.color = color
strikethrough.color = color
}
#endif
open func setFontNameForAllStyles(with name: String) {
h1.fontName = name
h2.fontName = name
h3.fontName = name
h4.fontName = name
h5.fontName = name
h6.fontName = name
body.fontName = name
italic.fontName = name
bold.fontName = name
code.fontName = name
link.fontName = name
blockquotes.fontName = name
strikethrough.fontName = name
}
/**
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
- returns: An NSAttributedString with the styles applied
*/
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
if let existentMarkdownString = markdownString {
self.string = existentMarkdownString
}
let attributedString = NSMutableAttributedString(string: "")
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
for (idx, line) in foundAttributes.enumerated() {
if idx > 0 {
attributedString.append(NSAttributedString(string: "\n"))
}
let finalTokens = self.tokeniser.process(line.line)
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
}
return attributedString
}
}
extension SwiftyMarkdown {
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
var finalTokens = tokens
let finalAttributedString = NSMutableAttributedString()
var attributes : [NSAttributedString.Key : AnyObject] = [:]
guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else {
preconditionFailure("The passed line style is not a valid Markdown Line Style")
}
var listItem = self.bullet
switch markdownLineStyle {
case .orderedList:
self.orderedListCount += 1
self.orderedListIndentFirstOrderCount = 0
self.orderedListIndentSecondOrderCount = 0
listItem = "\(self.orderedListCount)."
case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder:
self.orderedListIndentFirstOrderCount += 1
self.orderedListIndentSecondOrderCount = 0
if markdownLineStyle == .orderedListIndentFirstOrder {
listItem = "\(self.orderedListIndentFirstOrderCount)."
}
case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder:
self.orderedListIndentSecondOrderCount += 1
if markdownLineStyle == .orderedListIndentSecondOrder {
listItem = "\(self.orderedListIndentSecondOrderCount)."
}
default:
self.orderedListCount = 0
self.orderedListIndentFirstOrderCount = 0
self.orderedListIndentSecondOrderCount = 0
}
let lineProperties : LineProperties
switch markdownLineStyle {
case .h1:
lineProperties = self.h1
case .h2:
lineProperties = self.h2
case .h3:
lineProperties = self.h3
case .h4:
lineProperties = self.h4
case .h5:
lineProperties = self.h5
case .h6:
lineProperties = self.h6
case .codeblock:
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .blockquote:
lineProperties = self.blockquotes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 20.0
paragraphStyle.headIndent = 20.0
attributes[.paragraphStyle] = paragraphStyle
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
let interval : CGFloat = 30
var addition = interval
var indent = ""
switch line.lineStyle as! MarkdownLineStyle {
case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder:
addition = interval * 2
indent = "\t"
case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder:
addition = interval * 3
indent = "\t\t"
default:
break
}
lineProperties = body
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])]
paragraphStyle.defaultTabInterval = interval
paragraphStyle.headIndent = addition
attributes[.paragraphStyle] = paragraphStyle
finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0)
case .yaml:
lineProperties = body
case .previousH1:
lineProperties = body
case .previousH2:
lineProperties = body
case .body:
lineProperties = body
}
if lineProperties.alignment != .left {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = lineProperties.alignment
attributes[.paragraphStyle] = paragraphStyle
}
for token in finalTokens {
attributes[.font] = self.font(for: line)
attributes[.link] = nil
attributes[.strikethroughStyle] = nil
attributes[.foregroundColor] = self.color(for: line)
guard let styles = token.characterStyles as? [CharacterStyle] else {
continue
}
if styles.contains(.italic) {
attributes[.font] = self.font(for: line, characterOverride: .italic)
attributes[.foregroundColor] = self.italic.color
}
if styles.contains(.bold) {
attributes[.font] = self.font(for: line, characterOverride: .bold)
attributes[.foregroundColor] = self.bold.color
}
if styles.contains(.link), let url = token.metadataString {
attributes[.foregroundColor] = self.link.color
attributes[.font] = self.font(for: line, characterOverride: .link)
attributes[.link] = url as AnyObject
if underlineLinks {
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
}
}
if styles.contains(.strikethrough) {
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
attributes[.foregroundColor] = self.strikethrough.color
}
#if !os(watchOS)
if styles.contains(.image), let imageName = token.metadataString {
#if !os(macOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#elseif !os(watchOS)
let image1Attachment = NSTextAttachment()
image1Attachment.image = NSImage(named: imageName)
let str = NSAttributedString(attachment: image1Attachment)
finalAttributedString.append(str)
#endif
continue
}
#endif
if styles.contains(.code) {
attributes[.foregroundColor] = self.code.color
attributes[.font] = self.font(for: line, characterOverride: .code)
} else {
// Switch back to previous font
}
let str = NSAttributedString(string: token.outputString, attributes: attributes)
finalAttributedString.append(str)
}
return finalAttributedString
}
}
@@ -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
}
}
@@ -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
@@ -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>
@@ -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
@@ -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,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
+4 -2
View File
@@ -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;
};
-3
View File
@@ -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)
+69
View File
@@ -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
}