Files
hapi/ios/Packages/HapiKit/Tests/HapiUITests/MarkdownTransformsTests.swift
T

270 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import HapiUI
import Testing
/// Ports of the web transform tests (`remark-repair-tables.test.ts`,
/// `remark-strip-cjk-autolink.test.ts`, plus the indented-code and autolink
/// behaviors). Inline expectations mirror the web semantics except where a
/// documented iOS delta applies.
@Suite("MarkdownTransforms")
struct MarkdownTransformsTests {
// MARK: - repairTables
@Test func padsTwoCellSeparatorForThreeColumnHeader() {
let input = "| A | B | C |\n|---|---|\n| 1 | 2 | 3 |"
let output = MarkdownTransforms.repairTables(input)
let lines = output.components(separatedBy: "\n")
#expect(lines[1] == "|---|---| --- |")
#expect(lines[0] == "| A | B | C |")
#expect(lines[2] == "| 1 | 2 | 3 |")
}
@Test func padsOneCellSeparatorForFourColumnHeader() {
let input = "| a | b | c | d |\n|-|\n| 1 | 2 | 3 | 4 |"
let output = MarkdownTransforms.repairTables(input)
let separator = output.components(separatedBy: "\n")[1]
let cellCount = separator.components(separatedBy: "|")
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
.count
#expect(cellCount == 4)
}
@Test func leavesMatchingSeparatorUnchanged() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func leavesSeparatorWithMoreCellsThanHeaderUnchanged() {
let input = "| a | b |\n|---|---|---|"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func skipsSeparatorNotFollowingPipeHeader() {
let input = "A | B | C\n---|---"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func ignoresTableLikeLinesInsideBacktickFence() {
let input = "```\n| A | B | C |\n|---|---|\n```"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func ignoresTableLikeLinesInsideTildeFence() {
let input = "~~~\n| A | B | C |\n|---|---|\n~~~"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func fourBacktickFenceIsNotClosedByThreeBackticks() {
let input = "````\n```\n| A | B | C |\n|---|---|\n````"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func oppositeMarkerInsideFenceDoesNotFlipState() {
let input = "~~~\n```\n| A | B | C |\n|---|---|\n~~~"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func closingFenceWithInfoTextDoesNotClose() {
let input = "```\ntext\n``` info\n| A | B | C |\n|---|---|"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func repairsTableAfterFenceCloses() {
let input = "```\ncode\n```\n| A | B | C |\n|---|---|"
let output = MarkdownTransforms.repairTables(input)
let lines = output.components(separatedBy: "\n")
#expect(lines[4] == "|---|---| --- |")
}
@Test func preservesAlignmentHints() {
let input = "| a | b | c |\n|:--|--:|"
let output = MarkdownTransforms.repairTables(input)
#expect(output.components(separatedBy: "\n")[1] == "|:--|--:| --- |")
}
@Test func escapedPipeInHeaderIsLiteral() {
// `\|` is one literal pipe char, so the header has 2 cells — the
// 2-cell separator already matches and must stay untouched.
let input = "| a \\| b | c |\n|---|---|"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func pipeInsideCodeSpanIsNotACellBoundary() {
let input = "| `a|b` | c |\n|---|---|"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func preservesIndentationOfRepairedSeparator() {
let input = " | a | b | c |\n |---|---|"
let output = MarkdownTransforms.repairTables(input)
#expect(output.components(separatedBy: "\n")[1] == " |---|---| --- |")
}
@Test func truncatedDataRowIsLeftAlone() {
// Mid-row streaming truncation: GFM tolerates short body rows, so no
// string-level repair is needed (web parity).
let input = "| a | b | c |\n|---|---|---|\n| 1 | 2"
#expect(MarkdownTransforms.repairTables(input) == input)
}
@Test func repairsBrokenTableAndLeavesValidOne() {
let input = "| A | B |\n|---|---|\n| 1 | 2 |\n\n| X | Y | Z |\n|---|---|\n| 7 | 8 | 9 |"
let output = MarkdownTransforms.repairTables(input)
let lines = output.components(separatedBy: "\n")
#expect(lines[1] == "|---|---|")
#expect(lines[5] == "|---|---| --- |")
}
// MARK: - disableIndentedCode
@Test func deindentsTopLevelIndentedText() {
let input = "para:\n\n quoted reply"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "para:\n\n quoted reply")
}
@Test func deindentsDeeplyIndentedText() {
let input = "para:\n\n eight spaces"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "para:\n\n eight spaces")
}
@Test func treatsTabAsIndentation() {
let input = "para:\n\n\ttabbed"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "para:\n\n tabbed")
}
@Test func leavesFencedCodeAlone() {
let input = "```\n indented in fence\n```"
#expect(MarkdownTransforms.disableIndentedCode(input) == input)
}
@Test func leavesListContinuationAlone() {
let input = "1. item\n\n continuation text"
#expect(MarkdownTransforms.disableIndentedCode(input) == input)
}
@Test func leavesNestedListsAlone() {
let input = "- a\n - b\n - c\n - d"
#expect(MarkdownTransforms.disableIndentedCode(input) == input)
}
@Test func deindentsAfterListEnds() {
let input = "- item\n\ntext\n\n stray indent"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "- item\n\ntext\n\n stray indent")
}
@Test func deindentsTopLevelIndentedListMarker() {
// iOS delta: becomes a real list (web renders paragraph text).
let input = "para\n\n - foo"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "para\n\n - foo")
}
@Test func leavesIndentedFenceLookalikeBlockIntact() {
// A 4-space-indented ``` block stays one untouched unit.
let input = "text\n\n ```\n let x = 1\n ```"
#expect(MarkdownTransforms.disableIndentedCode(input) == input)
}
@Test func resumesDeindentingAfterIndentedFenceLookalikeCloses() {
let input = "text\n\n ```\n body\n ```\n\n tail"
let output = MarkdownTransforms.disableIndentedCode(input)
#expect(output == "text\n\n ```\n body\n ```\n\n tail")
}
// MARK: - splitTrailingCJKPunctuation
@Test func stripsTrailingFullwidthComma() {
let result = MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com,")
#expect(result?.url == "https://example.com")
#expect(result?.trailing == ",")
}
@Test func stripsTrailingIdeographicFullStop() {
let result = MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com/a。")
#expect(result?.url == "https://example.com/a")
#expect(result?.trailing == "。")
}
@Test func stripsMultipleTrailingCJKPunctuation() {
let result = MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com。,")
#expect(result?.url == "https://example.com")
#expect(result?.trailing == "。,")
}
@Test func stripsSentenceEnderFollowedByClosingBracket() {
let result = MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com/a。)")
#expect(result?.url == "https://example.com/a")
#expect(result?.trailing == "。)")
}
@Test func keepsCJKCharactersInsideThePath() {
#expect(MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com/文档") == nil)
}
@Test func keepsLoneFullwidthClosingBracket() {
// Brackets alone (no sentence-ender) may be part of the URL.
#expect(MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com/a)") == nil)
}
@Test func returnsNilForCleanURL() {
#expect(MarkdownTransforms.splitTrailingCJKPunctuation(fromAutolink: "https://example.com/a") == nil)
}
// MARK: - detectAutolinkRanges
@Test func detectsBareHTTPSURL() {
let text = "see https://example.com/x now"
let hits = MarkdownTransforms.detectAutolinkRanges(in: text)
#expect(hits.count == 1)
#expect(hits.first?.url == "https://example.com/x")
if let range = hits.first?.range {
#expect(String(text[range]) == "https://example.com/x")
}
}
@Test func detectsCustomSchemeURL() {
let hits = MarkdownTransforms.detectAutolinkRanges(in: "open obsidian://vault/Note here")
#expect(hits.first?.url == "obsidian://vault/Note")
}
@Test func stripsTrailingASCIIPunctuation() {
let hits = MarkdownTransforms.detectAutolinkRanges(in: "See obsidian://x.")
#expect(hits.first?.url == "obsidian://x")
}
@Test func keepsBalancedParensInURL() {
let hits = MarkdownTransforms.detectAutolinkRanges(in: "x obsidian://open?file=Note(1) y")
#expect(hits.first?.url == "obsidian://open?file=Note(1)")
}
@Test func stripsWikipediaStyleTrailingParen() {
let hits = MarkdownTransforms.detectAutolinkRanges(
in: "(https://en.wikipedia.org/wiki/Foo_(bar)) end"
)
#expect(hits.first?.url == "https://en.wikipedia.org/wiki/Foo_(bar)")
}
@Test func stripsTrailingCJKFromDetectedURL() {
// Sentence-ending punctuation right after the URL (before whitespace
// or end of text) must stay out of the link. Note: with NO whitespace
// after the punctuation, GFM swallows everything into the URL — that
// (web-matching) behavior is intentional and not stripped here.
let hits = MarkdownTransforms.detectAutolinkRanges(in: "见 https://example.com, 谢谢")
#expect(hits.count == 1)
#expect(hits.first?.url == "https://example.com")
let atEnd = MarkdownTransforms.detectAutolinkRanges(in: "见 https://example.com。")
#expect(atEnd.first?.url == "https://example.com")
}
@Test func ignoresSchemelessAndNonURIText() {
#expect(MarkdownTransforms.detectAutolinkRanges(in: "no links here").isEmpty)
#expect(MarkdownTransforms.detectAutolinkRanges(in: "javascript:alert(1)").isEmpty)
#expect(MarkdownTransforms.detectAutolinkRanges(in: "a ratio of 3://").isEmpty)
}
}