├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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: ``.
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)\(tagName)>"
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)\(tagName)>"
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 = { "" }
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: "\(elementName)>"
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 = ""
67 | }
68 |
69 | let prefix = self.prefix.map(String.init) ?? ""
70 | let suffix = self.suffix.map(String.init) ?? ""
71 | return prefix + leadingTag + style.htmlTagName + ">" + 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\nHeading
")
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: "")
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: "")
27 | XCTAssertEqual(html, #"
"#)
28 | }
29 |
30 | func testImageWithReferenceAndAltText() {
31 | let html = MarkdownParser().html(from: """
32 | ![Alt text][url]
33 | [url]: swiftbysundell.com
34 | """)
35 |
36 | XCTAssertEqual(html, #"
"#)
37 | }
38 |
39 | func testImageWithinParagraph() {
40 | let html = MarkdownParser().html(from: "Text  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, #"- One
- 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, #"- One
- 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, "- One
- Two
- 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, "- One 3!. Two
- Three
")
55 | }
56 |
57 | func testUnorderedList() {
58 | let html = MarkdownParser().html(from: """
59 | - One
60 | - Two
61 | - Three
62 | """)
63 |
64 | XCTAssertEqual(html, "")
65 | }
66 |
67 | func testMixedUnorderedList() {
68 | let html = MarkdownParser().html(from: """
69 | - One
70 | * Two
71 | * Three
72 | - Four
73 | """)
74 |
75 | XCTAssertEqual(html, "")
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, #"- One
- Two
- Three
"#)
87 | }
88 |
89 | func testUnorderedListWithMultiLineItem() {
90 | let html = MarkdownParser().html(from: """
91 | - One
92 | Some text
93 | - Two
94 | """)
95 |
96 | XCTAssertEqual(html, "")
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 | "",
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, "")
135 | }
136 |
137 | func testOrderedIndentedList() {
138 | let html = MarkdownParser().html(from: """
139 | 1. One
140 | 2. Two
141 | """)
142 |
143 | XCTAssertEqual(html, #"- One
- Two
"#)
144 | }
145 |
146 | func testUnorderedIndentedList() {
147 | let html = MarkdownParser().html(from: """
148 | - One
149 | - Two
150 | - Three
151 | """)
152 |
153 | XCTAssertEqual(html, "")
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)  `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 | HeaderA | HeaderB |
\
20 | CellA | CellB |
\
21 |
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 | HeaderA | HeaderB | HeaderC |
\
36 | \
37 | CellA1 | CellB1 | CellC1 |
\
38 | CellA2 | CellB2 | CellC2 |
\
39 | \
40 |
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 | HeaderA | HeaderB | HeaderC |
\
55 | \
56 | CellA1 | CellB1 | CellC1 |
\
57 | CellA2 | CellB2 | CellC2 |
\
58 | \
59 |
60 | """)
61 | }
62 |
63 | func testTableWithOnlyHeader() {
64 | let html = MarkdownParser().html(from: """
65 | | HeaderA | HeaderB | HeaderC |
66 | | ----------| ----------| ------- |
67 | """)
68 |
69 | XCTAssertEqual(html, """
70 | \
71 | HeaderA | HeaderB | HeaderC |
\
72 |
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 | \
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 | one | two | |
\
125 | three | four | five |
\
126 |
\
127 | \
128 | one | two |
\
129 | three | |
\
130 |
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 | Table | Header | Link |
\
146 | \
147 | \
148 | Some | emphasis | and |
\
149 | code | in | table |
\
150 | \
151 |
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 | Left | Center | Right | \
166 |
\
167 | \
168 | One | Two | Three |
\
169 | \
170 |
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 | HeaderA | HeaderB |
\
185 | CellA | CellB |
\
186 |
\
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 | HeaderA | HeaderB |
\
201 | ------- | |
\
202 | CellA | CellB |
\
203 |
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
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 |
--------------------------------------------------------------------------------