From 72a74833032614f4e59d226b713aae92fb6ad729 Mon Sep 17 00:00:00 2001 From: Utku Birkan Date: Fri, 24 Jul 2020 17:11:34 +0300 Subject: [PATCH 1/4] First implementation of math support --- README.md | 1 + Sources/Ink/API/Modifier.swift | 1 + Sources/Ink/Internal/FormattedText.swift | 1 + Sources/Ink/Internal/Math.swift | 53 ++++++++++++++++++++++++ Tests/InkTests/MathTests.swift | 29 +++++++++++++ Tests/InkTests/XCTestManifests.swift | 3 +- 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Sources/Ink/Internal/Math.swift create mode 100644 Tests/InkTests/MathTests.swift diff --git a/README.md b/README.md index 32ec481..e93ca33 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Ink supports the following Markdown features: | Row 1 | Cell 1 | | Row 2 | Cell 2 | ``` +- Inline equations can be created by enclosing the equation with dollar signs, like `$f(\mathbf{r},t)$`. Display mode equations can be created by enclosing the equation with two dollar signs, like `$$\braket{\psi\vert\psi}$$`. 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). 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..13a8796 100644 --- a/Sources/Ink/Internal/FormattedText.swift +++ b/Sources/Ink/Internal/FormattedText.swift @@ -334,6 +334,7 @@ private extension FormattedText { case "[": return Link.self case "!": return Image.self case "<": return HTML.self + case "$": return Math.self default: return nil } } diff --git a/Sources/Ink/Internal/Math.swift b/Sources/Ink/Internal/Math.swift new file mode 100644 index 0000000..88a2732 --- /dev/null +++ b/Sources/Ink/Internal/Math.swift @@ -0,0 +1,53 @@ +/** +* 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 { + let startingDollarCount = reader.readCount(of: "$") + let displayMode = startingDollarCount > 1 ? true : false + + var tex = "" + + while !reader.didReachEnd { + switch reader.currentCharacter { + case \.isNewline : + throw Reader.Error() + case "$": + if displayMode { + reader.advanceIndex(by: 2) + } else { + reader.advanceIndex() + } + return Math(displayMode: displayMode, tex: tex) + default: + 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 { + tex + + } +} + diff --git a/Tests/InkTests/MathTests.swift b/Tests/InkTests/MathTests.swift new file mode 100644 index 0000000..7dded86 --- /dev/null +++ b/Tests/InkTests/MathTests.swift @@ -0,0 +1,29 @@ +/** +* 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

") + } + +} + +extension MathTests { + static var allTests: Linux.TestList { + return [ + ("testInlineMath", testInlineMath), + ] + } +} 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) ] } From f127e936f8418013c66c384477ed5e7c68d15255 Mon Sep 17 00:00:00 2001 From: Utku Birkan Date: Sat, 25 Jul 2020 14:43:36 +0300 Subject: [PATCH 2/4] Switch to LaTeX like input --- README.md | 4 +++- Sources/Ink/Internal/FormattedText.swift | 6 ++++- Sources/Ink/Internal/Math.swift | 28 +++++++++++++++++------- Tests/InkTests/MathTests.swift | 12 +++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e93ca33..38f35c6 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,9 @@ Ink supports the following Markdown features: | Row 1 | Cell 1 | | Row 2 | Cell 2 | ``` -- Inline equations can be created by enclosing the equation with dollar signs, like `$f(\mathbf{r},t)$`. Display mode equations can be created by enclosing the equation with two dollar signs, like `$$\braket{\psi\vert\psi}$$`. 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). +- 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/Internal/FormattedText.swift b/Sources/Ink/Internal/FormattedText.swift index 13a8796..a6c52d9 100644 --- a/Sources/Ink/Internal/FormattedText.swift +++ b/Sources/Ink/Internal/FormattedText.swift @@ -334,7 +334,11 @@ private extension FormattedText { case "[": return Link.self case "!": return Image.self case "<": return HTML.self - case "$": return Math.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 index 88a2732..18ec051 100644 --- a/Sources/Ink/Internal/Math.swift +++ b/Sources/Ink/Internal/Math.swift @@ -11,22 +11,34 @@ internal struct Math: Fragment { private var tex: String static func read(using reader: inout Reader) throws -> Math { - let startingDollarCount = reader.readCount(of: "$") - let displayMode = startingDollarCount > 1 ? true : false - + reader.advanceIndex() + let displayMode: Bool + let closingCharacter: Character + if reader.currentCharacter == "[" { + displayMode = true + closingCharacter = "]" + } else { + displayMode = false + closingCharacter = ")" + } + reader.advanceIndex() var tex = "" while !reader.didReachEnd { switch reader.currentCharacter { case \.isNewline : throw Reader.Error() - case "$": - if displayMode { + case "\\" : + guard let nextCharacter = reader.nextCharacter else { + throw Reader.Error() + } + + if nextCharacter == closingCharacter { reader.advanceIndex(by: 2) - } else { - reader.advanceIndex() + return Math(displayMode: displayMode, tex: tex) } - return Math(displayMode: displayMode, tex: tex) + + fallthrough default: if let escaped = reader.currentCharacter.escaped { tex.append(escaped) diff --git a/Tests/InkTests/MathTests.swift b/Tests/InkTests/MathTests.swift index 7dded86..3438dda 100644 --- a/Tests/InkTests/MathTests.swift +++ b/Tests/InkTests/MathTests.swift @@ -9,21 +9,27 @@ import Ink final class MathTests: XCTestCase { func testInlineMath() { - let html = MarkdownParser().html(from: "$Hello \\Latex$") + let html = MarkdownParser().html(from: "\\(Hello \\Latex\\)") XCTAssertEqual(html, "

Hello \\Latex

") } func testDisplayMath() { - let html = MarkdownParser().html(from: "$$Hello \\Latex$$") + 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

") + } } extension MathTests { static var allTests: Linux.TestList { return [ ("testInlineMath", testInlineMath), + ("testDisplayMath", testDisplayMath), + ("testMathWithEscape", testMathWithEscape), ] } } From 6b519dafc25becca0e9ce26db6dea10705f1110c Mon Sep 17 00:00:00 2001 From: Utku Birkan Date: Sat, 25 Jul 2020 16:53:48 +0300 Subject: [PATCH 3/4] Fix testLinkWithEscapedSquareBrackets to also parse equation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `testLinkWithEscapedSquareBrackets` now parses the inner escaped brackets, and returns a span. I am not sure if this is how it is supposed to work though, as ‘equations in links’ is not a practical concept. --- Tests/InkTests/LinkTests.swift | 2 +- Tests/InkTests/MathTests.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 index 3438dda..3b5011f 100644 --- a/Tests/InkTests/MathTests.swift +++ b/Tests/InkTests/MathTests.swift @@ -9,18 +9,18 @@ import Ink final class MathTests: XCTestCase { func testInlineMath() { - let html = MarkdownParser().html(from: "\\(Hello \\Latex\\)") - XCTAssertEqual(html, "

Hello \\Latex

") + let html = MarkdownParser().html(from: #"\(Hello \Latex\)"#) + XCTAssertEqual(html, #"

Hello \Latex

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

Hello \\Latex

") + 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

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

Asterix * and Hello \Latex

"#) } } From a51180f07ebd24855e3fb1e5342400b37b47a40d Mon Sep 17 00:00:00 2001 From: Utku Birkan Date: Sat, 29 Aug 2020 14:07:18 +0300 Subject: [PATCH 4/4] Fix multiline equation handling --- Sources/Ink/Internal/Math.swift | 34 +++++++++++++++++++++++---------- Tests/InkTests/MathTests.swift | 31 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/Sources/Ink/Internal/Math.swift b/Sources/Ink/Internal/Math.swift index 18ec051..8ce4afb 100644 --- a/Sources/Ink/Internal/Math.swift +++ b/Sources/Ink/Internal/Math.swift @@ -22,24 +22,33 @@ internal struct Math: Fragment { closingCharacter = ")" } reader.advanceIndex() + reader.discardWhitespacesAndNewlines() + var tex = "" + // TeX math mode does not care about the whitespace count/type. + var previousCharacterIsSpace = false while !reader.didReachEnd { - switch reader.currentCharacter { - case \.isNewline : + guard let nextCharacter = reader.nextCharacter else { throw Reader.Error() - case "\\" : - 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 { @@ -58,8 +67,13 @@ internal struct Math: Fragment { } func plainText() -> String { - tex - + let plainTex: String + if displayMode { + plainTex = "\\[\(tex)\\]" + } else { + plainTex = "\\(\(tex)\\)" + } + return plainTex } } diff --git a/Tests/InkTests/MathTests.swift b/Tests/InkTests/MathTests.swift index 3b5011f..1960dab 100644 --- a/Tests/InkTests/MathTests.swift +++ b/Tests/InkTests/MathTests.swift @@ -22,6 +22,35 @@ final class MathTests: XCTestCase { 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 { @@ -30,6 +59,8 @@ extension MathTests { ("testInlineMath", testInlineMath), ("testDisplayMath", testDisplayMath), ("testMathWithEscape", testMathWithEscape), + ("testDisplayMultiLineProgression", testDisplayMultiLineProgression), + ("testDisplayMultilineWithParagraph", testDisplayMultilineWithParagraph), ] } }