diff --git a/README.md b/README.md index 32ec481..38f35c6 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,9 @@ Ink supports the following Markdown features: | Row 1 | Cell 1 | | Row 2 | Cell 2 | ``` +- LaTeX like equation support. As dollar signs can be found quite commonly on articles (and the slash character is already the escape character), TeX-like equation input is not supported. Note that Ink does _not_ render math equations. You need another library for that, like [KaTeX](https://katex.org) or [MathJax](https://www.mathjax.org). There are two equation modes: + 1. Inline mode equations: `\(x^2 + 5\)` + 2. Display mode equations: `\[z^2 + 5\]` 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). diff --git a/Sources/Ink/API/Modifier.swift b/Sources/Ink/API/Modifier.swift index 980ecb1..78cea78 100644 --- a/Sources/Ink/API/Modifier.swift +++ b/Sources/Ink/API/Modifier.swift @@ -53,5 +53,6 @@ public extension Modifier { case lists case paragraphs case tables + case math } } diff --git a/Sources/Ink/Internal/FormattedText.swift b/Sources/Ink/Internal/FormattedText.swift index a1d54f8..a6c52d9 100644 --- a/Sources/Ink/Internal/FormattedText.swift +++ b/Sources/Ink/Internal/FormattedText.swift @@ -334,6 +334,11 @@ private extension FormattedText { case "[": return Link.self case "!": return Image.self case "<": return HTML.self + case "\\": + if ["[","("].contains(reader.nextCharacter) { + return Math.self + } + fallthrough default: return nil } } diff --git a/Sources/Ink/Internal/Math.swift b/Sources/Ink/Internal/Math.swift new file mode 100644 index 0000000..8ce4afb --- /dev/null +++ b/Sources/Ink/Internal/Math.swift @@ -0,0 +1,79 @@ +/** +* Ink +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +internal struct Math: Fragment { + var modifierTarget: Modifier.Target { .math } + + private var displayMode: Bool + private var tex: String + + static func read(using reader: inout Reader) throws -> Math { + reader.advanceIndex() + let displayMode: Bool + let closingCharacter: Character + if reader.currentCharacter == "[" { + displayMode = true + closingCharacter = "]" + } else { + displayMode = false + closingCharacter = ")" + } + reader.advanceIndex() + reader.discardWhitespacesAndNewlines() + + var tex = "" + // TeX math mode does not care about the whitespace count/type. + var previousCharacterIsSpace = false + + while !reader.didReachEnd { + guard let nextCharacter = reader.nextCharacter else { + throw Reader.Error() + } + + switch reader.currentCharacter { + case \.isWhitespace: + previousCharacterIsSpace = true + reader.discardWhitespacesAndNewlines() + case "\\": + if nextCharacter == closingCharacter { + reader.advanceIndex(by: 2) + return Math(displayMode: displayMode, tex: tex) + } + fallthrough + default: + if previousCharacterIsSpace { + tex.append(" ") + previousCharacterIsSpace = false + } + + if let escaped = reader.currentCharacter.escaped { + tex.append(escaped) + } else { + tex.append(reader.currentCharacter) + } + reader.advanceIndex() + } + } + throw Reader.Error() + } + + func html(usingURLs urls: NamedURLCollection, + modifiers: ModifierCollection) -> String { + let modeString = displayMode ? "display" : "inline" + return "\(tex)" + } + + func plainText() -> String { + let plainTex: String + if displayMode { + plainTex = "\\[\(tex)\\]" + } else { + plainTex = "\\(\(tex)\\)" + } + return plainTex + } +} + diff --git a/Tests/InkTests/LinkTests.swift b/Tests/InkTests/LinkTests.swift index e6aed29..b772424 100644 --- a/Tests/InkTests/LinkTests.swift +++ b/Tests/InkTests/LinkTests.swift @@ -67,7 +67,7 @@ final class LinkTests: XCTestCase { func testLinkWithEscapedSquareBrackets() { let html = MarkdownParser().html(from: "[\\[Hello\\]](hello)") - XCTAssertEqual(html, #"

[Hello]

"#) + XCTAssertEqual(html, #"

Hello

"#) } } diff --git a/Tests/InkTests/MathTests.swift b/Tests/InkTests/MathTests.swift new file mode 100644 index 0000000..1960dab --- /dev/null +++ b/Tests/InkTests/MathTests.swift @@ -0,0 +1,66 @@ +/** +* Ink +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +import XCTest +import Ink + +final class MathTests: XCTestCase { + func testInlineMath() { + let html = MarkdownParser().html(from: #"\(Hello \Latex\)"#) + XCTAssertEqual(html, #"

Hello \Latex

"#) + } + + func testDisplayMath() { + let html = MarkdownParser().html(from: #"\[Hello \Latex\]"#) + XCTAssertEqual(html, #"

Hello \Latex

"#) + } + + func testMathWithEscape() { + let html = MarkdownParser().html(from: #"Asterix \* and \(Hello \Latex\)"#) + XCTAssertEqual(html, #"

Asterix * and Hello \Latex

"#) + } + func testDisplayMultiLineProgression() { + let html = MarkdownParser().html(from: #""" + \[\begin{aligned} + y&=\left(x-r\right)\left(x-s\right)\\ + y&=\left(x-\left(-7\right)\right)\left(x-\left(-2\right)\right)\\ + y&=\left(x+7\right)\left(x+2\right)\\ + y&=x^2+9x+14\\ + y&=x^2+bx+c\\ + \end{aligned}\] + """#) + print(html) + XCTAssertEqual(html, #""" +

\begin{aligned} y&=\left(x-r\right)\left(x-s\right)\\ y&=\left(x-\left(-7\right)\right)\left(x-\left(-2\right)\right)\\ y&=\left(x+7\right)\left(x+2\right)\\ y&=x^2+9x+14\\ y&=x^2+bx+c\\ \end{aligned}

+ """#) + } + + func testDisplayMultilineWithParagraph() { + let html = MarkdownParser().html(from: #""" + We can write a vector in a Hilbert space as a sum of basis and projection coefficients \[ + \begin{aligned} + \left\vert\psi\right\rangle &= \sum_iC_i\left\vert\varphi_i\right\rangle\\ + &=\left\langle\varphi_i\vert\psi\right\rangle \left\vert\varphi_i\right\rangle + \end{aligned} + \] as above. + """#) + XCTAssertEqual(html, #""" +

We can write a vector in a Hilbert space as a sum of basis and projection coefficients \begin{aligned} \left\vert\psi\right\rangle &= \sum_iC_i\left\vert\varphi_i\right\rangle\\ &=\left\langle\varphi_i\vert\psi\right\rangle \left\vert\varphi_i\right\rangle \end{aligned} as above.

+ """#) + } +} + +extension MathTests { + static var allTests: Linux.TestList { + return [ + ("testInlineMath", testInlineMath), + ("testDisplayMath", testDisplayMath), + ("testMathWithEscape", testMathWithEscape), + ("testDisplayMultiLineProgression", testDisplayMultiLineProgression), + ("testDisplayMultilineWithParagraph", testDisplayMultilineWithParagraph), + ] + } +} diff --git a/Tests/InkTests/XCTestManifests.swift b/Tests/InkTests/XCTestManifests.swift index 0244f80..484171a 100644 --- a/Tests/InkTests/XCTestManifests.swift +++ b/Tests/InkTests/XCTestManifests.swift @@ -18,6 +18,7 @@ public func allTests() -> [Linux.TestCase] { Linux.makeTestCase(using: MarkdownTests.allTests), Linux.makeTestCase(using: ModifierTests.allTests), Linux.makeTestCase(using: TableTests.allTests), - Linux.makeTestCase(using: TextFormattingTests.allTests) + Linux.makeTestCase(using: TextFormattingTests.allTests), + Linux.makeTestCase(using: MathTests.allTests) ] }