From f4ba3e0ebe7b3b30f032473f326f24191585e726 Mon Sep 17 00:00:00 2001 From: Rusty Zarse <28144+levous@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:17:58 -0400 Subject: [PATCH 1/7] Record hoedown's and the PEG style parser's outputs for the tests Before the C libraries go, their outputs that the port must match are recorded in engine-snapshots.json: hoedown's HTML for the renderer's byte-for-byte cases, front matter, TOC, hard wrap, math, autolink, smart punctuation, highlight/superscript and Prism languages; its escaping; and pmh_styleparser.c's parse of every bundled theme and a malformed stylesheet. HTMLRendererTests, HTMLEscapingTests, PrismLanguageTests and ThemeStyleParserTests now compare with these instead of calling the C code. swift test 386 passed; app build succeeded. Co-Authored-By: Claude Opus 5.5 --- Tests/MacDownKitTests/EngineSnapshots.swift | 60 +++++++++++ Tests/MacDownKitTests/HTMLRendererTests.swift | 100 +++++++++--------- .../Resources/engine-snapshots.json | 75 +++++++++++++ .../ThemeStyleParserTests.swift | 88 +++------------ 4 files changed, 201 insertions(+), 122 deletions(-) create mode 100644 Tests/MacDownKitTests/EngineSnapshots.swift create mode 100644 Tests/MacDownKitTests/Resources/engine-snapshots.json diff --git a/Tests/MacDownKitTests/EngineSnapshots.swift b/Tests/MacDownKitTests/EngineSnapshots.swift new file mode 100644 index 0000000..c054bb7 --- /dev/null +++ b/Tests/MacDownKitTests/EngineSnapshots.swift @@ -0,0 +1,60 @@ +// +// EngineSnapshots.swift +// MacDownKitTests +// +// What the original engines produced, recorded on 2026-10-22 just before +// they were removed: hoedown's HTML (with MacDown's patches) and escaping, +// and PEG Markdown Highlight's style parser (pmh_styleparser.c). The port +// matched them byte for byte, and the tests keep it that way against these +// recordings (Resources/engine-snapshots.json). +// + +import Foundation +import Testing +@testable import MacDownKit + +enum EngineSnapshots { + static let values: [String: String] = { + let url = Bundle.module.url(forResource: "engine-snapshots", withExtension: "json", + subdirectory: "Resources")! + return try! JSONDecoder().decode([String: String].self, from: Data(contentsOf: url)) + }() + + /// The recording named `key`. + static func value(_ key: String) throws -> String { + try #require(values[key], "no snapshot \(key.debugDescription)") + } +} + +/// A theme parse result as text, one attribute per line, to compare with +/// the recorded C parser's. +func themeDescription(_ style: ThemeStyle) -> String { + func color(_ c: ThemeStyle.Color) -> String { + String(format: "%02x%02x%02x%02x", c.alpha, c.red, c.green, c.blue) + } + func describe(_ attribute: ThemeStyle.Attribute) -> String { + let value = switch attribute.value { + case .foregroundColor(let c): "foreground \(color(c))" + case .backgroundColor(let c): "background \(color(c))" + case .caretColor(let c): "caret \(color(c))" + case .fontSize(let points, let relative): "size \(points) relative \(relative)" + case .fontFamily(let family): "family \(family)" + case .fontStyle(let italic, let bold, let underlined): + "style italic \(italic) bold \(bold) underlined \(underlined)" + case .other(let string): "other \(string)" + } + return " \(attribute.name): \(value)" + } + var lines: [String] = [] + for (name, attributes) in [("editor", style.editor), ("editor-current-line", style.currentLine), + ("editor-selection", style.selection)] { + lines.append(name) + lines += attributes.map(describe) + } + for element in style.elements { + lines.append(element.element) + lines += element.attributes.map(describe) + } + lines += style.errors.map { "error \($0)" } + return lines.joined(separator: "\n") +} diff --git a/Tests/MacDownKitTests/HTMLRendererTests.swift b/Tests/MacDownKitTests/HTMLRendererTests.swift index 11b36c3..1192e1e 100644 --- a/Tests/MacDownKitTests/HTMLRendererTests.swift +++ b/Tests/MacDownKitTests/HTMLRendererTests.swift @@ -2,7 +2,8 @@ // HTMLRendererTests.swift // MacDownKitTests // -// The cmark-gfm renderer writes hoedown's markup (TR-5). +// The cmark-gfm renderer writes hoedown's markup (TR-5), checked against +// hoedown's recorded output (EngineSnapshots). // import CHoedown @@ -13,23 +14,15 @@ import Testing @Suite struct HTMLEscapingTests { static let stress = #"a & b < c > d "e" 'f' /g/ \h [i] {j} |k| ~l `m` ^n é 😀 %20 ?x=1&y='2' #frag"# - func hoedown(_ text: String, _ escape: (UnsafeMutablePointer?, UnsafePointer, Int) -> Void) -> String { - let ob = hoedown_buffer_new(64) - defer { hoedown_buffer_free(ob) } - let bytes = Array(text.utf8) - bytes.withUnsafeBufferPointer { escape(ob, $0.baseAddress!, $0.count) } - return String(decoding: UnsafeBufferPointer(start: ob!.pointee.data, count: ob!.pointee.size), as: UTF8.self) + @Test func htmlMatchesHoedown() throws { + #expect(HTMLEscaping.html(Self.stress) == (try EngineSnapshots.value("escape/html"))) } - @Test func htmlMatchesHoedown() { - #expect(HTMLEscaping.html(Self.stress) == hoedown(Self.stress) { hoedown_escape_html($0, $1, $2, 0) }) - } - - @Test func hrefMatchesHoedown() { - #expect(HTMLEscaping.href(Self.stress) == hoedown(Self.stress) { hoedown_escape_href($0, $1, $2) }) + @Test func hrefMatchesHoedown() throws { + #expect(HTMLEscaping.href(Self.stress) == (try EngineSnapshots.value("escape/href"))) // Every byte value. let all = String(decoding: (1...127).map { UInt8($0) }, as: UTF8.self) - #expect(HTMLEscaping.href(all) == hoedown(all) { hoedown_escape_href($0, $1, $2) }) + #expect(HTMLEscaping.href(all) == (try EngineSnapshots.value("escape/href-ascii"))) } } @@ -38,14 +31,9 @@ import Testing MarkdownDocumentModel(text, options: options).body } - /// hoedown's output with fresh-install settings. - func hoedown(_ text: String) -> String { - MarkdownParser.parse(text, settings: CorpusTests.settings).body - } - /// Where CommonMark and hoedown agree, the output is hoedown's, byte /// for byte (TR-5). - @Test(arguments: [ + nonisolated static let byteForByte = [ "# One\n\nText *em* **strong** `code`.\n\n## Two\n", "- a\n- b\n", "- a\n\n- b\n", @@ -61,9 +49,28 @@ import Testing "A[^n] B[^m].\n\n[^m]: Em.\n\n[^n]: En.\n", "[a](https://x.org/?a=1&b='2' \"T\") ![alt](i.png \"t\") s\n", "
\nraw\n
\n\n---\n", - ]) - func matchesHoedown(_ text: String) { - #expect(html(text) == hoedown(text)) + ] + + static let tocTexts = { () throws -> [String] in + [try #require(try Corpus.files().first { $0.name == "13-toc.md" }).text, + "## Start\n\n[TOC]\n\n# Higher [link](x) *em*\n\n### Deep\n"] + } + + static let mathTexts = [ + "$$\n\\int_0^1 x^2 \\, dx < 1\n$$\n", + "Inline \\\\( a_1 * b_2 \\\\) and display \\\\[ c_{*} \\\\].\n", + "In text $$x_1 * y_2$$ here.\n", + "Dollars $A^T_S = B$ and *emphasis*.\n", + "# Header $$h_1$$\n\n| $t_1$ |\n|---|\n", + ] + + static let autolinkText = "See https://example.org/page?q=1, www.example.org and hello@example.org; .\n" + static let smartText = "\"Double,\" 'single,' it's, en -- dash, em --- dash... `\"code\" --`\n\n```\n\"block\" --\n```\n" + static let markText = "==marked== and == spaced == and x^2, 10^(-6), e^(i pi) and a ^ caret. `==no== x^2`\n" + + @Test(arguments: byteForByte) + func matchesHoedown(_ text: String) throws { + #expect(html(text) == (try EngineSnapshots.value("render/" + text))) } /// Front matter (FR-15): a valid YAML block is a table before the body; @@ -71,7 +78,7 @@ import Testing @Test(arguments: ["09-front-matter.md", "11-leading-rule.md"]) func frontMatter(_ name: String) throws { let text = try #require(try Corpus.files().first { $0.name == name }).text - #expect(HTMLDiff.normalize(html(text)) == HTMLDiff.normalize(hoedown(text))) + #expect(HTMLDiff.normalize(html(text)) == HTMLDiff.normalize(try EngineSnapshots.value("front-matter/" + name))) } /// Invalid front matter is Markdown; in CommonMark a setext header can @@ -88,22 +95,21 @@ import Testing @Test func tableOfContents() throws { var settings = CorpusTests.settings settings.rendersTOC = true - for text in [try #require(try Corpus.files().first { $0.name == "13-toc.md" }).text, - "## Start\n\n[TOC]\n\n# Higher [link](x) *em*\n\n### Deep\n"] { + for (index, text) in try Self.tocTexts().enumerated() { let cmark = MarkdownDocumentModel(text, options: .init(settings)).body #expect(cmark.contains("