├── .gitignore ├── LICENSE ├── Logo.png ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Ink │ ├── API │ │ ├── Markdown.swift │ │ ├── MarkdownParser.swift │ │ └── Modifier.swift │ └── Internal │ │ ├── Blockquote.swift │ │ ├── Character+Classification.swift │ │ ├── Character+Escaping.swift │ │ ├── CodeBlock.swift │ │ ├── FormattedText.swift │ │ ├── Fragment.swift │ │ ├── HTML.swift │ │ ├── HTMLConvertible.swift │ │ ├── Hashable+AnyOf.swift │ │ ├── Heading.swift │ │ ├── HorizontalLine.swift │ │ ├── Image.swift │ │ ├── InlineCode.swift │ │ ├── KeyPathPatterns.swift │ │ ├── Link.swift │ │ ├── List.swift │ │ ├── Metadata.swift │ │ ├── Modifiable.swift │ │ ├── ModifierCollection.swift │ │ ├── NamedURLCollection.swift │ │ ├── Paragraph.swift │ │ ├── PlainTextConvertible.swift │ │ ├── Readable.swift │ │ ├── Reader.swift │ │ ├── Require.swift │ │ ├── Substring+Trimming.swift │ │ ├── Table.swift │ │ ├── TextStyle.swift │ │ ├── TextStyleMarker.swift │ │ ├── URL.swift │ │ └── URLDeclaration.swift └── InkCLI │ ├── Printing.swift │ └── main.swift └── Tests ├── InkTests ├── CodeTests.swift ├── HTMLTests.swift ├── HeadingTests.swift ├── HorizontalLineTests.swift ├── ImageTests.swift ├── LinkTests.swift ├── LinuxCompatibility.swift ├── ListTests.swift ├── MarkdownTests.swift ├── ModifierTests.swift ├── TableTests.swift ├── TextFormattingTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /build 3 | /.build 4 | /.swiftpm 5 | /*.xcodeproj 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Sundell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/Ink/bcc9f219900a62c4210e6db726035d7f03ae757b/Logo.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | swift build -c release 3 | install .build/release/ink-cli /usr/local/bin/ink 4 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | /** 4 | * Ink 5 | * Copyright (c) John Sundell 2019 6 | * MIT license, see LICENSE file for details 7 | */ 8 | 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "Ink", 13 | products: [ 14 | .library(name: "Ink", targets: ["Ink"]), 15 | .executable(name: "ink-cli", targets: ["InkCLI"]) 16 | ], 17 | targets: [ 18 | .target(name: "Ink"), 19 | .target(name: "InkCLI", dependencies: ["Ink"]), 20 | .testTarget(name: "InkTests", dependencies: ["Ink"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | “Ink” 3 |

4 | 5 |

6 | 7 | 8 | Swift Package Manager 9 | 10 | Mac + Linux 11 | 12 | Twitter: @johnsundell 13 | 14 |

15 | 16 | Welcome to **Ink**, a fast and flexible Markdown parser written in Swift. It can be used to convert Markdown-formatted strings into HTML, and also supports metadata parsing, as well as powerful customization options for fine-grained post-processing. It was built with a focus on Swift-based web development and other HTML-centered workflows. 17 | 18 | Ink is used to render all articles on [swiftbysundell.com](https://swiftbysundell.com). 19 | 20 | ## Converting Markdown into HTML 21 | 22 | To get started with Ink, all you have to do is to import it, and use its `MarkdownParser` type to convert any Markdown string into efficiently rendered HTML: 23 | 24 | ```swift 25 | import Ink 26 | 27 | let markdown: String = ... 28 | let parser = MarkdownParser() 29 | let html = parser.html(from: markdown) 30 | ``` 31 | 32 | That’s it! The resulting HTML can then be displayed as-is, or embedded into some other context — and if that’s all you need Ink for, then no more code is required. 33 | 34 | ## Automatic metadata parsing 35 | 36 | Ink also comes with metadata support built-in, meaning that you can define key/value pairs at the top of any Markdown document, which will then be automatically parsed into a Swift dictionary. 37 | 38 | To take advantage of that feature, call the `parse` method on `MarkdownParser`, which gives you a `Markdown` value that both contains any metadata found within the parsed Markdown string, as well as its HTML representation: 39 | 40 | ```swift 41 | let markdown: String = ... 42 | let parser = MarkdownParser() 43 | let result = parser.parse(markdown) 44 | 45 | let dateString = result.metadata["date"] 46 | let html = result.html 47 | ``` 48 | 49 | To define metadata values within a Markdown document, use the following syntax: 50 | 51 | ``` 52 | --- 53 | keyA: valueA 54 | keyB: valueB 55 | --- 56 | 57 | Markdown text... 58 | ``` 59 | 60 | The above format is also supported by many different Markdown editors and other tools, even though it’s not part of the [original Markdown spec](https://daringfireball.net/projects/markdown). 61 | 62 | ## Powerful customization 63 | 64 | Besides its [built-in parsing rules](#markdown-syntax-supported), which aims to cover the most common features found in the various flavors of Markdown, you can also customize how Ink performs its parsing through the use of *modifiers*. 65 | 66 | A modifier is defined using the `Modifier` type, and is associated with a given `Target`, which determines the kind of Markdown fragments that it will be used for. For example, here’s how an H3 tag could be added before each code block: 67 | 68 | ```swift 69 | var parser = MarkdownParser() 70 | 71 | let modifier = Modifier(target: .codeBlocks) { html, markdown in 72 | return "

This is a code block:

" + html 73 | } 74 | 75 | parser.addModifier(modifier) 76 | 77 | let markdown: String = ... 78 | let html = parser.html(from: markdown) 79 | ``` 80 | 81 | Modifiers are passed both the HTML that Ink generated for the given fragment, and its raw Markdown representation as well — both of which can be used to determine how each fragment should be customized. 82 | 83 | ## Performance built-in 84 | 85 | Ink was designed to be as fast and efficient as possible, to enable hundreds of full-length Markdown articles to be parsed in a matter of seconds, while still offering a fully customizable API as well. Two key characteristics make this possible: 86 | 87 | 1. Ink aims to get as close to `O(N)` complexity as possible, by minimizing the amount of times it needs to read the Markdown strings that are passed to it, and by optimizing its HTML rendering to be completely linear. While *true* `O(N)` complexity is impossible to achieve when it comes to Markdown parsing, because of its very flexible syntax, the goal is to come as close to that target as possible. 88 | 2. A high degree of memory efficiency is achieved thanks to Swift’s powerful `String` API, which Ink makes full use of — by using string indexes, ranges and substrings, rather than performing unnecessary string copying between its various operations. 89 | 90 | ## System requirements 91 | 92 | To be able to successfully use Ink, make sure that your system has Swift version 5.2 (or later) installed. If you’re using a Mac, also make sure that `xcode-select` is pointed at an Xcode installation that includes the required version of Swift, and that you’re running macOS Catalina (10.15) or later. 93 | 94 | Please note that Ink **does not** officially support any form of beta software, including beta versions of Xcode and macOS, or unreleased versions of Swift. 95 | 96 | ## Installation 97 | 98 | Ink is distributed using the [Swift Package Manager](https://swift.org/package-manager). To install it into a project, simply add it as a dependency within your `Package.swift` manifest: 99 | 100 | ```swift 101 | let package = Package( 102 | ... 103 | dependencies: [ 104 | .package(url: "https://github.com/johnsundell/ink.git", from: "0.1.0") 105 | ], 106 | ... 107 | ) 108 | ``` 109 | 110 | Then import Ink wherever you’d like to use it: 111 | 112 | ```swift 113 | import Ink 114 | ``` 115 | 116 | For more information on how to use the Swift Package Manager, check out [this article](https://www.swiftbysundell.com/articles/managing-dependencies-using-the-swift-package-manager), or [its official documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation). 117 | 118 | ## Command line tool 119 | 120 | Ink also ships with a simple but useful command line tool that lets you convert Markdown to HTML directly from the command line. 121 | 122 | To install it, clone the project and run `make`: 123 | 124 | ``` 125 | $ git clone https://github.com/johnsundell/Ink.git 126 | $ cd Ink 127 | $ make 128 | ``` 129 | 130 | The command line tool will be installed as `ink`, and can be passed Markdown text for conversion into HTML in several ways. 131 | 132 | Calling it without arguments will start reading from `stdin` until terminated with `Ctrl+D`: 133 | 134 | ``` 135 | $ ink 136 | ``` 137 | 138 | Markdown text can be piped in when `ink` is called without arguments: 139 | 140 | ``` 141 | $ echo "*Hello World*" | ink 142 | ``` 143 | 144 | A single argument is treated as a filename, and the corresponding file will be parsed: 145 | 146 | ``` 147 | $ ink file.md 148 | ``` 149 | 150 | A Markdown string can be passed directly using the `-m` or `--markdown` flag: 151 | 152 | ``` 153 | $ ink -m "*Hello World*" 154 | ``` 155 | 156 | You can of course also build your own command line tools that utilizes Ink in more advanced ways by importing it as a package. 157 | 158 | ## Markdown syntax supported 159 | 160 | Ink supports the following Markdown features: 161 | 162 | - Headings (H1 - H6), using leading pound signs, for example `## H2`. 163 | - Italic text, by surrounding a piece of text with either an asterisk (`*`), or an underscore (`_`). For example `*Italic text*`. 164 | - Bold text, by surrounding a piece of text with either two asterisks (`**`), or two underscores (`__`). For example `**Bold text**`. 165 | - Text strikethrough, by surrounding a piece of text with two tildes (`~~`), for example `~~Strikethrough text~~`. 166 | - Inline code, marked with a backtick on either site of the code. 167 | - Code blocks, marked with three or more backticks both above and below the block. 168 | - Links, using the following syntax: `[Title](url)`. 169 | - Images, using the following syntax: `![Alt text](image-url)`. 170 | - Both images and links can also use reference URLs, which can be defined anywhere in a Markdown document using this syntax: `[referenceName]: url`. 171 | - Both ordered lists (using numbers followed by a period (`.`) or right parenthesis (`)`) as bullets) and unordered lists (using either a dash (`-`), plus (`+`), or asterisk (`*`) as bullets) are supported. 172 | - Ordered lists start from the index of the first entry 173 | - Nested lists are supported as well, by indenting any part of a list that should be nested within its parent. 174 | - Horizontal lines can be placed using either three asterisks (`***`) or three dashes (`---`) on a new line. 175 | - HTML can be inlined both at the root level, and within text paragraphs. 176 | - Blockquotes can be created by placing a greater-than arrow at the start of a line, like this: `> This is a blockquote`. 177 | - Tables can be created using the following syntax (the line consisting of dashes (`-`) can be omitted to create a table without a header row): 178 | ``` 179 | | Header | Header 2 | 180 | | ------ | -------- | 181 | | Row 1 | Cell 1 | 182 | | Row 2 | Cell 2 | 183 | ``` 184 | 185 | Please note that, being a very young implementation, Ink does not fully support all Markdown specs, such as [CommonMark](https://commonmark.org). Ink definitely aims to cover as much ground as possible, and to include support for the most commonly used Markdown features, but if complete CommonMark compatibility is what you’re looking for — then you might want to check out tools like [CMark](https://github.com/commonmark/cmark). 186 | 187 | ## Internal architecture 188 | 189 | Ink uses a highly modular [rule-based](https://www.swiftbysundell.com/articles/rule-based-logic-in-swift) internal architecture, to enable new rules and formatting options to be added without impacting the system as a whole. 190 | 191 | Each Markdown fragment is individually parsed and rendered by a type conforming to the internal `Readable` and `HTMLConvertible` protocols — such as `FormattedText`, `List`, and `Image`. 192 | 193 | To parse a part of a Markdown document, each fragment type uses a `Reader` instance to read the Markdown string, and to make assertions about its structure. Errors are [used as control flow](https://www.swiftbysundell.com/articles/using-errors-as-control-flow-in-swift) to signal whether a parsing operation was successful or not, which in turn enables the parent context to decide whether to advance the current `Reader` instance, or whether to rewind it. 194 | 195 | A good place to start exploring Ink’s implementation is to look at the main `MarkdownParser` type’s `parse` method, and to then dive deeper into the various `Fragment` implementations, and the `Reader` type. 196 | 197 | ## Credits 198 | 199 | Ink was originally written by [John Sundell](https://twitter.com/johnsundell) as part of the Publish suite of static site generation tools, which is used to build and generate [Swift by Sundell](https://swiftbysundell.com). The other tools that make up the Publish suite will also be open sourced soon. 200 | 201 | The Markdown format was created by [John Gruber](https://twitter.com/gruber). You can find [more information about it here](https://daringfireball.net/projects/markdown). 202 | 203 | ## Contributions and support 204 | 205 | Ink is developed completely in the open, and your contributions are more than welcome. 206 | 207 | Before you start using Ink in any of your projects, it’s highly recommended that you spend a few minutes familiarizing yourself with its documentation and internal implementation, so that you’ll be ready to tackle any issues or edge cases that you might encounter. 208 | 209 | Since this is a very young project, it’s likely to have many limitations and missing features, which is something that can really only be discovered and addressed as more people start using it. While Ink is used in production to render all of [Swift by Sundell](https://swiftbysundell.com), it’s recommended that you first try it out for your specific use case, to make sure it supports the features that you need. 210 | 211 | This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or by improving the documentation wherever it’s found to be lacking. 212 | 213 | If you wish to make a change, [open a Pull Request](https://github.com/JohnSundell/Ink/pull/new) — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there. 214 | 215 | Hope you’ll enjoy using **Ink**! 216 | -------------------------------------------------------------------------------- /Sources/Ink/API/Markdown.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | /// 8 | /// A parsed Markdown value, which contains its rendered 9 | /// HTML representation, as well as any metadata found at 10 | /// the top of the Markdown document. 11 | /// 12 | /// You create instances of this type by parsing Markdown 13 | /// strings using `MarkdownParser`. 14 | public struct Markdown { 15 | /// The HTML representation of the Markdown, ready to 16 | /// be rendered in a web browser. 17 | public var html: String 18 | /// The inferred title of the document, from any top-level 19 | /// heading found when parsing. If the Markdown text contained 20 | /// two top-level headings, then this property will contain 21 | /// the first one. Note that this property does not take modifiers 22 | /// into acccount. 23 | public var title: String? { 24 | get { makeTitle() } 25 | set { overrideTitle(with: newValue) } 26 | } 27 | /// Any metadata values found at the top of the Markdown 28 | /// document. See this project's README for more information. 29 | public var metadata: [String : String] 30 | 31 | private let titleHeading: Heading? 32 | private var titleStorage = TitleStorage() 33 | 34 | internal init(html: String, 35 | titleHeading: Heading?, 36 | metadata: [String : String]) { 37 | self.html = html 38 | self.titleHeading = titleHeading 39 | self.metadata = metadata 40 | } 41 | } 42 | 43 | private extension Markdown { 44 | final class TitleStorage { 45 | var title: String? 46 | } 47 | 48 | mutating func overrideTitle(with title: String?) { 49 | let storage = TitleStorage() 50 | storage.title = title 51 | titleStorage = storage 52 | } 53 | 54 | func makeTitle() -> String? { 55 | if let stored = titleStorage.title { return stored } 56 | titleStorage.title = titleHeading?.plainText() 57 | return titleStorage.title 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Ink/API/MarkdownParser.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | /// 8 | /// A parser used to convert Markdown text into HTML. 9 | /// 10 | /// You can use an instance of this type to either convert 11 | /// a Markdown string into an HTML string, or into a `Markdown` 12 | /// value, which also contains any metadata values found in 13 | /// the parsed Markdown text. 14 | /// 15 | /// To customize how this parser performs its work, attach 16 | /// a `Modifier` using the `addModifier` method. 17 | public struct MarkdownParser { 18 | private var modifiers: ModifierCollection 19 | 20 | /// Initialize an instance, optionally passing an array 21 | /// of modifiers used to customize the parsing process. 22 | public init(modifiers: [Modifier] = []) { 23 | self.modifiers = ModifierCollection(modifiers: modifiers) 24 | } 25 | 26 | /// Add a modifier to this parser, which can be used to 27 | /// customize the parsing process. See `Modifier` for more info. 28 | public mutating func addModifier(_ modifier: Modifier) { 29 | modifiers.insert(modifier) 30 | } 31 | 32 | /// Convert a Markdown string into HTML, discarding any metadata 33 | /// found in the process. To preserve the Markdown's metadata, 34 | /// use the `parse` method instead. 35 | public func html(from markdown: String) -> String { 36 | parse(markdown).html 37 | } 38 | 39 | /// Parse a Markdown string into a `Markdown` value, which contains 40 | /// both the HTML representation of the given string, and also any 41 | /// metadata values found within it. 42 | public func parse(_ markdown: String) -> Markdown { 43 | var reader = Reader(string: markdown) 44 | var fragments = [ParsedFragment]() 45 | var urlsByName = [String : URL]() 46 | var titleHeading: Heading? 47 | var metadata: Metadata? 48 | 49 | while !reader.didReachEnd { 50 | reader.discardWhitespacesAndNewlines() 51 | guard !reader.didReachEnd else { break } 52 | 53 | do { 54 | if metadata == nil, fragments.isEmpty, reader.currentCharacter == "-" { 55 | if let parsedMetadata = try? Metadata.readOrRewind(using: &reader) { 56 | metadata = parsedMetadata.applyingModifiers(modifiers) 57 | continue 58 | } 59 | } 60 | 61 | guard reader.currentCharacter != "[" else { 62 | let declaration = try URLDeclaration.readOrRewind(using: &reader) 63 | urlsByName[declaration.name] = declaration.url 64 | continue 65 | } 66 | 67 | let type = fragmentType(for: reader.currentCharacter, 68 | nextCharacter: reader.nextCharacter) 69 | 70 | #if swift(>=5.3) && swift(<5.4) && os(Linux) 71 | // inline function call to work around https://bugs.swift.org/browse/SR-13645 72 | let fragment: ParsedFragment = try { 73 | let startIndex = reader.currentIndex 74 | let fragment = try type.readOrRewind(using: &reader) 75 | let rawString = reader.characters(in: startIndex.. Fragment, 121 | reader: inout Reader) rethrows -> ParsedFragment { 122 | let startIndex = reader.currentIndex 123 | let fragment = try closure(&reader) 124 | let rawString = reader.characters(in: startIndex.. Fragment.Type { 130 | switch character { 131 | case "#": return Heading.self 132 | case "!": return Image.self 133 | case "<": return HTML.self 134 | case ">": return Blockquote.self 135 | case "`": return CodeBlock.self 136 | case "-" where character == nextCharacter, 137 | "*" where character == nextCharacter: 138 | return HorizontalLine.self 139 | case "-", "*", "+", \.isNumber: return List.self 140 | case "|": return Table.self 141 | default: return Paragraph.self 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/Ink/API/Modifier.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | /// 8 | /// Modifiers can be attached to a `MarkdownParser` and are used 9 | /// to customize Ink's parsing process. Each modifier is associated 10 | /// with a given `Target`, which determines which type of Markdown 11 | /// fragments that it is capable of modifying. 12 | /// 13 | /// You can use a `Modifier` to adjust the HTML that was generated 14 | /// for a given fragment, or to inject completely custom HTML based 15 | /// on the fragment's raw Markdown representation. 16 | public struct Modifier { 17 | /// The type of input that each modifier is given, which both 18 | /// contains the HTML that was generated for a fragment, and 19 | /// its raw Markdown representation. Note that for metadata 20 | /// targets, the two input arguments will be equivalent. 21 | public typealias Input = (html: String, markdown: Substring) 22 | /// The type of closure that Modifiers are based on. Each 23 | /// modifier is given a set of input, and is expected to return 24 | /// an HTML string after performing its modifications. 25 | public typealias Closure = (Input) -> String 26 | 27 | /// The modifier's target, that defines what kind of fragment 28 | /// that it's used to modify. See `Target` for more info. 29 | public var target: Target 30 | /// The closure that makes up the modifier's body. 31 | public var closure: Closure 32 | 33 | /// Initialize an instance with the kind of target that the modifier 34 | /// should be used on, and a closure that makes up its body. 35 | public init(target: Target, closure: @escaping Closure) { 36 | self.target = target 37 | self.closure = closure 38 | } 39 | } 40 | 41 | public extension Modifier { 42 | enum Target { 43 | case metadataKeys 44 | case metadataValues 45 | case blockquotes 46 | case codeBlocks 47 | case headings 48 | case horizontalLines 49 | case html 50 | case images 51 | case inlineCode 52 | case links 53 | case lists 54 | case paragraphs 55 | case tables 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Blockquote.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Blockquote: Fragment { 8 | var modifierTarget: Modifier.Target { .blockquotes } 9 | 10 | private var text: FormattedText 11 | 12 | static func read(using reader: inout Reader) throws -> Blockquote { 13 | try reader.read(">") 14 | try reader.readWhitespaces() 15 | 16 | var text = FormattedText.readLine(using: &reader) 17 | 18 | while !reader.didReachEnd { 19 | switch reader.currentCharacter { 20 | case \.isNewline: 21 | return Blockquote(text: text) 22 | case ">": 23 | reader.advanceIndex() 24 | default: 25 | break 26 | } 27 | 28 | text.append(FormattedText.readLine(using: &reader)) 29 | } 30 | 31 | return Blockquote(text: text) 32 | } 33 | 34 | func html(usingURLs urls: NamedURLCollection, 35 | modifiers: ModifierCollection) -> String { 36 | let body = text.html(usingURLs: urls, modifiers: modifiers) 37 | return "

\(body)

" 38 | } 39 | 40 | func plainText() -> String { 41 | text.plainText() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Character+Classification.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal extension Character { 8 | var isSameLineWhitespace: Bool { 9 | isWhitespace && !isNewline 10 | } 11 | } 12 | 13 | internal extension Set where Element == Character { 14 | static let boldItalicStyleMarkers: Self = ["*", "_"] 15 | static let allStyleMarkers: Self = boldItalicStyleMarkers.union(["~"]) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Character+Escaping.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal extension Character { 8 | var escaped: String? { 9 | switch self { 10 | case ">": return ">" 11 | case "<": return "<" 12 | case "&": return "&" 13 | default: return nil 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/CodeBlock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct CodeBlock: Fragment { 8 | var modifierTarget: Modifier.Target { .codeBlocks } 9 | 10 | private static let marker: Character = "`" 11 | 12 | private var language: Substring 13 | private var code: String 14 | 15 | static func read(using reader: inout Reader) throws -> CodeBlock { 16 | let startingMarkerCount = reader.readCount(of: marker) 17 | try require(startingMarkerCount >= 3) 18 | reader.discardWhitespaces() 19 | 20 | let language = reader 21 | .readUntilEndOfLine() 22 | .trimmingTrailingWhitespaces() 23 | 24 | var code = "" 25 | 26 | while !reader.didReachEnd { 27 | if code.last == "\n", reader.currentCharacter == marker { 28 | let markerCount = reader.readCount(of: marker) 29 | 30 | if markerCount == startingMarkerCount { 31 | break 32 | } else { 33 | code.append(String(repeating: marker, count: markerCount)) 34 | guard !reader.didReachEnd else { break } 35 | } 36 | } 37 | 38 | if let escaped = reader.currentCharacter.escaped { 39 | code.append(escaped) 40 | } else { 41 | code.append(reader.currentCharacter) 42 | } 43 | 44 | reader.advanceIndex() 45 | } 46 | 47 | return CodeBlock(language: language, code: code) 48 | } 49 | 50 | func html(usingURLs urls: NamedURLCollection, 51 | modifiers: ModifierCollection) -> String { 52 | let languageClass = language.isEmpty ? "" : " class=\"language-\(language)\"" 53 | return "
\(code)
" 54 | } 55 | 56 | func plainText() -> String { 57 | code 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/FormattedText.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct FormattedText: Readable, HTMLConvertible, PlainTextConvertible { 8 | private var components = [Component]() 9 | 10 | static func read(using reader: inout Reader) -> Self { 11 | read(using: &reader, terminators: []) 12 | } 13 | 14 | static func readLine(using reader: inout Reader) -> Self { 15 | let text = read(using: &reader, terminators: ["\n"]) 16 | if !reader.didReachEnd { reader.advanceIndex() } 17 | return text 18 | } 19 | 20 | static func read(using reader: inout Reader, 21 | terminators: Set) -> Self { 22 | var parser = Parser(reader: reader, terminators: terminators) 23 | parser.parse() 24 | reader = parser.reader 25 | return parser.text 26 | } 27 | 28 | func html(usingURLs urls: NamedURLCollection, 29 | modifiers: ModifierCollection) -> String { 30 | components.reduce(into: "") { string, component in 31 | switch component { 32 | case .linebreak: 33 | string.append("
") 34 | case .text(let text): 35 | string.append(String(text)) 36 | case .styleMarker(let marker): 37 | let html = marker.html(usingURLs: urls, modifiers: modifiers) 38 | string.append(html) 39 | case .fragment(let fragment, let rawString): 40 | let html = fragment.html( 41 | usingURLs: urls, 42 | rawString: rawString, 43 | applyingModifiers: modifiers 44 | ) 45 | 46 | string.append(html) 47 | } 48 | } 49 | } 50 | 51 | func plainText() -> String { 52 | components.reduce(into: "") { string, component in 53 | switch component { 54 | case .linebreak: 55 | string.append("\n") 56 | case .text(let text): 57 | string.append(String(text)) 58 | case .styleMarker: 59 | break 60 | case .fragment(let fragment, _): 61 | string.append(fragment.plainText()) 62 | } 63 | } 64 | } 65 | 66 | mutating func append(_ text: FormattedText, separator: Substring = "") { 67 | let separator = separator.isEmpty ? [] : [Component.text(separator)] 68 | components += separator + text.components 69 | } 70 | } 71 | 72 | private extension FormattedText { 73 | enum Component { 74 | case linebreak 75 | case text(Substring) 76 | case styleMarker(TextStyleMarker) 77 | case fragment(Fragment, rawString: Substring) 78 | } 79 | 80 | struct Parser { 81 | var reader: Reader 82 | let terminators: Set 83 | var text = FormattedText() 84 | var pendingTextRange: Range 85 | var activeStyles = Set() 86 | var activeStyleMarkers = [TextStyleMarker]() 87 | 88 | init(reader: Reader, terminators: Set) { 89 | self.reader = reader 90 | self.terminators = terminators 91 | self.pendingTextRange = reader.currentIndex..() 246 | 247 | for otherMarker in activeStyleMarkers.reversed() { 248 | stylesToRemove.insert(otherMarker.style) 249 | 250 | if otherMarker.style == marker.style { 251 | break 252 | } 253 | 254 | otherMarker.isValid = false 255 | } 256 | 257 | activeStyleMarkers.removeLast(stylesToRemove.count) 258 | activeStyles.subtract(stylesToRemove) 259 | } 260 | 261 | private mutating func turnBoldMarkerIntoItalicIfNeeded(_ marker: TextStyleMarker) { 262 | guard marker.style == .bold, activeStyles.contains(.italic) else { return } 263 | guard !reader.didReachEnd else { return } 264 | guard reader.currentCharacter.isAny(of: .boldItalicStyleMarkers) else { return } 265 | 266 | marker.style = .italic 267 | marker.rawMarkers.removeLast() 268 | reader.rewindIndex() 269 | } 270 | 271 | private mutating func handleUnterminatedStyleMarkers() { 272 | var boldMarker: TextStyleMarker? 273 | var italicMarker: TextStyleMarker? 274 | 275 | if activeStyles.isSuperset(of: [.bold, .italic]) { 276 | markerIteration: for marker in activeStyleMarkers { 277 | switch marker.style { 278 | case .bold: 279 | marker.style = .italic 280 | 281 | if let otherMarker = italicMarker { 282 | guard marker.characterRange.lowerBound != 283 | otherMarker.characterRange.upperBound else { 284 | italicMarker = nil 285 | break markerIteration 286 | } 287 | 288 | marker.suffix = marker.rawMarkers.removeLast() 289 | marker.kind = .closing 290 | } else { 291 | marker.prefix = marker.rawMarkers.removeFirst() 292 | } 293 | 294 | boldMarker = marker 295 | case .italic: 296 | if let otherMarker = boldMarker { 297 | guard marker.characterRange.lowerBound != 298 | otherMarker.characterRange.upperBound else { 299 | if let prefix = otherMarker.prefix { 300 | otherMarker.rawMarkers = "\(prefix)\(otherMarker.rawMarkers)" 301 | } else if let suffix = otherMarker.suffix { 302 | otherMarker.rawMarkers.append(suffix) 303 | } 304 | 305 | boldMarker = nil 306 | break markerIteration 307 | } 308 | 309 | marker.kind = .closing 310 | } 311 | 312 | italicMarker = marker 313 | case .strikethrough: 314 | break 315 | } 316 | } 317 | } 318 | 319 | for marker in activeStyleMarkers { 320 | guard marker !== boldMarker else { continue } 321 | guard marker !== italicMarker else { continue } 322 | marker.isValid = false 323 | } 324 | } 325 | 326 | private mutating func skipCharacter() { 327 | reader.advanceIndex() 328 | pendingTextRange = reader.currentIndex.. Fragment.Type? { 332 | switch reader.currentCharacter { 333 | case "`": return InlineCode.self 334 | case "[": return Link.self 335 | case "!": return Image.self 336 | case "<": return HTML.self 337 | default: return nil 338 | } 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Fragment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal typealias Fragment = Readable & Modifiable & HTMLConvertible & PlainTextConvertible 8 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/HTML.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct HTML: Fragment { 8 | var modifierTarget: Modifier.Target { .html } 9 | 10 | private var string: Substring 11 | 12 | static func read(using reader: inout Reader) throws -> HTML { 13 | let startIndex = reader.currentIndex 14 | let rootElement = try reader.readHTMLElement() 15 | 16 | guard !rootElement.isSelfClosing else { 17 | let html = reader.characters(in: startIndex.. 0 else { break } 47 | } 48 | } 49 | 50 | let html = reader.characters(in: startIndex.. String { 56 | String(string) 57 | } 58 | 59 | func plainText() -> String { 60 | // Since we want to strip all HTML from plain text output, 61 | // there is nothing to return here, just an empty string. 62 | "" 63 | } 64 | } 65 | 66 | private extension Reader { 67 | typealias HTMLElement = (name: Substring, isSelfClosing: Bool) 68 | 69 | mutating func readHTMLElement() throws -> HTMLElement { 70 | try read("<") 71 | let startIndex = currentIndex 72 | 73 | while !didReachEnd { 74 | guard !currentCharacter.isWhitespace, currentCharacter != ">" else { 75 | let name = characters(in: startIndex..", allowLineBreaks: true) 78 | 79 | guard name.last != "/" else { 80 | return (name.dropLast(), true) 81 | } 82 | 83 | return (name, suffix.last == "/" || name == "!--") 84 | } 85 | 86 | advanceIndex() 87 | } 88 | 89 | throw Error() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/HTMLConvertible.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal protocol HTMLConvertible { 8 | func html(usingURLs urls: NamedURLCollection, 9 | modifiers: ModifierCollection) -> String 10 | } 11 | 12 | extension HTMLConvertible where Self: Modifiable { 13 | func html(usingURLs urls: NamedURLCollection, 14 | rawString: Substring, 15 | applyingModifiers modifiers: ModifierCollection) -> String { 16 | var html = self.html(usingURLs: urls, modifiers: modifiers) 17 | 18 | modifiers.applyModifiers(for: modifierTarget) { modifier in 19 | html = modifier.closure((html, rawString)) 20 | } 21 | 22 | return html 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Hashable+AnyOf.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal extension Hashable { 8 | func isAny(of candidates: Set) -> Bool { 9 | return candidates.contains(self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Heading.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Heading: Fragment { 8 | var modifierTarget: Modifier.Target { .headings } 9 | var level: Int 10 | 11 | private var text: FormattedText 12 | 13 | static func read(using reader: inout Reader) throws -> Heading { 14 | let level = reader.readCount(of: "#") 15 | try require(level > 0 && level < 7) 16 | try reader.readWhitespaces() 17 | let text = FormattedText.read(using: &reader, terminators: ["\n"]) 18 | 19 | return Heading(level: level, text: text) 20 | } 21 | 22 | func html(usingURLs urls: NamedURLCollection, 23 | modifiers: ModifierCollection) -> String { 24 | let body = stripTrailingMarkers( 25 | from: text.html(usingURLs: urls, modifiers: modifiers) 26 | ) 27 | 28 | let tagName = "h\(level)" 29 | return "<\(tagName)>\(body)" 30 | } 31 | 32 | func plainText() -> String { 33 | stripTrailingMarkers(from: text.plainText()) 34 | } 35 | } 36 | 37 | private extension Heading { 38 | func stripTrailingMarkers(from text: String) -> String { 39 | guard !text.isEmpty else { return text } 40 | 41 | let lastCharacterIndex = text.index(before: text.endIndex) 42 | var trimIndex = lastCharacterIndex 43 | 44 | while text[trimIndex] == "#", trimIndex != text.startIndex { 45 | trimIndex = text.index(before: trimIndex) 46 | } 47 | 48 | if trimIndex != lastCharacterIndex { 49 | return String(text[.. HorizontalLine { 11 | guard reader.currentCharacter.isAny(of: ["-", "*"]) else { 12 | throw Reader.Error() 13 | } 14 | 15 | try require(reader.readCount(of: reader.currentCharacter) > 2) 16 | try require(reader.readUntilEndOfLine().isEmpty) 17 | return HorizontalLine() 18 | } 19 | 20 | func html(usingURLs urls: NamedURLCollection, 21 | modifiers: ModifierCollection) -> String { 22 | "
" 23 | } 24 | 25 | func plainText() -> String { 26 | // Since we want to strip all HTML from plain text output, 27 | // there is nothing to return here, just an empty string. 28 | "" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Image.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Image: Fragment { 8 | var modifierTarget: Modifier.Target { .images } 9 | 10 | private var link: Link 11 | 12 | static func read(using reader: inout Reader) throws -> Image { 13 | try reader.read("!") 14 | return try Image(link: .read(using: &reader)) 15 | } 16 | 17 | func html(usingURLs urls: NamedURLCollection, 18 | modifiers: ModifierCollection) -> String { 19 | let url = link.target.url(from: urls) 20 | var alt = link.text.html(usingURLs: urls, modifiers: modifiers) 21 | 22 | if !alt.isEmpty { 23 | alt = " alt=\"\(alt)\"" 24 | } 25 | 26 | return "" 27 | } 28 | 29 | func plainText() -> String { 30 | link.plainText() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/InlineCode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | struct InlineCode: Fragment { 8 | var modifierTarget: Modifier.Target { .inlineCode } 9 | 10 | private var code: String 11 | 12 | static func read(using reader: inout Reader) throws -> InlineCode { 13 | try reader.read("`") 14 | var code = "" 15 | 16 | while !reader.didReachEnd { 17 | switch reader.currentCharacter { 18 | case \.isNewline: 19 | throw Reader.Error() 20 | case "`": 21 | reader.advanceIndex() 22 | return InlineCode(code: code) 23 | default: 24 | if let escaped = reader.currentCharacter.escaped { 25 | code.append(escaped) 26 | } else { 27 | code.append(reader.currentCharacter) 28 | } 29 | 30 | reader.advanceIndex() 31 | } 32 | } 33 | 34 | throw Reader.Error() 35 | } 36 | 37 | func html(usingURLs urls: NamedURLCollection, 38 | modifiers: ModifierCollection) -> String { 39 | return "\(code)" 40 | } 41 | 42 | func plainText() -> String { 43 | code 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/KeyPathPatterns.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal func ~=(rhs: KeyPath, lhs: T) -> Bool { 8 | lhs[keyPath: rhs] 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Link.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Link: Fragment { 8 | var modifierTarget: Modifier.Target { .links } 9 | 10 | var target: Target 11 | var text: FormattedText 12 | 13 | static func read(using reader: inout Reader) throws -> Link { 14 | try reader.read("[") 15 | let text = FormattedText.read(using: &reader, terminators: ["]"]) 16 | try reader.read("]") 17 | 18 | guard !reader.didReachEnd else { throw Reader.Error() } 19 | 20 | if reader.currentCharacter == "(" { 21 | reader.advanceIndex() 22 | let url = try reader.read(until: ")", balanceAgainst: "(") 23 | return Link(target: .url(url), text: text) 24 | } else { 25 | try reader.read("[") 26 | let reference = try reader.read(until: "]") 27 | return Link(target: .reference(reference), text: text) 28 | } 29 | } 30 | 31 | func html(usingURLs urls: NamedURLCollection, 32 | modifiers: ModifierCollection) -> String { 33 | let url = target.url(from: urls) 34 | let title = text.html(usingURLs: urls, modifiers: modifiers) 35 | return "\(title)" 36 | } 37 | 38 | func plainText() -> String { 39 | text.plainText() 40 | } 41 | } 42 | 43 | extension Link { 44 | enum Target { 45 | case url(URL) 46 | case reference(Substring) 47 | } 48 | } 49 | 50 | extension Link.Target { 51 | func url(from urls: NamedURLCollection) -> URL { 52 | switch self { 53 | case .url(let url): 54 | return url 55 | case .reference(let name): 56 | return urls.url(named: name) ?? name 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/List.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct List: Fragment { 8 | var modifierTarget: Modifier.Target { .lists } 9 | 10 | private var listMarker: Character 11 | private var kind: Kind 12 | private var items = [Item]() 13 | 14 | static func read(using reader: inout Reader) throws -> List { 15 | // Calculate initial indentation 16 | var indentationLength = 0 17 | while reader.previousCharacter?.isSameLineWhitespace == true { 18 | indentationLength += 1 19 | reader.rewindIndex() 20 | } 21 | reader.advanceIndex(by: indentationLength) 22 | 23 | return try read(using: &reader, indentationLength: indentationLength) 24 | } 25 | 26 | private static func read(using reader: inout Reader, 27 | indentationLength: Int) throws -> List { 28 | let startIndex = reader.currentIndex 29 | let isOrdered = reader.currentCharacter.isNumber 30 | 31 | var list: List 32 | 33 | if isOrdered { 34 | let firstNumberString = try reader.readCharacters(matching: \.isNumber, max: 9) 35 | let firstNumber = Int(firstNumberString) ?? 1 36 | 37 | let listMarker = try reader.readCharacter(in: List.orderedListMarkers) 38 | list = List(listMarker: listMarker, kind: .ordered(firstNumber: firstNumber)) 39 | } else { 40 | let listMarker = reader.currentCharacter 41 | list = List(listMarker: listMarker, kind: .unordered) 42 | } 43 | 44 | reader.moveToIndex(startIndex) 45 | 46 | func addTextToLastItem() throws { 47 | try require(!list.items.isEmpty) 48 | let text = FormattedText.readLine(using: &reader) 49 | var lastItem = list.items.removeLast() 50 | lastItem.text.append(text, separator: " ") 51 | list.items.append(lastItem) 52 | } 53 | 54 | while !reader.didReachEnd { 55 | switch reader.currentCharacter { 56 | case \.isNewline: 57 | return list 58 | case \.isWhitespace: 59 | guard !list.items.isEmpty else { 60 | try reader.readWhitespaces() 61 | continue 62 | } 63 | 64 | let itemIndentationLength = try reader.readWhitespaces().count 65 | 66 | if itemIndentationLength < indentationLength { 67 | return list 68 | } else if itemIndentationLength == indentationLength { 69 | continue 70 | } 71 | 72 | let fallbackIndex = reader.currentIndex 73 | 74 | do { 75 | let nestedList = try List.read( 76 | using: &reader, indentationLength: 77 | itemIndentationLength 78 | ) 79 | 80 | var lastItem = list.items.removeLast() 81 | lastItem.nestedList = nestedList 82 | list.items.append(lastItem) 83 | } catch { 84 | reader.moveToIndex(fallbackIndex) 85 | try addTextToLastItem() 86 | } 87 | case \.isNumber: 88 | guard case .ordered = list.kind else { 89 | try addTextToLastItem() 90 | continue 91 | } 92 | 93 | let startIndex = reader.currentIndex 94 | 95 | do { 96 | try reader.readCharacters(matching: \.isNumber, max: 9) 97 | let foundMarker = try reader.readCharacter(in: List.orderedListMarkers) 98 | 99 | guard foundMarker == list.listMarker else { 100 | reader.moveToIndex(startIndex) 101 | return list 102 | } 103 | 104 | try reader.readWhitespaces() 105 | 106 | list.items.append(Item(text: .readLine(using: &reader))) 107 | } catch { 108 | reader.moveToIndex(startIndex) 109 | try addTextToLastItem() 110 | } 111 | case "-", "*", "+": 112 | guard let nextCharacter = reader.nextCharacter, 113 | nextCharacter.isSameLineWhitespace else { 114 | try addTextToLastItem() 115 | continue 116 | } 117 | 118 | guard reader.currentCharacter == list.listMarker else { 119 | return list 120 | } 121 | 122 | reader.advanceIndex() 123 | try reader.readWhitespaces() 124 | list.items.append(Item(text: .readLine(using: &reader))) 125 | default: 126 | try addTextToLastItem() 127 | } 128 | } 129 | 130 | return list 131 | } 132 | 133 | func html(usingURLs urls: NamedURLCollection, 134 | modifiers: ModifierCollection) -> String { 135 | let tagName: String 136 | let startAttribute: String 137 | 138 | switch kind { 139 | case .unordered: 140 | tagName = "ul" 141 | startAttribute = "" 142 | case let .ordered(startingIndex): 143 | tagName = "ol" 144 | 145 | if startingIndex != 1 { 146 | startAttribute = #" start="\#(startingIndex)""# 147 | } else { 148 | startAttribute = "" 149 | } 150 | } 151 | 152 | let body = items.reduce(into: "") { html, item in 153 | html.append(item.html(usingURLs: urls, modifiers: modifiers)) 154 | } 155 | 156 | return "<\(tagName)\(startAttribute)>\(body)" 157 | } 158 | 159 | func plainText() -> String { 160 | var isFirst = true 161 | 162 | return items.reduce(into: "") { string, item in 163 | if isFirst { 164 | isFirst = false 165 | } else { 166 | string.append(", ") 167 | } 168 | 169 | string.append(item.text.plainText()) 170 | } 171 | } 172 | } 173 | 174 | private extension List { 175 | struct Item: HTMLConvertible { 176 | var text: FormattedText 177 | var nestedList: List? = nil 178 | 179 | func html(usingURLs urls: NamedURLCollection, 180 | modifiers: ModifierCollection) -> String { 181 | let textHTML = text.html(usingURLs: urls, modifiers: modifiers) 182 | let listHTML = nestedList?.html(usingURLs: urls, modifiers: modifiers) 183 | return "
  • \(textHTML)\(listHTML ?? "")
  • " 184 | } 185 | } 186 | 187 | enum Kind { 188 | case unordered 189 | case ordered(firstNumber: Int) 190 | } 191 | 192 | static let orderedListMarkers: Set = [".", ")"] 193 | } 194 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Metadata.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Metadata: Readable { 8 | var values = [String : String]() 9 | 10 | static func read(using reader: inout Reader) throws -> Metadata { 11 | try require(reader.readCount(of: "-") == 3) 12 | try reader.read("\n") 13 | 14 | var metadata = Metadata() 15 | var lastKey: String? 16 | 17 | while !reader.didReachEnd { 18 | reader.discardWhitespacesAndNewlines() 19 | 20 | guard reader.currentCharacter != "-" else { 21 | try require(reader.readCount(of: "-") == 3) 22 | return metadata 23 | } 24 | 25 | let key = try trim(reader.read(until: ":", required: false)) 26 | 27 | guard reader.previousCharacter == ":" else { 28 | if let lastKey = lastKey { 29 | metadata.values[lastKey]?.append(" " + key) 30 | } 31 | 32 | continue 33 | } 34 | 35 | let value = trim(reader.readUntilEndOfLine()) 36 | 37 | if !value.isEmpty { 38 | metadata.values[key] = value 39 | lastKey = key 40 | } 41 | } 42 | 43 | throw Reader.Error() 44 | } 45 | 46 | func applyingModifiers(_ modifiers: ModifierCollection) -> Self { 47 | var modified = self 48 | 49 | modifiers.applyModifiers(for: .metadataKeys) { modifier in 50 | for (key, value) in modified.values { 51 | let newKey = modifier.closure((key, Substring(key))) 52 | modified.values[key] = nil 53 | modified.values[newKey] = value 54 | } 55 | } 56 | 57 | modifiers.applyModifiers(for: .metadataValues) { modifier in 58 | modified.values = modified.values.mapValues { value in 59 | modifier.closure((value, Substring(value))) 60 | } 61 | } 62 | 63 | return modified 64 | } 65 | } 66 | 67 | private extension Metadata { 68 | static func trim(_ string: Substring) -> String { 69 | String(string 70 | .trimmingLeadingWhitespaces() 71 | .trimmingTrailingWhitespaces() 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Modifiable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal protocol Modifiable { 8 | var modifierTarget: Modifier.Target { get } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/ModifierCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct ModifierCollection { 8 | private var modifiers: [Modifier.Target : [Modifier]] 9 | 10 | init(modifiers: [Modifier]) { 11 | self.modifiers = Dictionary(grouping: modifiers, by: { $0.target }) 12 | } 13 | 14 | func applyModifiers(for target: Modifier.Target, 15 | using closure: (Modifier) -> Void) { 16 | modifiers[target]?.forEach(closure) 17 | } 18 | 19 | mutating func insert(_ modifier: Modifier) { 20 | modifiers[modifier.target, default: []].append(modifier) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/NamedURLCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct NamedURLCollection { 8 | private let urlsByName: [String : URL] 9 | 10 | init(urlsByName: [String : URL]) { 11 | self.urlsByName = urlsByName 12 | } 13 | 14 | func url(named name: Substring) -> URL? { 15 | urlsByName[name.lowercased()] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Paragraph.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Paragraph: Fragment { 8 | var modifierTarget: Modifier.Target { .paragraphs } 9 | 10 | private var text: FormattedText 11 | 12 | static func read(using reader: inout Reader) -> Paragraph { 13 | return Paragraph(text: .read(using: &reader)) 14 | } 15 | 16 | func html(usingURLs urls: NamedURLCollection, 17 | modifiers: ModifierCollection) -> String { 18 | let body = text.html(usingURLs: urls, modifiers: modifiers) 19 | return "

    \(body)

    " 20 | } 21 | 22 | func plainText() -> String { 23 | text.plainText() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/PlainTextConvertible.swift: -------------------------------------------------------------------------------- 1 | internal protocol PlainTextConvertible { 2 | func plainText() -> String 3 | } 4 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Readable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal protocol Readable { 8 | static func read(using reader: inout Reader) throws -> Self 9 | } 10 | 11 | extension Readable { 12 | static func readOrRewind(using reader: inout Reader) throws -> Self { 13 | guard reader.previousCharacter != "\\" else { 14 | throw Reader.Error() 15 | } 16 | 17 | let previousReader = reader 18 | 19 | do { 20 | return try read(using: &reader) 21 | } catch { 22 | reader = previousReader 23 | throw error 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Reader.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct Reader { 8 | private let string: String 9 | private(set) var currentIndex: String.Index 10 | 11 | init(string: String) { 12 | self.string = string 13 | self.currentIndex = string.startIndex 14 | } 15 | } 16 | 17 | extension Reader { 18 | struct Error: Swift.Error {} 19 | 20 | var didReachEnd: Bool { currentIndex == endIndex } 21 | var previousCharacter: Character? { lookBehindAtPreviousCharacter() } 22 | var currentCharacter: Character { string[currentIndex] } 23 | var nextCharacter: Character? { lookAheadAtNextCharacter() } 24 | var endIndex: String.Index { string.endIndex } 25 | 26 | func characters(in range: Range) -> Substring { 27 | return string[range] 28 | } 29 | 30 | mutating func read(_ character: Character) throws { 31 | guard !didReachEnd else { throw Error() } 32 | guard currentCharacter == character else { throw Error() } 33 | advanceIndex() 34 | } 35 | 36 | @discardableResult 37 | mutating func read(until character: Character, 38 | required: Bool = true, 39 | allowWhitespace: Bool = true, 40 | allowLineBreaks: Bool = false, 41 | balanceAgainst balancingCharacter: Character? = nil) throws -> Substring { 42 | let startIndex = currentIndex 43 | var characterBalance = 0 44 | 45 | while !didReachEnd { 46 | guard currentCharacter != character || characterBalance > 0 else { 47 | let result = string[startIndex.. Int { 78 | var count = 0 79 | 80 | while !didReachEnd { 81 | guard currentCharacter == character else { break } 82 | count += 1 83 | advanceIndex() 84 | } 85 | 86 | return count 87 | } 88 | 89 | /// Read characters that match by evaluating a keypath 90 | /// 91 | /// - Parameters: 92 | /// - keyPath: A keypath to evaluate that is `true` for target characters. 93 | /// - maxCount: The maximum number of characters to attempt to read. 94 | /// - Returns: The substring of characters successfully read 95 | /// - Complexity: O(*n*), where *n* is the length of the string being read. 96 | @discardableResult 97 | mutating func readCharacters(matching keyPath: KeyPath, 98 | max maxCount: Int = Int.max) throws -> Substring { 99 | let startIndex = currentIndex 100 | var count = 0 101 | 102 | while !didReachEnd 103 | && count < maxCount 104 | && currentCharacter[keyPath: keyPath] { 105 | advanceIndex() 106 | count += 1 107 | } 108 | 109 | guard startIndex != currentIndex else { 110 | throw Error() 111 | } 112 | 113 | return string[startIndex..) throws -> Character { 124 | guard !didReachEnd else { throw Error() } 125 | guard currentCharacter.isAny(of: set) else { throw Error() } 126 | defer { advanceIndex() } 127 | 128 | return currentCharacter 129 | } 130 | 131 | @discardableResult 132 | mutating func readWhitespaces() throws -> Substring { 133 | try readCharacters(matching: \.isSameLineWhitespace) 134 | } 135 | 136 | mutating func readUntilEndOfLine() -> Substring { 137 | let startIndex = currentIndex 138 | 139 | while !didReachEnd { 140 | guard !currentCharacter.isNewline else { 141 | let text = string[startIndex.. Character? { 181 | guard currentIndex != string.startIndex else { return nil } 182 | let previousIndex = string.index(before: currentIndex) 183 | return string[previousIndex] 184 | } 185 | 186 | func lookAheadAtNextCharacter() -> Character? { 187 | guard !didReachEnd else { return nil } 188 | let nextIndex = string.index(after: currentIndex) 189 | guard nextIndex != string.endIndex else { return nil } 190 | return string[nextIndex] 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Require.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | func require(_ bool: Bool) throws { 8 | struct RequireError: Error {} 9 | guard bool else { throw RequireError() } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Substring+Trimming.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal extension Substring { 8 | func trimmingLeadingWhitespaces() -> Self { 9 | drop(while: { $0.isWhitespace }) 10 | } 11 | 12 | func trimmingTrailingWhitespaces() -> Self { 13 | var trimmed = self 14 | 15 | while trimmed.last?.isWhitespace == true { 16 | trimmed = trimmed.dropLast() 17 | } 18 | 19 | return trimmed 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/Table.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2020 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | struct Table: Fragment { 10 | var modifierTarget: Modifier.Target { .tables } 11 | 12 | private var header: Row? 13 | private var rows = [Row]() 14 | private var columnCount = 0 15 | private var columnAlignments = [ColumnAlignment]() 16 | 17 | static func read(using reader: inout Reader) throws -> Table { 18 | var table = Table() 19 | 20 | while !reader.didReachEnd, !reader.currentCharacter.isNewline { 21 | guard reader.currentCharacter == "|" else { 22 | break 23 | } 24 | 25 | let row = try reader.readTableRow() 26 | table.rows.append(row) 27 | table.columnCount = max(table.columnCount, row.count) 28 | } 29 | 30 | guard !table.rows.isEmpty else { throw Reader.Error() } 31 | table.formHeaderAndColumnAlignmentsIfNeeded() 32 | return table 33 | } 34 | 35 | func html(usingURLs urls: NamedURLCollection, 36 | modifiers: ModifierCollection) -> String { 37 | var html = "" 38 | let render: () -> String = { "\(html)
    " } 39 | 40 | if let header = header { 41 | let rowHTML = self.html( 42 | forRow: header, 43 | cellElementName: "th", 44 | urls: urls, 45 | modifiers: modifiers 46 | ) 47 | 48 | html.append("\(rowHTML)") 49 | } 50 | 51 | guard !rows.isEmpty else { 52 | return render() 53 | } 54 | 55 | html.append("") 56 | 57 | for row in rows { 58 | let rowHTML = self.html( 59 | forRow: row, 60 | cellElementName: "td", 61 | urls: urls, 62 | modifiers: modifiers 63 | ) 64 | 65 | html.append(rowHTML) 66 | } 67 | 68 | html.append("") 69 | return render() 70 | } 71 | 72 | func plainText() -> String { 73 | var text = header.map(plainText) ?? "" 74 | 75 | for row in rows { 76 | if !text.isEmpty { text.append("\n") } 77 | text.append(plainText(forRow: row)) 78 | } 79 | 80 | return text 81 | } 82 | } 83 | 84 | private extension Table { 85 | typealias Row = [FormattedText] 86 | typealias Cell = FormattedText 87 | 88 | static let delimiters: Set = ["|", "\n"] 89 | static let allowedHeaderCharacters: Set = ["-", ":"] 90 | 91 | enum ColumnAlignment { 92 | case none 93 | case left 94 | case center 95 | case right 96 | 97 | var attribute: String { 98 | switch self { 99 | case .none: 100 | return "" 101 | case .left: 102 | return #" align="left""# 103 | case .center: 104 | return #" align="center""# 105 | case .right: 106 | return #" align="right""# 107 | } 108 | } 109 | } 110 | 111 | mutating func formHeaderAndColumnAlignmentsIfNeeded() { 112 | guard rows.count > 1 else { return } 113 | guard rows[0].count == rows[1].count else { return } 114 | 115 | let textPredicate = Self.allowedHeaderCharacters.contains 116 | var alignments = [ColumnAlignment]() 117 | 118 | for cell in rows[1] { 119 | let text = cell.plainText() 120 | 121 | guard text.allSatisfy(textPredicate) else { 122 | return 123 | } 124 | 125 | alignments.append(parseColumnAlignment(from: text)) 126 | } 127 | 128 | header = rows[0] 129 | columnAlignments = alignments 130 | rows.removeSubrange(0...1) 131 | } 132 | 133 | func parseColumnAlignment(from text: String) -> ColumnAlignment { 134 | switch (text.first, text.last) { 135 | case (":", ":"): 136 | return .center 137 | case (":", _): 138 | return .left 139 | case (_, ":"): 140 | return .right 141 | default: 142 | return .none 143 | } 144 | } 145 | 146 | func html(forRow row: Row, 147 | cellElementName: String, 148 | urls: NamedURLCollection, 149 | modifiers: ModifierCollection) -> String { 150 | var html = "" 151 | 152 | for index in 0.." 164 | } 165 | 166 | func htmlForCell(at index: Int, contents: String, elementName: String) -> String { 167 | let alignment = index < columnAlignments.count 168 | ? columnAlignments[index] 169 | : .none 170 | 171 | let tags = ( 172 | opening: "<\(elementName)\(alignment.attribute)>", 173 | closing: "" 174 | ) 175 | 176 | return tags.opening + contents + tags.closing 177 | } 178 | 179 | func plainText(forRow row: Row) -> String { 180 | var text = "" 181 | 182 | for index in 0.. 0 { text.append(" | ") } 185 | text.append(cell?.plainText() ?? "") 186 | } 187 | 188 | return text + " |" 189 | } 190 | } 191 | 192 | private extension Reader { 193 | mutating func readTableRow() throws -> Table.Row { 194 | try readTableDelimiter() 195 | var row = Table.Row() 196 | 197 | while !didReachEnd { 198 | let cell = FormattedText.read( 199 | using: &self, 200 | terminators: Table.delimiters 201 | ) 202 | 203 | try readTableDelimiter() 204 | row.append(cell) 205 | 206 | if !didReachEnd, currentCharacter.isNewline { 207 | advanceIndex() 208 | break 209 | } 210 | } 211 | 212 | return row 213 | } 214 | 215 | mutating func readTableDelimiter() throws { 216 | try read("|") 217 | discardWhitespaces() 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/TextStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal enum TextStyle { 8 | case italic 9 | case bold 10 | case strikethrough 11 | } 12 | 13 | extension TextStyle { 14 | var htmlTagName: String { 15 | switch self { 16 | case .italic: return "em" 17 | case .bold: return "strong" 18 | case .strikethrough: return "s" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/TextStyleMarker.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal final class TextStyleMarker: Readable, HTMLConvertible { 8 | var style: TextStyle 9 | var rawMarkers: String 10 | let characterRange: Range 11 | var kind: Kind = .opening 12 | var isValid = true 13 | var prefix: Character? 14 | var suffix: Character? 15 | 16 | private init(style: TextStyle, rawMarkers: String, characterRange: Range) { 17 | self.style = style 18 | self.rawMarkers = rawMarkers 19 | self.characterRange = characterRange 20 | } 21 | 22 | static func read(using reader: inout Reader) throws -> Self { 23 | let startIndex = reader.currentIndex 24 | 25 | if reader.currentCharacter.isAny(of: .boldItalicStyleMarkers) { 26 | let firstMarker = reader.currentCharacter 27 | reader.advanceIndex() 28 | 29 | if !reader.didReachEnd, reader.currentCharacter.isAny(of: .boldItalicStyleMarkers) { 30 | let secondMarker = reader.currentCharacter 31 | let markers = String([firstMarker, secondMarker]) 32 | reader.advanceIndex() 33 | 34 | return Self( 35 | style: .bold, 36 | rawMarkers: markers, 37 | characterRange: startIndex.. String { 60 | guard isValid else { return rawMarkers } 61 | 62 | let leadingTag: String 63 | 64 | switch kind { 65 | case .opening: leadingTag = "<" 66 | case .closing: leadingTag = "" + suffix 72 | } 73 | } 74 | 75 | extension TextStyleMarker { 76 | enum Kind { 77 | case opening 78 | case closing 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/URL.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal typealias URL = Substring 8 | -------------------------------------------------------------------------------- /Sources/Ink/Internal/URLDeclaration.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | internal struct URLDeclaration: Readable { 8 | var name: String 9 | var url: URL 10 | 11 | static func read(using reader: inout Reader) throws -> Self { 12 | try reader.read("[") 13 | let name = try reader.read(until: "]") 14 | try reader.read(":") 15 | try reader.readWhitespaces() 16 | let url = reader.readUntilEndOfLine() 17 | 18 | return URLDeclaration(name: name.lowercased(), url: url) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/InkCLI/Printing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | 9 | internal func printError(_ error: CustomStringConvertible) { 10 | fputs("\(error)\n", stderr) 11 | } 12 | 13 | internal func printUsageMessage() { 14 | printError(usageMessage) 15 | } 16 | 17 | private let usageMessage = """ 18 | Usage: ink [file | -m markdown] 19 | Options: 20 | --markdown, -m Parse a markdown string directly 21 | --help, -h Print usage information 22 | """ 23 | 24 | internal let helpMessage = """ 25 | Ink: Markdown -> HTML converter 26 | ------------------------------- 27 | \(usageMessage) 28 | 29 | Ink takes Markdown formatted text as input, 30 | and returns HTML as output. If called without 31 | arguments, it will read from STDIN. If called 32 | with a single argument, the file at the 33 | specified path will be used as input. If 34 | called with the -m option, the following 35 | argument will be parsed as a Markdown string. 36 | """ 37 | -------------------------------------------------------------------------------- /Sources/InkCLI/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import Foundation 8 | import Ink 9 | 10 | let arguments = CommandLine.arguments 11 | 12 | if arguments.contains(where: { $0 == "-h" || $0 == "--help" }) { 13 | print(helpMessage) 14 | exit(0) 15 | } 16 | 17 | let markdown: String 18 | 19 | switch arguments.count { 20 | case 1: 21 | // No arguments, parse stdin 22 | markdown = AnyIterator { readLine() }.joined(separator: "\n") 23 | case let count where arguments[1] == "-m" || arguments[1] == "--markdown": 24 | // First argument is -m or --markdown, parse Markdown string 25 | guard count == 3 else { 26 | printError("-m, --markdown flag takes a single following argument") 27 | printUsageMessage() 28 | exit(1) 29 | } 30 | markdown = arguments[2] 31 | case 2: 32 | // Single argument, parse contents of file 33 | let fileURL: URL 34 | 35 | switch arguments[1] { 36 | case let argument where argument.hasPrefix("/"): 37 | fileURL = URL(fileURLWithPath: argument, isDirectory: false) 38 | case let argument where argument.hasPrefix("~"): 39 | let absoluteString = NSString(string: argument).expandingTildeInPath 40 | fileURL = URL(fileURLWithPath: absoluteString, isDirectory: false) 41 | default: 42 | let directory = FileManager.default.currentDirectoryPath 43 | let directoryURL = URL(fileURLWithPath: directory, isDirectory: true) 44 | fileURL = directoryURL.appendingPathComponent(arguments[1]) 45 | } 46 | 47 | do { 48 | let data = try Data(contentsOf: fileURL) 49 | markdown = String(decoding: data, as: UTF8.self) 50 | } catch { 51 | printError(error.localizedDescription) 52 | printUsageMessage() 53 | exit(1) 54 | } 55 | default: 56 | printError("Too many arguments") 57 | printUsageMessage() 58 | exit(1) 59 | } 60 | 61 | let parser = MarkdownParser() 62 | print(parser.html(from: markdown)) 63 | -------------------------------------------------------------------------------- /Tests/InkTests/CodeTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class CodeTests: XCTestCase { 11 | func testInlineCode() { 12 | let html = MarkdownParser().html(from: "Hello `inline.code()`") 13 | XCTAssertEqual(html, "

    Hello inline.code()

    ") 14 | } 15 | 16 | func testCodeBlockWithJustBackticks() { 17 | let html = MarkdownParser().html(from: """ 18 | ``` 19 | code() 20 | block() 21 | ``` 22 | """) 23 | 24 | XCTAssertEqual(html, "
    code()\nblock()\n
    ") 25 | } 26 | 27 | func testCodeBlockWithBackticksAndLabel() { 28 | let html = MarkdownParser().html(from: """ 29 | ```swift 30 | code() 31 | ``` 32 | """) 33 | 34 | XCTAssertEqual(html, "
    code()\n
    ") 35 | } 36 | 37 | func testCodeBlockWithBackticksAndLabelNeedingTrimming() { 38 | // there are 2 spaces after the swift label that need trimming too 39 | let html = MarkdownParser().html(from: """ 40 | ``` swift 41 | code() 42 | ``` 43 | """) 44 | 45 | XCTAssertEqual(html, "
    code()\n
    ") 46 | } 47 | 48 | func testCodeBlockManyBackticks() { 49 | // there are 2 spaces after the swift label that need trimming too 50 | let html = MarkdownParser().html(from: """ 51 | 52 | ```````````````````````````````` foo 53 | bar 54 | ```````````````````````````````` 55 | """) 56 | 57 | XCTAssertEqual(html, "
    bar\n
    ") 58 | } 59 | 60 | func testEncodingSpecialCharactersWithinCodeBlock() { 61 | let html = MarkdownParser().html(from: """ 62 | ```swift 63 | Generic() && expression() 64 | ``` 65 | """) 66 | 67 | XCTAssertEqual(html, """ 68 |
    Generic<T>() && expression()\n
    69 | """) 70 | } 71 | 72 | func testIgnoringFormattingWithinCodeBlock() { 73 | let html = MarkdownParser().html(from: """ 74 | ``` 75 | # Not A Header 76 | return View() 77 | - Not a list 78 | ``` 79 | """) 80 | 81 | XCTAssertEqual(html, """ 82 |
    # Not A Header
     83 |         return View()
     84 |         - Not a list\n
    85 | """) 86 | } 87 | } 88 | 89 | extension CodeTests { 90 | static var allTests: Linux.TestList { 91 | return [ 92 | ("testInlineCode", testInlineCode), 93 | ("testCodeBlockWithJustBackticks", testCodeBlockWithJustBackticks), 94 | ("testCodeBlockWithBackticksAndLabel", testCodeBlockWithBackticksAndLabel), 95 | ("testCodeBlockWithBackticksAndLabelNeedingTrimming", testCodeBlockWithBackticksAndLabelNeedingTrimming), 96 | ("testCodeBlockManyBackticks", testCodeBlockManyBackticks), 97 | ("testEncodingSpecialCharactersWithinCodeBlock", testEncodingSpecialCharactersWithinCodeBlock), 98 | ("testIgnoringFormattingWithinCodeBlock", testIgnoringFormattingWithinCodeBlock) 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/InkTests/HTMLTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class HTMLTests: XCTestCase { 11 | func testTopLevelHTML() { 12 | let html = MarkdownParser().html(from: """ 13 | Hello 14 | 15 |
    16 | Whole wide 17 |
    18 | 19 | World 20 | """) 21 | 22 | XCTAssertEqual(html, """ 23 |

    Hello

    24 | Whole wide 25 |

    World

    26 | """) 27 | } 28 | 29 | func testNestedTopLevelHTML() { 30 | let html = MarkdownParser().html(from: """ 31 |
    32 |
    Hello
    33 |
    World
    34 |
    35 | """) 36 | 37 | XCTAssertEqual(html, """ 38 |
    39 |
    Hello
    40 |
    World
    41 |
    42 | """) 43 | } 44 | 45 | func testTopLevelHTMLWithPreviousNewline() { 46 | let html = MarkdownParser().html(from: "Text\n

    Heading

    ") 47 | XCTAssertEqual(html, "

    Text

    Heading

    ") 48 | } 49 | 50 | func testIgnoringFormattingWithinTopLevelHTML() { 51 | let html = MarkdownParser().html(from: "
    _Hello_
    ") 52 | XCTAssertEqual(html, "
    _Hello_
    ") 53 | } 54 | 55 | func testIgnoringTextFormattingWithinInlineHTML() { 56 | let html = MarkdownParser().html(from: "Hello _World_") 57 | XCTAssertEqual(html, "

    Hello _World_

    ") 58 | } 59 | 60 | func testIgnoringListsWithinInlineHTML() { 61 | let html = MarkdownParser().html(from: "

    1. Hello

    - World

    ") 62 | XCTAssertEqual(html, "

    1. Hello

    - World

    ") 63 | } 64 | 65 | func testInlineParagraphTagEndingCurrentParagraph() { 66 | let html = MarkdownParser().html(from: "One

    Two

    Three") 67 | XCTAssertEqual(html, "

    One

    Two

    Three

    ") 68 | } 69 | 70 | func testTopLevelSelfClosingHTMLElement() { 71 | let html = MarkdownParser().html(from: """ 72 | Hello 73 | 74 | 75 | 76 | World 77 | """) 78 | 79 | XCTAssertEqual(html, #"

    Hello

    World

    "#) 80 | } 81 | 82 | func testInlineSelfClosingHTMLElement() { 83 | let html = MarkdownParser().html(from: #"Hello World"#) 84 | XCTAssertEqual(html, #"

    Hello World

    "#) 85 | } 86 | 87 | func testTopLevelHTMLLineBreak() { 88 | let html = MarkdownParser().html(from: """ 89 | Hello 90 |
    91 | World 92 | """) 93 | 94 | XCTAssertEqual(html, "

    Hello


    World

    ") 95 | } 96 | 97 | func testHTMLComment() { 98 | let html = MarkdownParser().html(from: """ 99 | Hello 100 | 101 | World 102 | """) 103 | 104 | XCTAssertEqual(html, "

    Hello

    World

    ") 105 | } 106 | 107 | func testHTMLEntities() { 108 | let html = MarkdownParser().html(from: """ 109 | Hello & welcome to <Ink> 110 | """) 111 | 112 | XCTAssertEqual(html, "

    Hello & welcome to <Ink>

    ") 113 | } 114 | } 115 | 116 | extension HTMLTests { 117 | static var allTests: Linux.TestList { 118 | return [ 119 | ("testTopLevelHTML", testTopLevelHTML), 120 | ("testNestedTopLevelHTML", testNestedTopLevelHTML), 121 | ("testTopLevelHTMLWithPreviousNewline", testTopLevelHTMLWithPreviousNewline), 122 | ("testIgnoringFormattingWithinTopLevelHTML", testIgnoringFormattingWithinTopLevelHTML), 123 | ("testIgnoringTextFormattingWithinInlineHTML", testIgnoringTextFormattingWithinInlineHTML), 124 | ("testIgnoringListsWithinInlineHTML", testIgnoringListsWithinInlineHTML), 125 | ("testInlineParagraphTagEndingCurrentParagraph", testInlineParagraphTagEndingCurrentParagraph), 126 | ("testTopLevelSelfClosingHTMLElement", testTopLevelSelfClosingHTMLElement), 127 | ("testInlineSelfClosingHTMLElement", testInlineSelfClosingHTMLElement), 128 | ("testTopLevelHTMLLineBreak", testTopLevelHTMLLineBreak), 129 | ("testHTMLComment", testHTMLComment), 130 | ("testHTMLEntities", testHTMLEntities) 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/InkTests/HeadingTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class HeadingTests: XCTestCase { 11 | func testHeading() { 12 | let html = MarkdownParser().html(from: "# Hello, world!") 13 | XCTAssertEqual(html, "

    Hello, world!

    ") 14 | } 15 | 16 | func testHeadingsSeparatedBySingleNewline() { 17 | let html = MarkdownParser().html(from: "# Hello\n## World") 18 | XCTAssertEqual(html, "

    Hello

    World

    ") 19 | } 20 | 21 | func testHeadingsWithLeadingNumbers() { 22 | let html = MarkdownParser().html(from: """ 23 | # 1. First 24 | ## 2. Second 25 | ## 3. Third 26 | ### 4. Forth 27 | """) 28 | 29 | XCTAssertEqual(html, """ 30 |

    1. First

    2. Second

    3. Third

    4. Forth

    31 | """) 32 | } 33 | 34 | func testHeadingWithPreviousWhitespace() { 35 | let html = MarkdownParser().html(from: "Text \n## Heading") 36 | XCTAssertEqual(html, "

    Text

    Heading

    ") 37 | } 38 | 39 | func testHeadingWithPreviousNewlineAndWhitespace() { 40 | let html = MarkdownParser().html(from: "Hello\n \n## Heading\n\nWorld") 41 | XCTAssertEqual(html, "

    Hello

    Heading

    World

    ") 42 | } 43 | 44 | func testInvalidHeaderLevel() { 45 | let markdown = String(repeating: "#", count: 7) 46 | let html = MarkdownParser().html(from: markdown) 47 | XCTAssertEqual(html, "

    \(markdown)

    ") 48 | } 49 | 50 | func testRemovingTrailingMarkersFromHeading() { 51 | let markdown = "# Heading #######" 52 | let html = MarkdownParser().html(from: markdown) 53 | XCTAssertEqual(html, "

    Heading

    ") 54 | } 55 | 56 | func testHeadingWithOnlyTrailingMarkers() { 57 | let markdown = "# #######" 58 | let html = MarkdownParser().html(from: markdown) 59 | XCTAssertEqual(html, "

    ") 60 | } 61 | } 62 | 63 | extension HeadingTests { 64 | static var allTests: Linux.TestList { 65 | return [ 66 | ("testHeading", testHeading), 67 | ("testHeadingsSeparatedBySingleNewline", testHeadingsSeparatedBySingleNewline), 68 | ("testHeadingsWithLeadingNumbers", testHeadingsWithLeadingNumbers), 69 | ("testHeadingWithPreviousWhitespace", testHeadingWithPreviousWhitespace), 70 | ("testHeadingWithPreviousNewlineAndWhitespace", testHeadingWithPreviousNewlineAndWhitespace), 71 | ("testInvalidHeaderLevel", testInvalidHeaderLevel), 72 | ("testRemovingTrailingMarkersFromHeading", testRemovingTrailingMarkersFromHeading), 73 | ("testHeadingWithOnlyTrailingMarkers", testHeadingWithOnlyTrailingMarkers) 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/InkTests/HorizontalLineTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class HorizontalLineTests: XCTestCase { 11 | func testHorizonalLineWithDashes() { 12 | let html = MarkdownParser().html(from: """ 13 | Hello 14 | 15 | --- 16 | 17 | World 18 | """) 19 | 20 | XCTAssertEqual(html, "

    Hello


    World

    ") 21 | } 22 | 23 | func testHorizontalLineWithDashesAtTheStartOfString() { 24 | let html = MarkdownParser().html(from: "---\nHello") 25 | XCTAssertEqual(html, "

    Hello

    ") 26 | } 27 | 28 | func testHorizontalLineWithAsterisks() { 29 | let html = MarkdownParser().html(from: """ 30 | Hello 31 | 32 | *** 33 | 34 | World 35 | """) 36 | 37 | XCTAssertEqual(html, "

    Hello


    World

    ") 38 | } 39 | } 40 | 41 | extension HorizontalLineTests { 42 | static var allTests: Linux.TestList { 43 | return [ 44 | ("testHorizonalLineWithDashes", testHorizonalLineWithDashes), 45 | ("testHorizontalLineWithDashesAtTheStartOfString", testHorizontalLineWithDashesAtTheStartOfString), 46 | ("testHorizontalLineWithAsterisks", testHorizontalLineWithAsterisks) 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/InkTests/ImageTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class ImageTests: XCTestCase { 11 | func testImageWithURL() { 12 | let html = MarkdownParser().html(from: "![](url)") 13 | XCTAssertEqual(html, #""#) 14 | } 15 | 16 | func testImageWithReference() { 17 | let html = MarkdownParser().html(from: """ 18 | ![][url] 19 | [url]: https://swiftbysundell.com 20 | """) 21 | 22 | XCTAssertEqual(html, #""#) 23 | } 24 | 25 | func testImageWithURLAndAltText() { 26 | let html = MarkdownParser().html(from: "![Alt text](url)") 27 | XCTAssertEqual(html, #"Alt text"#) 28 | } 29 | 30 | func testImageWithReferenceAndAltText() { 31 | let html = MarkdownParser().html(from: """ 32 | ![Alt text][url] 33 | [url]: swiftbysundell.com 34 | """) 35 | 36 | XCTAssertEqual(html, #"Alt text"#) 37 | } 38 | 39 | func testImageWithinParagraph() { 40 | let html = MarkdownParser().html(from: "Text ![](url) text") 41 | XCTAssertEqual(html, #"

    Text text

    "#) 42 | } 43 | } 44 | 45 | extension ImageTests { 46 | static var allTests: Linux.TestList { 47 | return [ 48 | ("testImageWithURL", testImageWithURL), 49 | ("testImageWithReference", testImageWithReference), 50 | ("testImageWithURLAndAltText", testImageWithURLAndAltText), 51 | ("testImageWithReferenceAndAltText", testImageWithReferenceAndAltText), 52 | ("testImageWithinParagraph", testImageWithinParagraph) 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/InkTests/LinkTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class LinkTests: XCTestCase { 11 | func testLinkWithURL() { 12 | let html = MarkdownParser().html(from: "[Title](url)") 13 | XCTAssertEqual(html, #"

    Title

    "#) 14 | } 15 | 16 | func testLinkWithReference() { 17 | let html = MarkdownParser().html(from: """ 18 | [Title][url] 19 | 20 | [url]: swiftbysundell.com 21 | """) 22 | 23 | XCTAssertEqual(html, #"

    Title

    "#) 24 | } 25 | 26 | func testCaseMismatchedLinkWithReference() { 27 | let html = MarkdownParser().html(from: """ 28 | [Title][Foo] 29 | [Title][αγω] 30 | 31 | [FOO]: /url 32 | [ΑΓΩ]: /φου 33 | """) 34 | 35 | XCTAssertEqual(html, #"

    Title Title

    "#) 36 | } 37 | 38 | func testNumericLinkWithReference() { 39 | let html = MarkdownParser().html(from: """ 40 | [1][1] 41 | 42 | [1]: swiftbysundell.com 43 | """) 44 | 45 | XCTAssertEqual(html, #"

    1

    "#) 46 | } 47 | 48 | func testBoldLinkWithInternalMarkers() { 49 | let html = MarkdownParser().html(from: "[**Hello**](/hello)") 50 | XCTAssertEqual(html, #"

    Hello

    "#) 51 | } 52 | 53 | func testBoldLinkWithExternalMarkers() { 54 | let html = MarkdownParser().html(from: "**[Hello](/hello)**") 55 | XCTAssertEqual(html, #"

    Hello

    "#) 56 | } 57 | 58 | func testLinkWithUnderscores() { 59 | let html = MarkdownParser().html(from: "[He_llo](/he_llo)") 60 | XCTAssertEqual(html, "

    He_llo

    ") 61 | } 62 | 63 | func testLinkWithParenthesis() { 64 | let html = MarkdownParser().html(from: "[Hello](/(hello))") 65 | XCTAssertEqual(html, "

    Hello

    ") 66 | } 67 | 68 | func testLinkWithNestedParenthesis() { 69 | let html = MarkdownParser().html(from: "[Hello](/(h(e(l(l(o()))))))") 70 | XCTAssertEqual(html, "

    Hello

    ") 71 | } 72 | 73 | func testLinkWithParenthesisAndClosingParenthesisInContent() { 74 | let html = MarkdownParser().html(from: "[Hello](/(hello)))") 75 | XCTAssertEqual(html, "

    Hello)

    ") 76 | } 77 | 78 | func testUnterminatedLink() { 79 | let html = MarkdownParser().html(from: "[Hello]") 80 | XCTAssertEqual(html, "

    [Hello]

    ") 81 | } 82 | 83 | func testLinkWithEscapedSquareBrackets() { 84 | let html = MarkdownParser().html(from: "[\\[Hello\\]](hello)") 85 | XCTAssertEqual(html, #"

    [Hello]

    "#) 86 | } 87 | } 88 | 89 | extension LinkTests { 90 | static var allTests: Linux.TestList { 91 | return [ 92 | ("testLinkWithURL", testLinkWithURL), 93 | ("testLinkWithReference", testLinkWithReference), 94 | ("testCaseMismatchedLinkWithReference", testCaseMismatchedLinkWithReference), 95 | ("testNumericLinkWithReference", testNumericLinkWithReference), 96 | ("testBoldLinkWithInternalMarkers", testBoldLinkWithInternalMarkers), 97 | ("testBoldLinkWithExternalMarkers", testBoldLinkWithExternalMarkers), 98 | ("testLinkWithUnderscores", testLinkWithUnderscores), 99 | ("testLinkWithParenthesis", testLinkWithParenthesis), 100 | ("testLinkWithNestedParenthesis", testLinkWithNestedParenthesis), 101 | ("testLinkWithParenthesisAndClosingParenthesisInContent", testLinkWithParenthesisAndClosingParenthesisInContent), 102 | ("testUnterminatedLink", testUnterminatedLink), 103 | ("testLinkWithEscapedSquareBrackets", testLinkWithEscapedSquareBrackets) 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/InkTests/LinuxCompatibility.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | 9 | public enum Linux {} 10 | 11 | public extension Linux { 12 | typealias TestCase = (testCaseClass: XCTestCase.Type, allTests: TestManifest) 13 | typealias TestManifest = [(String, TestRunner)] 14 | typealias TestRunner = (XCTestCase) throws -> Void 15 | typealias TestList = [(String, Test)] 16 | typealias Test = (T) -> () throws -> Void 17 | } 18 | 19 | internal extension Linux { 20 | static func makeTestCase(using list: TestList) -> TestCase { 21 | let manifest: TestManifest = list.map { name, function in 22 | (name, { type in 23 | try function(type as! T)() 24 | }) 25 | } 26 | 27 | return (T.self, manifest) 28 | } 29 | } 30 | 31 | #if canImport(ObjectiveC) 32 | internal final class LinuxVerificationTests: XCTestCase { 33 | func testAllTestsRunOnLinux() { 34 | for testCase in allTests() { 35 | let type = testCase.testCaseClass 36 | 37 | let testNames: [String] = type.defaultTestSuite.tests.map { test in 38 | let components = test.name.components(separatedBy: .whitespaces) 39 | return components[1].replacingOccurrences(of: "]", with: "") 40 | } 41 | 42 | let linuxTestNames = Set(testCase.allTests.map { $0.0 }) 43 | 44 | for name in testNames { 45 | if !linuxTestNames.contains(name) { 46 | XCTFail(""" 47 | \(type).\(name) does not run on Linux. 48 | Please add it to \(type).allTests. 49 | """) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Tests/InkTests/ListTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class ListTests: XCTestCase { 11 | func testOrderedList() { 12 | let html = MarkdownParser().html(from: """ 13 | 1. One 14 | 2. Two 15 | """) 16 | 17 | XCTAssertEqual(html, #"
    1. One
    2. Two
    "#) 18 | } 19 | 20 | func test10DigitOrderedList() { 21 | let html = MarkdownParser().html(from: """ 22 | 1234567890. Not a list 23 | """) 24 | 25 | XCTAssertEqual(html, "

    1234567890. Not a list

    ") 26 | } 27 | 28 | func testOrderedListParentheses() { 29 | let html = MarkdownParser().html(from: """ 30 | 1) One 31 | 2) Two 32 | """) 33 | 34 | XCTAssertEqual(html, #"
    1. One
    2. Two
    "#) 35 | } 36 | 37 | func testOrderedListWithoutIncrementedNumbers() { 38 | let html = MarkdownParser().html(from: """ 39 | 1. One 40 | 3. Two 41 | 17. Three 42 | """) 43 | 44 | XCTAssertEqual(html, "
    1. One
    2. Two
    3. Three
    ") 45 | } 46 | 47 | func testOrderedListWithInvalidNumbers() { 48 | let html = MarkdownParser().html(from: """ 49 | 1. One 50 | 3!. Two 51 | 17. Three 52 | """) 53 | 54 | XCTAssertEqual(html, "
    1. One 3!. Two
    2. Three
    ") 55 | } 56 | 57 | func testUnorderedList() { 58 | let html = MarkdownParser().html(from: """ 59 | - One 60 | - Two 61 | - Three 62 | """) 63 | 64 | XCTAssertEqual(html, "
    • One
    • Two
    • Three
    ") 65 | } 66 | 67 | func testMixedUnorderedList() { 68 | let html = MarkdownParser().html(from: """ 69 | - One 70 | * Two 71 | * Three 72 | - Four 73 | """) 74 | 75 | XCTAssertEqual(html, "
    • One
    • Two
    • Three
    • Four
    ") 76 | } 77 | 78 | func testMixedList() { 79 | let html = MarkdownParser().html(from: """ 80 | 1. One 81 | 2. Two 82 | 3) Three 83 | * Four 84 | """) 85 | 86 | XCTAssertEqual(html, #"
    1. One
    2. Two
    1. Three
    • Four
    "#) 87 | } 88 | 89 | func testUnorderedListWithMultiLineItem() { 90 | let html = MarkdownParser().html(from: """ 91 | - One 92 | Some text 93 | - Two 94 | """) 95 | 96 | XCTAssertEqual(html, "
    • One Some text
    • Two
    ") 97 | } 98 | 99 | func testUnorderedListWithNestedList() { 100 | let html = MarkdownParser().html(from: """ 101 | - A 102 | - B 103 | - B1 104 | - B11 105 | - B2 106 | """) 107 | 108 | let expectedComponents: [String] = [ 109 | "
      ", 110 | "
    • A
    • ", 111 | "
    • B", 112 | "
        ", 113 | "
      • B1", 114 | "
          ", 115 | "
        • B11
        • ", 116 | "
        ", 117 | "
      • ", 118 | "
      • B2
      • ", 119 | "
      ", 120 | "
    • ", 121 | "
    " 122 | ] 123 | 124 | XCTAssertEqual(html, expectedComponents.joined()) 125 | } 126 | 127 | func testUnorderedListWithInvalidMarker() { 128 | let html = MarkdownParser().html(from: """ 129 | - One 130 | -Two 131 | - Three 132 | """) 133 | 134 | XCTAssertEqual(html, "
    • One -Two
    • Three
    ") 135 | } 136 | 137 | func testOrderedIndentedList() { 138 | let html = MarkdownParser().html(from: """ 139 | 1. One 140 | 2. Two 141 | """) 142 | 143 | XCTAssertEqual(html, #"
    1. One
    2. Two
    "#) 144 | } 145 | 146 | func testUnorderedIndentedList() { 147 | let html = MarkdownParser().html(from: """ 148 | - One 149 | - Two 150 | - Three 151 | """) 152 | 153 | XCTAssertEqual(html, "
    • One
    • Two
    • Three
    ") 154 | } 155 | } 156 | 157 | extension ListTests { 158 | static var allTests: Linux.TestList { 159 | return [ 160 | ("testOrderedList", testOrderedList), 161 | ("test10DigitOrderedList", test10DigitOrderedList), 162 | ("testOrderedListParentheses", testOrderedListParentheses), 163 | ("testOrderedListWithoutIncrementedNumbers", testOrderedListWithoutIncrementedNumbers), 164 | ("testOrderedListWithInvalidNumbers", testOrderedListWithInvalidNumbers), 165 | ("testUnorderedList", testUnorderedList), 166 | ("testMixedUnorderedList", testMixedUnorderedList), 167 | ("testMixedList", testMixedList), 168 | ("testUnorderedListWithMultiLineItem", testUnorderedListWithMultiLineItem), 169 | ("testUnorderedListWithNestedList", testUnorderedListWithNestedList), 170 | ("testUnorderedListWithInvalidMarker", testUnorderedListWithInvalidMarker), 171 | ("testOrderedIndentedList", testUnorderedIndentedList), 172 | ("testUnorderedIndentedList", testUnorderedIndentedList), 173 | ] 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Tests/InkTests/MarkdownTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class MarkdownTests: XCTestCase { 11 | func testParsingMetadata() { 12 | let markdown = MarkdownParser().parse(""" 13 | --- 14 | a: 1 15 | b : 2 16 | --- 17 | # Title 18 | """) 19 | 20 | XCTAssertEqual(markdown.metadata, [ 21 | "a": "1", 22 | "b": "2" 23 | ]) 24 | 25 | XCTAssertEqual(markdown.html, "

    Title

    ") 26 | } 27 | 28 | func testDiscardingEmptyMetadataValues() { 29 | let markdown = MarkdownParser().parse(""" 30 | --- 31 | a: 1 32 | b: 33 | c: 2 34 | --- 35 | # Title 36 | """) 37 | 38 | XCTAssertEqual(markdown.metadata, [ 39 | "a": "1", 40 | "c": "2" 41 | ]) 42 | 43 | XCTAssertEqual(markdown.html, "

    Title

    ") 44 | } 45 | 46 | func testMergingOrphanMetadataValueIntoPreviousOne() { 47 | let markdown = MarkdownParser().parse(""" 48 | --- 49 | a: 1 50 | b 51 | --- 52 | # Title 53 | """) 54 | 55 | XCTAssertEqual(markdown.metadata, ["a": "1 b"]) 56 | XCTAssertEqual(markdown.html, "

    Title

    ") 57 | } 58 | 59 | func testMissingMetadata() { 60 | let markdown = MarkdownParser().parse(""" 61 | --- 62 | --- 63 | # Title 64 | """) 65 | 66 | XCTAssertEqual(markdown.metadata, [:]) 67 | XCTAssertEqual(markdown.html, "

    Title

    ") 68 | } 69 | 70 | func testMetadataModifiers() { 71 | let parser = MarkdownParser(modifiers: [ 72 | Modifier(target: .metadataKeys) { key, _ in 73 | "ModifiedKey-" + key 74 | }, 75 | Modifier(target: .metadataValues) { value, _ in 76 | "ModifiedValue-" + value 77 | } 78 | ]) 79 | 80 | let markdown = parser.parse(""" 81 | --- 82 | keyA: valueA 83 | keyB: valueB 84 | --- 85 | """) 86 | 87 | XCTAssertEqual(markdown.metadata, [ 88 | "ModifiedKey-keyA" : "ModifiedValue-valueA", 89 | "ModifiedKey-keyB" : "ModifiedValue-valueB" 90 | ]) 91 | } 92 | 93 | func testPlainTextTitle() { 94 | let markdown = MarkdownParser().parse(""" 95 | # Hello, world! 96 | """) 97 | 98 | XCTAssertEqual(markdown.title, "Hello, world!") 99 | } 100 | 101 | func testRemovingTrailingMarkersFromTitle() { 102 | let markdown = MarkdownParser().parse(""" 103 | # Hello, world! #### 104 | """) 105 | 106 | XCTAssertEqual(markdown.title, "Hello, world!") 107 | } 108 | 109 | func testConvertingFormattedTitleTextToPlainText() { 110 | let markdown = MarkdownParser().parse(""" 111 | # *Italic* **Bold** [Link](url) ![Image](url) `Code` 112 | """) 113 | 114 | XCTAssertEqual(markdown.title, "Italic Bold Link Image Code") 115 | } 116 | 117 | func testTreatingFirstHeadingAsTitle() { 118 | let markdown = MarkdownParser().parse(""" 119 | # Title 1 120 | # Title 2 121 | ## Title 3 122 | """) 123 | 124 | XCTAssertEqual(markdown.title, "Title 1") 125 | } 126 | 127 | func testOverridingTitle() { 128 | var markdown = MarkdownParser().parse("# Title") 129 | markdown.title = "Title 2" 130 | XCTAssertEqual(markdown.title, "Title 2") 131 | } 132 | } 133 | 134 | extension MarkdownTests { 135 | static var allTests: Linux.TestList { 136 | return [ 137 | ("testParsingMetadata", testParsingMetadata), 138 | ("testDiscardingEmptyMetadataValues", testDiscardingEmptyMetadataValues), 139 | ("testMergingOrphanMetadataValueIntoPreviousOne", testMergingOrphanMetadataValueIntoPreviousOne), 140 | ("testMissingMetadata", testMissingMetadata), 141 | ("testMetadataModifiers", testMetadataModifiers), 142 | ("testPlainTextTitle", testPlainTextTitle), 143 | ("testRemovingTrailingMarkersFromTitle", testRemovingTrailingMarkersFromTitle), 144 | ("testConvertingFormattedTitleTextToPlainText", testConvertingFormattedTitleTextToPlainText), 145 | ("testTreatingFirstHeadingAsTitle", testTreatingFirstHeadingAsTitle), 146 | ("testOverridingTitle", testOverridingTitle) 147 | ] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Tests/InkTests/ModifierTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class ModifierTests: XCTestCase { 11 | func testModifierInput() { 12 | var allHTML = [String]() 13 | var allMarkdown = [Substring]() 14 | 15 | let parser = MarkdownParser(modifiers: [ 16 | Modifier(target: .paragraphs) { html, markdown in 17 | allHTML.append(html) 18 | allMarkdown.append(markdown) 19 | return html 20 | } 21 | ]) 22 | 23 | let html = parser.html(from: "One\n\nTwo\n\nThree") 24 | XCTAssertEqual(html, "

    One

    Two

    Three

    ") 25 | XCTAssertEqual(allHTML, ["

    One

    ", "

    Two

    ", "

    Three

    "]) 26 | XCTAssertEqual(allMarkdown, ["One", "Two", "Three"]) 27 | } 28 | 29 | func testInitializingParserWithModifiers() { 30 | let parser = MarkdownParser(modifiers: [ 31 | Modifier(target: .links) { "LINK:" + $0.html }, 32 | Modifier(target: .inlineCode) { _ in "Replacement" } 33 | ]) 34 | 35 | let html = parser.html(from: "Text [Link](url) `code`") 36 | 37 | XCTAssertEqual( 38 | html, 39 | #"

    Text LINK:Link Replacement

    "# 40 | ) 41 | } 42 | 43 | func testAddingModifiers() { 44 | var parser = MarkdownParser() 45 | parser.addModifier(Modifier(target: .headings) { _ in "

    New heading

    " }) 46 | parser.addModifier(Modifier(target: .links) { "LINK:" + $0.html }) 47 | parser.addModifier(Modifier(target: .inlineCode) { _ in "Code" }) 48 | 49 | let html = parser.html(from: """ 50 | # Heading 51 | 52 | Text [Link](url) `code` 53 | """) 54 | 55 | XCTAssertEqual(html, #""" 56 |

    New heading

    Text LINK:Link Code

    57 | """#) 58 | } 59 | 60 | func testMultipleModifiersForSameTarget() { 61 | var parser = MarkdownParser() 62 | 63 | parser.addModifier(Modifier(target: .codeBlocks) { 64 | " is cool:

    " + $0.html 65 | }) 66 | 67 | parser.addModifier(Modifier(target: .codeBlocks) { 68 | "

    Code" + $0.html 69 | }) 70 | 71 | let html = parser.html(from: """ 72 | ``` 73 | Code 74 | ``` 75 | """) 76 | 77 | XCTAssertEqual(html, "

    Code is cool:

    Code\n
    ") 78 | } 79 | } 80 | 81 | extension ModifierTests { 82 | static var allTests: Linux.TestList { 83 | return [ 84 | ("testModifierInput", testModifierInput), 85 | ("testInitializingParserWithModifiers", testInitializingParserWithModifiers), 86 | ("testAddingModifiers", testAddingModifiers), 87 | ("testMultipleModifiersForSameTarget", testMultipleModifiersForSameTarget) 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/InkTests/TableTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2020 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class TableTests: XCTestCase { 11 | func testTableWithoutHeader() { 12 | let html = MarkdownParser().html(from: """ 13 | | HeaderA | HeaderB | 14 | | CellA | CellB | 15 | """) 16 | 17 | XCTAssertEqual(html, """ 18 | \ 19 | \ 20 | \ 21 |
    HeaderAHeaderB
    CellACellB
    22 | """) 23 | } 24 | 25 | func testTableWithHeader() { 26 | let html = MarkdownParser().html(from: """ 27 | | HeaderA | HeaderB | HeaderC | 28 | | ------- | ------- | ------- | 29 | | CellA1 | CellB1 | CellC1 | 30 | | CellA2 | CellB2 | CellC2 | 31 | """) 32 | 33 | XCTAssertEqual(html, """ 34 | \ 35 | \ 36 | \ 37 | \ 38 | \ 39 | \ 40 |
    HeaderAHeaderBHeaderC
    CellA1CellB1CellC1
    CellA2CellB2CellC2
    41 | """) 42 | } 43 | 44 | func testTableWithUnalignedColumns() { 45 | let html = MarkdownParser().html(from: """ 46 | | HeaderA | HeaderB | HeaderC | 47 | | ------------------------------ | ----------- | ------------ | 48 | | CellA1 | CellB1 | CellC1 | 49 | | CellA2 | CellB2 | CellC2 | 50 | """) 51 | 52 | XCTAssertEqual(html, """ 53 | \ 54 | \ 55 | \ 56 | \ 57 | \ 58 | \ 59 |
    HeaderAHeaderBHeaderC
    CellA1CellB1CellC1
    CellA2CellB2CellC2
    60 | """) 61 | } 62 | 63 | func testTableWithOnlyHeader() { 64 | let html = MarkdownParser().html(from: """ 65 | | HeaderA | HeaderB | HeaderC | 66 | | ----------| ----------| ------- | 67 | """) 68 | 69 | XCTAssertEqual(html, """ 70 | \ 71 | \ 72 |
    HeaderAHeaderBHeaderC
    73 | """) 74 | } 75 | 76 | func testIncompleteTable() { 77 | let html = MarkdownParser().html(from: """ 78 | | one | two | 79 | | three | 80 | | four | five | six 81 | """) 82 | 83 | XCTAssertEqual(html, "

    | one | two | | three | | four | five | six

    ") 84 | } 85 | 86 | func testInvalidTable() { 87 | let html = MarkdownParser().html(from: """ 88 | |123 Not a table 89 | """) 90 | 91 | XCTAssertEqual(html, "

    |123 Not a table

    ") 92 | } 93 | 94 | func testTableBetweenParagraphs() { 95 | let html = MarkdownParser().html(from: """ 96 | A paragraph. 97 | 98 | | A | B | 99 | | C | D | 100 | 101 | Another paragraph. 102 | """) 103 | 104 | XCTAssertEqual(html, """ 105 |

    A paragraph.

    \ 106 | \ 107 | \ 108 |
    AB
    CD
    \ 109 |

    Another paragraph.

    110 | """) 111 | } 112 | 113 | func testTableWithUnevenColumns() { 114 | let html = MarkdownParser().html(from: """ 115 | | one | two | 116 | | three | four | five | 117 | 118 | | one | two | 119 | | three | 120 | """) 121 | 122 | XCTAssertEqual(html, """ 123 | \ 124 | \ 125 | \ 126 |
    onetwo
    threefourfive
    \ 127 | \ 128 | \ 129 | \ 130 |
    onetwo
    three
    131 | """) 132 | } 133 | 134 | func testTableWithInternalMarkdown() { 135 | let html = MarkdownParser().html(from: """ 136 | | Table | Header | [Link](/uri) | 137 | | ------ | ---------- | ------------ | 138 | | Some | *emphasis* | and | 139 | | `code` | in | table | 140 | """) 141 | 142 | XCTAssertEqual(html, """ 143 | \ 144 | \ 145 | \ 146 | \ 147 | \ 148 | \ 149 | \ 150 | \ 151 |
    TableHeaderLink
    Someemphasisand
    codeintable
    152 | """) 153 | } 154 | 155 | func testTableWithAlignment() { 156 | let html = MarkdownParser().html(from: """ 157 | | Left | Center | Right | 158 | | :- | :-: | -:| 159 | | One | Two | Three | 160 | """) 161 | 162 | XCTAssertEqual(html, """ 163 | \ 164 | \ 165 | \ 166 | \ 167 | \ 168 | \ 169 | \ 170 |
    LeftCenterRight
    OneTwoThree
    171 | """) 172 | } 173 | 174 | func testMissingPipeEndsTable() { 175 | let html = MarkdownParser().html(from: """ 176 | | HeaderA | HeaderB | 177 | | ------- | ------- | 178 | | CellA | CellB | 179 | > Quote 180 | """) 181 | 182 | XCTAssertEqual(html, """ 183 | \ 184 | \ 185 | \ 186 |
    HeaderAHeaderB
    CellACellB
    \ 187 |

    Quote

    188 | """) 189 | } 190 | 191 | func testHeaderNotParsedForColumnCountMismatch() { 192 | let html = MarkdownParser().html(from: """ 193 | | HeaderA | HeaderB | 194 | | ------- | 195 | | CellA | CellB | 196 | """) 197 | 198 | XCTAssertEqual(html, """ 199 | \ 200 | \ 201 | \ 202 | \ 203 |
    HeaderAHeaderB
    -------
    CellACellB
    204 | """) 205 | } 206 | } 207 | 208 | extension TableTests { 209 | static var allTests: Linux.TestList { 210 | return [ 211 | ("testTableWithoutHeader", testTableWithoutHeader), 212 | ("testTableWithHeader", testTableWithHeader), 213 | ("testTableWithUnalignedColumns", testTableWithUnalignedColumns), 214 | ("testTableWithOnlyHeader", testTableWithOnlyHeader), 215 | ("testIncompleteTable", testIncompleteTable), 216 | ("testInvalidTable", testInvalidTable), 217 | ("testTableBetweenParagraphs", testTableBetweenParagraphs), 218 | ("testTableWithUnevenColumns", testTableWithUnevenColumns), 219 | ("testTableWithInternalMarkdown", testTableWithInternalMarkdown), 220 | ("testTableWithAlignment", testTableWithAlignment), 221 | ("testMissingPipeEndsTable", testMissingPipeEndsTable), 222 | ("testHeaderNotParsedForColumnCountMismatch", testHeaderNotParsedForColumnCountMismatch), 223 | ] 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Tests/InkTests/TextFormattingTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import Ink 9 | 10 | final class TextFormattingTests: XCTestCase { 11 | func testParagraph() { 12 | let html = MarkdownParser().html(from: "Hello, world!") 13 | XCTAssertEqual(html, "

    Hello, world!

    ") 14 | } 15 | 16 | func testParagraphs() { 17 | let html = MarkdownParser().html(from: "Hello, world!\n\nAgain.") 18 | XCTAssertEqual(html, "

    Hello, world!

    Again.

    ") 19 | } 20 | 21 | func testDosParagraphs() { 22 | let html = MarkdownParser().html(from: "Hello, world!\r\n\r\nAgain.") 23 | XCTAssertEqual(html, "

    Hello, world!

    Again.

    ") 24 | } 25 | 26 | func testItalicText() { 27 | let html = MarkdownParser().html(from: "Hello, *world*!") 28 | XCTAssertEqual(html, "

    Hello, world!

    ") 29 | } 30 | 31 | func testBoldText() { 32 | let html = MarkdownParser().html(from: "Hello, **world**!") 33 | XCTAssertEqual(html, "

    Hello, world!

    ") 34 | } 35 | 36 | func testItalicBoldText() { 37 | let html = MarkdownParser().html(from: "Hello, ***world***!") 38 | XCTAssertEqual(html, "

    Hello, world!

    ") 39 | } 40 | 41 | func testItalicBoldTextWithSeparateStartMarkers() { 42 | let html = MarkdownParser().html(from: "**Hello, *world***!") 43 | XCTAssertEqual(html, "

    Hello, world!

    ") 44 | } 45 | 46 | func testItalicTextWithinBoldText() { 47 | let html = MarkdownParser().html(from: "**Hello, *world*!**") 48 | XCTAssertEqual(html, "

    Hello, world!

    ") 49 | } 50 | 51 | func testBoldTextWithinItalicText() { 52 | let html = MarkdownParser().html(from: "*Hello, **world**!*") 53 | XCTAssertEqual(html, "

    Hello, world!

    ") 54 | } 55 | 56 | func testItalicTextWithExtraLeadingMarkers() { 57 | let html = MarkdownParser().html(from: "**Hello*") 58 | XCTAssertEqual(html, "

    *Hello

    ") 59 | } 60 | 61 | func testBoldTextWithExtraLeadingMarkers() { 62 | let html = MarkdownParser().html(from: "***Hello**") 63 | XCTAssertEqual(html, "

    *Hello

    ") 64 | } 65 | 66 | func testItalicTextWithExtraTrailingMarkers() { 67 | let html = MarkdownParser().html(from: "*Hello**") 68 | XCTAssertEqual(html, "

    Hello*

    ") 69 | } 70 | 71 | func testBoldTextWithExtraTrailingMarkers() { 72 | let html = MarkdownParser().html(from: "**Hello***") 73 | XCTAssertEqual(html, "

    Hello*

    ") 74 | } 75 | 76 | func testItalicBoldTextWithExtraTrailingMarkers() { 77 | let html = MarkdownParser().html(from: "**Hello, *world*****!") 78 | XCTAssertEqual(html, "

    Hello, world**!

    ") 79 | } 80 | 81 | func testUnterminatedItalicMarker() { 82 | let html = MarkdownParser().html(from: "*Hello") 83 | XCTAssertEqual(html, "

    *Hello

    ") 84 | } 85 | 86 | func testUnterminatedBoldMarker() { 87 | let html = MarkdownParser().html(from: "**Hello") 88 | XCTAssertEqual(html, "

    **Hello

    ") 89 | } 90 | 91 | func testUnterminatedItalicBoldMarker() { 92 | let html = MarkdownParser().html(from: "***Hello") 93 | XCTAssertEqual(html, "

    ***Hello

    ") 94 | } 95 | 96 | func testUnterminatedItalicMarkerWithinBoldText() { 97 | let html = MarkdownParser().html(from: "**Hello, *world!**") 98 | XCTAssertEqual(html, "

    Hello, *world!

    ") 99 | } 100 | 101 | func testUnterminatedBoldMarkerWithinItalicText() { 102 | let html = MarkdownParser().html(from: "*Hello, **world!*") 103 | XCTAssertEqual(html, "

    Hello, **world!

    ") 104 | } 105 | 106 | func testStrikethroughText() { 107 | let html = MarkdownParser().html(from: "Hello, ~~world!~~") 108 | XCTAssertEqual(html, "

    Hello, world!

    ") 109 | } 110 | 111 | func testSingleTildeWithinStrikethroughText() { 112 | let html = MarkdownParser().html(from: "Hello, ~~wor~ld!~~") 113 | XCTAssertEqual(html, "

    Hello, wor~ld!

    ") 114 | } 115 | 116 | func testUnterminatedStrikethroughMarker() { 117 | let html = MarkdownParser().html(from: "~~Hello") 118 | XCTAssertEqual(html, "

    ~~Hello

    ") 119 | } 120 | 121 | func testEncodingSpecialCharacters() { 122 | let html = MarkdownParser().html(from: "Hello < World & >") 123 | XCTAssertEqual(html, "

    Hello < World & >

    ") 124 | } 125 | 126 | func testSingleLineBlockquote() { 127 | let html = MarkdownParser().html(from: "> Hello, world!") 128 | XCTAssertEqual(html, "

    Hello, world!

    ") 129 | } 130 | 131 | func testMultiLineBlockquote() { 132 | let html = MarkdownParser().html(from: """ 133 | > One 134 | > Two 135 | > Three 136 | """) 137 | 138 | XCTAssertEqual(html, "

    One Two Three

    ") 139 | } 140 | 141 | func testEscapingSymbolsWithBackslash() { 142 | let html = MarkdownParser().html(from: """ 143 | \\# Not a title 144 | \\*Not italic\\* 145 | """) 146 | 147 | XCTAssertEqual(html, "

    # Not a title *Not italic*

    ") 148 | } 149 | 150 | 151 | func testListAfterFormattedText() { 152 | let html = MarkdownParser().html(from: """ 153 | This is a test 154 | - One 155 | - Two 156 | """) 157 | 158 | XCTAssertEqual(html, """ 159 |

    This is a test

    • One
    • Two
    160 | """) 161 | } 162 | 163 | func testDoubleSpacedHardLinebreak() { 164 | let html = MarkdownParser().html(from: "Line 1 \nLine 2") 165 | 166 | XCTAssertEqual(html, "

    Line 1
    Line 2

    ") 167 | } 168 | 169 | func testEscapedHardLinebreak() { 170 | let html = MarkdownParser().html(from: "Line 1\\\nLine 2") 171 | 172 | XCTAssertEqual(html, "

    Line 1
    Line 2

    ") 173 | } 174 | } 175 | 176 | extension TextFormattingTests { 177 | static var allTests: Linux.TestList { 178 | return [ 179 | ("testParagraph", testParagraph), 180 | ("testParagraphs", testParagraphs), 181 | ("testDosParagraphs", testDosParagraphs), 182 | ("testItalicText", testItalicText), 183 | ("testBoldText", testBoldText), 184 | ("testItalicBoldText", testItalicBoldText), 185 | ("testItalicBoldTextWithSeparateStartMarkers", testItalicBoldTextWithSeparateStartMarkers), 186 | ("testItalicTextWithinBoldText", testItalicTextWithinBoldText), 187 | ("testBoldTextWithinItalicText", testBoldTextWithinItalicText), 188 | ("testItalicTextWithExtraLeadingMarkers", testItalicTextWithExtraLeadingMarkers), 189 | ("testBoldTextWithExtraLeadingMarkers", testBoldTextWithExtraLeadingMarkers), 190 | ("testItalicTextWithExtraTrailingMarkers", testItalicTextWithExtraTrailingMarkers), 191 | ("testBoldTextWithExtraTrailingMarkers", testBoldTextWithExtraTrailingMarkers), 192 | ("testItalicBoldTextWithExtraTrailingMarkers", testItalicBoldTextWithExtraTrailingMarkers), 193 | ("testUnterminatedItalicMarker", testUnterminatedItalicMarker), 194 | ("testUnterminatedBoldMarker", testUnterminatedBoldMarker), 195 | ("testUnterminatedItalicBoldMarker", testUnterminatedItalicBoldMarker), 196 | ("testUnterminatedItalicMarkerWithinBoldText", testUnterminatedItalicMarkerWithinBoldText), 197 | ("testUnterminatedBoldMarkerWithinItalicText", testUnterminatedBoldMarkerWithinItalicText), 198 | ("testStrikethroughText", testStrikethroughText), 199 | ("testSingleTildeWithinStrikethroughText", testSingleTildeWithinStrikethroughText), 200 | ("testUnterminatedStrikethroughMarker", testUnterminatedStrikethroughMarker), 201 | ("testEncodingSpecialCharacters", testEncodingSpecialCharacters), 202 | ("testSingleLineBlockquote", testSingleLineBlockquote), 203 | ("testMultiLineBlockquote", testMultiLineBlockquote), 204 | ("testEscapingSymbolsWithBackslash", testEscapingSymbolsWithBackslash), 205 | ("testListAfterFormattedText", testListAfterFormattedText), 206 | ("testDoubleSpacedHardLinebreak", testDoubleSpacedHardLinebreak), 207 | ("testEscapedHardLinebreak", testEscapedHardLinebreak) 208 | ] 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Tests/InkTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | 9 | public func allTests() -> [Linux.TestCase] { 10 | return [ 11 | Linux.makeTestCase(using: CodeTests.allTests), 12 | Linux.makeTestCase(using: HeadingTests.allTests), 13 | Linux.makeTestCase(using: HorizontalLineTests.allTests), 14 | Linux.makeTestCase(using: HTMLTests.allTests), 15 | Linux.makeTestCase(using: ImageTests.allTests), 16 | Linux.makeTestCase(using: LinkTests.allTests), 17 | Linux.makeTestCase(using: ListTests.allTests), 18 | Linux.makeTestCase(using: MarkdownTests.allTests), 19 | Linux.makeTestCase(using: ModifierTests.allTests), 20 | Linux.makeTestCase(using: TableTests.allTests), 21 | Linux.makeTestCase(using: TextFormattingTests.allTests) 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Ink 3 | * Copyright (c) John Sundell 2019 4 | * MIT license, see LICENSE file for details 5 | */ 6 | 7 | import XCTest 8 | import InkTests 9 | 10 | var tests = [XCTestCaseEntry]() 11 | tests += InkTests.allTests() 12 | XCTMain(tests) 13 | --------------------------------------------------------------------------------