From 6292495ce77b88361dced566adef8d17c945c65e Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 15:46:36 +0100 Subject: [PATCH 001/105] revert: spacing around blocks back to 1.14.2 Reverts 185c99d. Trimming the blank lines at the ends of a text run made every heading sit 8 pt from its text and from the section above, closer than paragraphs sit to each other (28 pt): documents looked cramped. The blank lines in the source supplied that air for years, so they do again until headings get margins of their own. The parser is byte-identical to 1.14.2. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 28 ++--- .../BlockHeightMeasurerTests.swift | 15 --- QuickMD/QuickMDTests/ParserTests.swift | 101 ------------------ 3 files changed, 8 insertions(+), 136 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 850d4c4..8ad9ab6 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -846,7 +846,7 @@ struct MarkdownBlockParser: Sendable { // blockquotes, a heading and a code fence, …) used to flush an EMPTY // text block that rendered as a ~42 pt gap. Drop it: block spacing is // the stack's job, not the parser's. - guard buffer.contains(where: { !Self.isBlank($0.line) }) else { + guard buffer.contains(where: { !$0.line.trimmingCharacters(in: .whitespaces).isEmpty }) else { buffer.removeAll() return } @@ -874,28 +874,16 @@ struct MarkdownBlockParser: Sendable { } let chunk = buffer[chunkStart.. [(text: String, line: Int)] { - parse(markdown).compactMap { block in - plainText(block).map { ($0, block.sourceLine) } - } - } - - /// The renderer's string for `markdown` — what a text block holding exactly - /// those lines shows. - private func rendered(_ markdown: String) -> String { - String(MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)).render(markdown).characters) - } - - func testBlankLineAfterHeadingAddsNoEmptyParagraph() { - let loose = textBlocks("## Para\n\nText after blank.") - let tight = textBlocks("## Para\nText after blank.") - XCTAssertEqual(loose.map(\.text), tight.map(\.text)) - XCTAssertEqual(loose.map(\.text), [rendered("Text after blank.")]) - XCTAssertEqual(loose.map(\.line), [2], "the paragraph's own line, not the blank above it") - XCTAssertEqual(tight.map(\.line), [1]) - } - - func testBlankLinesAroundAListAddNoEmptyParagraphs() { - let loose = textBlocks("## Tight\n\n- one\n- two\n\n\n## Next") - let tight = textBlocks("## Tight\n- one\n- two\n## Next") - XCTAssertEqual(loose.map(\.text), tight.map(\.text)) - XCTAssertEqual(loose.map(\.text), [rendered("- one\n- two")]) - XCTAssertEqual(loose.map(\.line), [2]) - } - - func testTrailingBlankLinesBeforeTheNextBlockAreDropped() { - // Whitespace-only lines are blank too; the document ends in blanks. - for markdown in ["Text.\n\n## Next", "Text.\n \n\t\n```\ncode\n```", "Text.\n\n\n"] { - let blocks = textBlocks(markdown) - XCTAssertEqual(blocks.map(\.text), [rendered("Text.")], markdown.debugDescription) - XCTAssertEqual(blocks.map(\.line), [0], markdown.debugDescription) - } - } - - func testLeadingWhitespaceOnlyLinesAreDropped() { - let blocks = textBlocks("> quote\n \n\n\t\nText.") - XCTAssertEqual(blocks.map(\.text), [rendered("Text.")]) - XCTAssertEqual(blocks.map(\.line), [4]) - } - - func testBlankLinesInsideATextBlockStay() { - // Paragraph separation inside the block is exactly what it was. - let inner = "First paragraph.\n\nSecond paragraph.\n\n\n- item\n\nLast." - let blocks = textBlocks("# H\n\n" + inner + "\n\n# Next") - XCTAssertEqual(blocks.map(\.text), [rendered(inner)]) - XCTAssertEqual(blocks.map(\.line), [2]) - } - - func testCRLFInputTrimsLikeLF() { - let lf = "## X\n\nText.\n\n- a\n- b\n\n## Y\n" - let crlf = lf.replacingOccurrences(of: "\n", with: "\r\n") - let normalized = textBlocks(MarkdownDocument.normalizeLineEndings(crlf)) - let expected = textBlocks(lf) - XCTAssertEqual(normalized.map(\.text), expected.map(\.text)) - XCTAssertEqual(normalized.map(\.line), expected.map(\.line)) - XCTAssertEqual(expected.map(\.text), [rendered("Text.\n\n- a\n- b")]) - } - - func testDocumentOfOnlyBlankLinesHasNoBlocks() { - for markdown in ["", "\n", "\n\n\n", " \n\t\n ", - MarkdownDocument.normalizeLineEndings("\r\n\r\n")] { - XCTAssertTrue(parse(markdown).isEmpty, markdown.debugDescription) - } - } - - func testChunksNeitherStartNorEndWithBlankLines() { - // 80 paragraphs separated by runs of 1–3 blank lines (some - // whitespace-only): the run is cut into several chunks, the cuts land - // on blank lines, and no chunk may carry them at either end. - var lines: [String] = [] - for n in 1...80 { - lines.append("paragraph \(n)") - for blank in 0..<(n % 3 + 1) { lines.append(blank == 1 ? " " : "") } - } - let blocks = textBlocks("# H\n" + lines.joined(separator: "\n")) - XCTAssertGreaterThan(blocks.count, 1, "the run must be chunked") - for block in blocks { - XCTAssertFalse(block.text.hasPrefix("\n"), "chunk at line \(block.line) starts blank") - XCTAssertFalse(block.text.hasSuffix("\n\n"), "chunk at line \(block.line) ends blank") - XCTAssertTrue(block.text.hasPrefix("paragraph "), "line \(block.line)") - XCTAssertEqual(lines[block.line - 1], block.text.components(separatedBy: "\n")[0], - "sourceLine is the first line the chunk renders") - } - // Every paragraph is still rendered exactly once. - let paragraphs = blocks.flatMap { $0.text.components(separatedBy: "\n") } - .filter { $0.hasPrefix("paragraph ") } - XCTAssertEqual(paragraphs, (1...80).map { "paragraph \($0)" }) - } - func testHeadingStoredSourceLineMatchesAssociatedValue() { // 0 `[ref]: …` (filtered), 1 blank, 2 `# ATX`, 3 `body`, 4 blank, // 5 `Setext`, 6 `------` From 27ce616cce880cbaab3894bf5e7bd0a99935893e Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 15:50:03 +0100 Subject: [PATCH 002/105] release: v1.14.4 (build 30) Spacing around headings and lists is back to 1.14.2: the 1.14.3 trim of blank lines around blocks is reverted. The formula fallback and the App Store rating prompt from 1.14.3 stay. Co-Authored-By: Claude Opus 5.5 --- QuickMD/CHANGELOG.md | 7 +++++++ QuickMD/QuickMD.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/QuickMD/CHANGELOG.md b/QuickMD/CHANGELOG.md index 4132ddb..72ef356 100644 --- a/QuickMD/CHANGELOG.md +++ b/QuickMD/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to QuickMD will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.14.4] - 2026-11-04 + +Spacing around headings is back. + +### Fixed +- Headings and lists have the spacing they had before 1.14.3 again. The 1.14.3 change that dropped the blank lines around blocks left headings too close to their text, so documents looked cramped; it is withdrawn. The other 1.14.3 changes stay. + ## [1.14.3] - 2026-11-04 Two rendering fixes. diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 286ee44..8e59032 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -891,7 +891,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 29; + CURRENT_PROJECT_VERSION = 30; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VJ92852M32; ENABLE_APP_SANDBOX = YES; @@ -916,7 +916,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.14.3; + MARKETING_VERSION = 1.14.4; PRODUCT_BUNDLE_IDENTIFIER = pl.falami.studio.QuickMD; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -935,7 +935,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 29; + CURRENT_PROJECT_VERSION = 30; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VJ92852M32; ENABLE_APP_SANDBOX = YES; @@ -960,7 +960,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.14.3; + MARKETING_VERSION = 1.14.4; PRODUCT_BUNDLE_IDENTIFIER = pl.falami.studio.QuickMD; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 69757f14964a9a96c48522009381bd64ff529717 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 17:39:56 +0100 Subject: [PATCH 003/105] Parser: one text row per CommonMark block A text run (everything between two non-text blocks) used to become rows of up to 30 raw lines, blank lines included. Every blank line rendered as two attribute-less "\n" paragraphs (Helvetica 12, 14 pt each, never scaled with zoom), and every row ended in a line break that laid out an empty line under it. So the space between two paragraphs depended on the blank lines in the source and on where the 30-line cut fell. Now a run is cut into rows the way CommonMark reads it: - a blank line ends a block (a paragraph, an item of a loose list); - a list item right after paragraph text starts a block (the list); - a thematic break is a block of its own. Blank lines never reach a row, and a text row never ends in "\n". A block longer than textChunkLineLimit (30 joined lines) is still cut, and every row after the cut is marked MarkdownBlock.joinsPrevious, as are the second and later nesting groups of one `>` run. Soft breaks are joined and the ordered-list hangs computed over the WHOLE run before it is cut (MarkdownRenderer.joinSoftBreaksIndexed, render(joined:hangs:)), so a loose ordered list split into one row per item keeps one text edge. ParserTests: three paragraphs are now three rows, and a definition list row has five lines, not six (no trailing line break). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlock.swift | 38 ++++++- QuickMD/QuickMD/MarkdownBlockParser.swift | 122 +++++++++++++--------- QuickMD/QuickMD/MarkdownRenderer.swift | 43 ++++++-- QuickMD/QuickMDTests/ParserTests.swift | 9 +- 4 files changed, 148 insertions(+), 64 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift index a305707..2e11277 100644 --- a/QuickMD/QuickMD/MarkdownBlock.swift +++ b/QuickMD/QuickMD/MarkdownBlock.swift @@ -110,6 +110,24 @@ struct MarkdownBlock: Identifiable, Sendable { /// `.heading(level:title:sourceLine:)`, which section copy (`SectionExtractor`) /// reads — that associated value stays the authority there. let sourceLine: Int + /// This row continues the block of the row before it: there is no block + /// boundary between them, only a cut the parser made. Two cases: + /// - a text run longer than `MarkdownBlockParser.textChunkLineLimit` lines, + /// cut to keep every row cheap to lay out; + /// - the second and later nesting groups of ONE `>` run (`> a` then + /// `>> b`), which render as separate quote rows. + /// + /// Such a row gets no space above it (`BlockLayout.Spacing`), and a copy + /// across the cut joins the two rows with a line break, not a blank line + /// (`DocumentCopyBuilder`). + let joinsPrevious: Bool + + init(id: String, content: BlockContent, sourceLine: Int, joinsPrevious: Bool = false) { + self.id = id + self.content = content + self.sourceLine = sourceLine + self.joinsPrevious = joinsPrevious + } enum BlockContent: Sendable { case text(AttributedString) @@ -138,8 +156,18 @@ struct MarkdownBlock: Identifiable, Sendable { // Every factory takes `sourceLine` WITHOUT a default: the parser is the only // caller and it always knows the mapped original line. A default here would // silently produce 0-anchored blocks the moment a new parser branch forgets it. - static func text(index: Int, _ attributed: AttributedString, sourceLine: Int) -> MarkdownBlock { - MarkdownBlock(id: "text-\(index)", content: .text(attributed), sourceLine: sourceLine) + /// A text row never ends in a line break: the renderer ends every line + /// with "\n", and a final one would lay out an extra, empty line under the + /// row — in the system's default font, so it would not even scale with + /// zoom. The space under a row is `BlockLayout.Spacing`'s job. + static func text(index: Int, _ attributed: AttributedString, sourceLine: Int, + joinsPrevious: Bool = false) -> MarkdownBlock { + var text = attributed + while let last = text.characters.last, last == "\n" { + text.removeSubrange(text.characters.index(before: text.endIndex).. MarkdownBlock { @@ -153,8 +181,10 @@ struct MarkdownBlock: Identifiable, Sendable { static func image(index: Int, url: String, alt: String, width: ImageWidth?, sourceLine: Int) -> MarkdownBlock { MarkdownBlock(id: "image-\(index)", content: .image(url: url, alt: alt, width: width), sourceLine: sourceLine) } - static func blockquote(index: Int, content: String, level: Int, sourceLine: Int) -> MarkdownBlock { - MarkdownBlock(id: "blockquote-\(index)", content: .blockquote(content: content, level: level), sourceLine: sourceLine) + static func blockquote(index: Int, content: String, level: Int, sourceLine: Int, + joinsPrevious: Bool = false) -> MarkdownBlock { + MarkdownBlock(id: "blockquote-\(index)", content: .blockquote(content: content, level: level), + sourceLine: sourceLine, joinsPrevious: joinsPrevious) } static func alert(index: Int, kind: AlertKind, content: String, sourceLine: Int) -> MarkdownBlock { MarkdownBlock(id: "alert-\(index)", content: .alert(kind: kind, content: content), sourceLine: sourceLine) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 8ad9ab6..d3d33e6 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -330,6 +330,10 @@ struct MarkdownBlockParser: Sendable { // Blockquote detection - group consecutive > lines by nesting level if trimmed.hasPrefix(">") { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + // A later nesting group of this run continues the quote row + // before it (`MarkdownBlock.joinsPrevious`): `> a` then `>> b` + // is one quote on the page, drawn as two rows. + var previousGroupWasQuote = false while i < lines.count { let qLine = lines[i].trimmingCharacters(in: .whitespaces) @@ -384,8 +388,12 @@ struct MarkdownBlockParser: Sendable { let body = groupLines.dropFirst().joined(separator: "\n") .trimmingCharacters(in: .whitespacesAndNewlines) blocks.append(.alert(index: blockIndex, kind: kind, content: body, sourceLine: groupSourceLine)) + previousGroupWasQuote = false } else { - blocks.append(.blockquote(index: blockIndex, content: content, level: groupLevel, sourceLine: groupSourceLine)) + blocks.append(.blockquote(index: blockIndex, content: content, level: groupLevel, + sourceLine: groupSourceLine, + joinsPrevious: previousGroupWasQuote)) + previousGroupWasQuote = true } blockIndex += 1 } @@ -833,60 +841,78 @@ struct MarkdownBlockParser: Sendable { // MARK: - Text Buffer - /// Cap on lines per text block. Large bullet/link lists (e.g. 11K-line bookmark dumps) - /// would otherwise coalesce into one ~10000-char Text(AttributedString) per heading - /// section — slow to lay out on window resize. We split at blank-line (paragraph) - /// boundaries so visual rendering is unaffected, but each individual block stays - /// small enough for SwiftUI Text to lay out quickly. - private static let textChunkLineLimit = 30 - + /// Cap on joined lines per text row. Large bullet/link lists (e.g. 11K-line + /// bookmark dumps) would otherwise become one ~10000-character row per + /// heading section — slow to lay out on window resize. A longer block is + /// cut into rows of this many lines; every row after the first is marked + /// `joinsPrevious`, so the cut adds no space and copies as a plain line break. + static let textChunkLineLimit = 30 + + /// Turns a run of text lines (everything between two non-text blocks) into + /// rows — ONE ROW PER BLOCK, as CommonMark reads the run: + /// - a blank line ends a block (a paragraph, an item of a loose list); + /// - a list item right after paragraph text starts a block (the list); + /// - a thematic break is a block of its own. + /// + /// Blank lines themselves never reach a row: the space between two blocks + /// is `BlockLayout.Spacing`'s, the same whether the source has a blank line + /// there or not. + /// + /// Soft breaks are joined and the ordered-list hangs computed over the WHOLE + /// run before it is cut, so a list split into several rows (a loose list, + /// or one past the line cap) keeps one text edge. private func flushTextBuffer(_ buffer: inout [(line: String, originalIndex: Int)], to blocks: inout [MarkdownBlock], index: inout Int, using renderer: MarkdownRenderer) { guard !buffer.isEmpty else { return } - // A buffer of nothing but blank lines (the separator between two - // blockquotes, a heading and a code fence, …) used to flush an EMPTY - // text block that rendered as a ~42 pt gap. Drop it: block spacing is - // the stack's job, not the parser's. - guard buffer.contains(where: { !$0.line.trimmingCharacters(in: .whitespaces).isEmpty }) else { - buffer.removeAll() - return + defer { buffer = [] } + + let joined = MarkdownRenderer.joinSoftBreaksIndexed(buffer.map(\.line)) + let hangs = renderer.widestOrderedPrefixes(joined.map(\.line)) + + var row: [String] = [] + var rowHangs: [String?] = [] + var rowSourceLine = 0 + var rowJoinsPrevious = false + // The current block is a list: an item started it or continues it. + var inList = false + + func flushRow() { + guard !row.isEmpty else { return } + blocks.append(.text(index: index, renderer.render(joined: row, hangs: rowHangs), + sourceLine: rowSourceLine, joinsPrevious: rowJoinsPrevious)) + index += 1 + row = [] + rowHangs = [] + rowJoinsPrevious = false + } + func append(_ position: Int) { + if row.isEmpty { rowSourceLine = buffer[joined[position].first].originalIndex } + row.append(joined[position].line) + rowHangs.append(hangs[position]) } - // Slice the buffer into chunks of ≤ textChunkLineLimit lines, preferring - // blank-line boundaries so we never split a paragraph. - var chunkStart = 0 - while chunkStart < buffer.count { - let hardEnd = min(chunkStart + Self.textChunkLineLimit, buffer.count) - var sliceEnd = hardEnd - - // If we're not at the end of the buffer, try to back up to the last blank - // line within this chunk so we don't split a paragraph mid-flow. - if hardEnd < buffer.count { - var probe = hardEnd - 1 - while probe > chunkStart { - if buffer[probe].line.trimmingCharacters(in: .whitespaces).isEmpty { - sliceEnd = probe + 1 // include the blank line in this chunk - break - } - probe -= 1 - } - // If no blank line found inside the window, fall through with hardEnd — - // the chunk is one long paragraph and we just have to take the hit. + for position in joined.indices { + let line = joined[position].line + if Self.isBlank(line) { + flushRow() + inList = false + continue } - - let chunk = buffer[chunkStart..= Self.textChunkLineLimit { + flushRow() + rowJoinsPrevious = true + } + append(position) + if isItem { inList = true } } - buffer = [] + flushRow() } } diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 05f8053..6e38fa2 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -167,19 +167,26 @@ struct MarkdownRenderer: Sendable { // MARK: - Main Render func render(_ markdown: String) -> AttributedString { - var result = AttributedString() - let lines = Self.joinSoftBreaks(markdown.components(separatedBy: "\n")) // Ordered items of one list line up on the WIDEST marker of that list: // the body font is not monospaced, so "1. " and "10. " have different // widths and each item's text would otherwise start at a different x. // The run is known before any rendering, so the widest prefix is too. - let hangs = widestOrderedPrefixes(lines) + return render(joined: lines, hangs: widestOrderedPrefixes(lines)) + } + + /// Renders lines that are ALREADY soft-break joined (`joinSoftBreaks`), + /// each ordered item hanging under `hangs[i]` (`widestOrderedPrefixes`). + /// + /// The parser's entry point: it joins and computes the hangs over a whole + /// text run, then renders the run one block at a time — so the items of a + /// loose list, one row each, still share their list's widest marker. + func render(joined lines: [String], hangs: [String?]) -> AttributedString { + var result = AttributedString() for (index, line) in lines.enumerated() { - result.append(renderLine(line, hangingPrefix: hangs[index])) + result.append(renderLine(line, hangingPrefix: index < hangs.count ? hangs[index] : nil)) result.append(AttributedString("\n")) } - return result } @@ -253,13 +260,19 @@ struct MarkdownRenderer: Sendable { /// spaces, or a trailing backslash — still ends the visual line, and /// headers, rules, and blanks always keep their own line. static func joinSoftBreaks(_ lines: [String]) -> [String] { - var joined: [String] = [] + joinSoftBreaksIndexed(lines).map(\.line) + } + + /// `joinSoftBreaks`, with the index in `lines` of each joined line's FIRST + /// input line — how the parser maps a row back to its source line. + static func joinSoftBreaksIndexed(_ lines: [String]) -> [(line: String, first: Int)] { + var joined: [(line: String, first: Int)] = [] var openParagraph = false // last joined line can absorb a continuation - for line in lines { + for (index, line) in lines.enumerated() { let kind = classify(line) guard kind != .structural else { - joined.append(line) + joined.append((line, index)) openParagraph = false continue } @@ -270,9 +283,9 @@ struct MarkdownRenderer: Sendable { if hardBreak && text.hasSuffix("\\") { text.removeLast() } if kind == .paragraph && openParagraph { - joined[joined.count - 1] += " " + text.trimmingCharacters(in: .whitespaces) + joined[joined.count - 1].line += " " + text.trimmingCharacters(in: .whitespaces) } else { - joined.append(text) + joined.append((text, index)) } openParagraph = !hardBreak } @@ -291,6 +304,16 @@ struct MarkdownRenderer: Sendable { case paragraph } + /// A bullet, ordered or task list item — `renderLine` renders it as one. + /// The parser starts a new row at an item that follows paragraph text. + static func isListItem(_ line: String) -> Bool { classify(line) == .listItem } + + /// A thematic break (`---`, `***`, `___`) — `renderLine` draws it as a rule. + static func isThematicBreak(_ line: String) -> Bool { + line.trimmingCharacters(in: .whitespaces) + .range(of: MarkdownTheme.horizontalRulePattern, options: .regularExpression) != nil + } + /// Must mirror the dispatch in `renderLine`, or a structural line could /// get glued into the paragraph before it. private static func classify(_ line: String) -> LineKind { diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 3dfbe10..2b0f9e3 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -548,7 +548,10 @@ final class ParserTests: XCTestCase { ![nope][] """) XCTAssertTrue(images(blocks).isEmpty) - XCTAssertEqual(blocks.count, 1, "all three lines stay paragraph text") + // Three paragraphs, so three text rows: since the vertical rhythm a row + // is one block (it used to be every text line up to the 30-line cap). + XCTAssertEqual(blocks.count, 3, "all three lines stay paragraph text") + XCTAssertTrue(blocks.allSatisfy { $0.id.hasPrefix("text-") }, "\(blocks.map(\.id))") } func testShortcutImageWithoutAnyDefinitionsStaysText() { @@ -922,8 +925,10 @@ final class ParserTests: XCTestCase { XCTAssertEqual(blocks.count, 1, "\(blocks.map(\.id))") XCTAssertEqual(textLines(blocks[0]), ["Apple", "fruit", "Orange", "citrus"]) // …separated by a blank line, so the groups read apart on screen and in print. + // Five lines, not six: a text row no longer ends in a line break (the + // final "\n" laid out an empty line under every row). let raw = (plainText(blocks[0]) ?? "").components(separatedBy: "\n") - XCTAssertEqual(raw.count, 6, "\(raw)") + XCTAssertEqual(raw.count, 5, "\(raw)") XCTAssertTrue(raw[2].isEmpty, "expected a blank line between groups: \(raw)") } From d64dc20f1e9381592ade8f9f3b65412c59e32d06 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 17:40:34 +0100 Subject: [PATCH 004/105] Vertical rhythm: one spacing system for every pair of blocks The space between two blocks came from four unrelated sources: an 8 pt intercell spacing, blank-line paragraphs set in Helvetica 12, the empty line under every text row, and the heading row's copy-button floor. It depended on the source's blank lines, ignored zoom in part, and the same pair of blocks could be 20 or 48 pt apart. BlockLayout.Spacing (VerticalRhythm.swift) replaces all of it. In units of the body size at the current zoom, u = 14 x fontScale: - 1.75u between any two non-heading blocks, edge to edge (line box or box edge), boxes included; - 2.25u under every heading, optical: last baseline to the next block's cap top or box edge, whatever block follows; - 2.0u for a heading directly under its parent heading; - above headings, optical: H1 4.75u, H2 4.25u, H3 3.75u, H4-H6 3.5u; - front matter to the title: 1.75u edge to edge; first row: 0; - 0 above a row that only continues the previous one (joinsPrevious). The measurer reads every row's edges from the layout it already runs: textInk returns the first cap top and the last baseline from the same TextKit pass as exactHeight, and a heading row is measured with its title inset and button floor (the old estimate left those to the report). MeasuredBlocks.rhythm keeps the inputs so measureMathRows can update the rows it finishes and derive the column again; a height report changes only the block's height (replacingHeight). The list: each row is the space above its block plus the block (BlockHeightTable.rowHeight); intercell spacing is 0 and the content inset is exactly the style's vertical padding. RowGeometry puts every position on the BLOCK: `.top` jumps (ToC, links, Source Edit landing) land on its top, `.center` (search) centres it, the scroll anchor is relative to it (stable across zoom, theme and width), height-report compensation tests the block's top, the selection tint covers the block only, an atomic row splits at its block's middle, and a point in the gap belongs to the nearer block. Document.blockSpacing is gone; the image caption's 8 pt is ImageBlock.captionSpacing. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 6 + QuickMD/QuickMD/BlockHeightMeasurer.swift | 213 +++++++++++++++--- QuickMD/QuickMD/MarkdownView.swift | 10 +- QuickMD/QuickMD/VerticalRhythm.swift | 222 +++++++++++++++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 193 +++++++++------- 5 files changed, 523 insertions(+), 121 deletions(-) create mode 100644 QuickMD/QuickMD/VerticalRhythm.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 8e59032..f44c862 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ A1B2C3D401000201 /* PrintableTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000200 /* PrintableTableTests.swift */; }; A1B2C3D4010001C0 /* SelectionRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; + A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A0 /* VerticalRhythm.swift */; }; + A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A0 /* VerticalRhythm.swift */; }; A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C1 /* TableSelectionTests.swift */; }; A1B2C3D40100000E /* ImageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000012 /* ImageBlockView.swift */; }; A1B2C3D40100000F /* BlockquoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000013 /* BlockquoteView.swift */; }; @@ -228,6 +230,7 @@ A1B2C3D400000232 /* ReviewPromptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptTests.swift; sourceTree = ""; }; A1B2C3D400000200 /* PrintableTableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintableTableTests.swift; sourceTree = ""; }; A1B2C3D4000001C0 /* SelectionRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRows.swift; sourceTree = ""; }; + A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; A1B2C3D4000001C1 /* TableSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSelectionTests.swift; sourceTree = ""; }; A1B2C3D400000012 /* ImageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlockView.swift; sourceTree = ""; }; A1B2C3D400000013 /* BlockquoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockquoteView.swift; sourceTree = ""; }; @@ -377,6 +380,7 @@ A1B2C3D400000081 /* ImageLoader.swift */, A1B2C3D4000000C0 /* DocumentSelection.swift */, A1B2C3D4000001C0 /* SelectionRows.swift */, + A1B2C3D4000002A0 /* VerticalRhythm.swift */, A1B2C3D400000110 /* DocumentReadingPosition.swift */, A1B2C3D400000120 /* DocumentFileFormat.swift */, A1B2C3D400000121 /* DocumentFileWriter.swift */, @@ -674,6 +678,7 @@ A1B2C3D401000082 /* ImageLoader.swift in Sources */, A1B2C3D4010000C0 /* DocumentSelection.swift in Sources */, A1B2C3D4010001C0 /* SelectionRows.swift in Sources */, + A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */, A1B2C3D401000110 /* DocumentReadingPosition.swift in Sources */, A1B2C3D401000120 /* DocumentFileFormat.swift in Sources */, A1B2C3D401000122 /* DocumentFileWriter.swift in Sources */, @@ -726,6 +731,7 @@ A1B2C3D4010000E2 /* HTMLImageTests.swift in Sources */, A1B2C3D4010000C1 /* DocumentSelection.swift in Sources (Tests) */, A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */, + A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */, A1B2C3D4010000C2 /* DocumentSelectionTests.swift in Sources */, A1B2C3D401000111 /* DocumentReadingPosition.swift in Sources (Tests) */, A1B2C3D401000112 /* ExternalEditorLineTests.swift in Sources */, diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index b1f9b5e..f4a4432 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -65,14 +65,14 @@ enum BlockLayout { // MARK: Document chrome (MarkdownView) - /// Padding and spacing `MarkdownView` puts around and between blocks. + /// Padding `MarkdownView` puts around the document and around blocks. /// The per-kind `…OuterVerticalPadding` values are the `.padding(.vertical:)` /// modifiers applied in `MarkdownView.blockView(for:)`; they are part of a - /// row's height because the virtualized list hosts that whole view. + /// row's height because the virtualized list hosts that whole view. The + /// space BETWEEN blocks is `BlockLayout.Spacing` (VerticalRhythm.swift). enum Document { static let contentHorizontalPadding: CGFloat = 32 static let contentVerticalPadding: CGFloat = 24 - static let blockSpacing: CGFloat = 8 // MARK: Reading mode (v1.9) // @@ -404,6 +404,10 @@ enum BlockLayout { /// the bitmap is decoded, so this is what a measured row starts at. static let placeholderHeight: CGFloat = 100 static let maxDisplayWidth: CGFloat = 600 + /// Between an image and its italic alt-text caption. `ImageBlockView`'s + /// body is a two-view tuple, flattened into the row's `VStack`, so this + /// is that stack's spacing (the 8 pt the 1.8.0 document stack had). + static let captionSpacing: CGFloat = 8 /// Scale the fitted 100% width, then keep the result inside the column. static func displayWidth(fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat { @@ -690,15 +694,42 @@ enum RowKind: Equatable { /// One height per block, in block order, for a specific content width. struct BlockHeightTable: Equatable { + /// The BLOCK's height: what its view lays out at, what a `.reported` row + /// reports and what the parity tests pin. The list row is taller by + /// `spacingAbove` (see `rowHeight`). let heights: [CGFloat] let kinds: [RowKind] /// The width the heights were measured at. A row height is only valid for /// this width — the list re-measures when it changes. let contentWidth: CGFloat + /// The space above each block (`BlockLayout.Spacing`; row 0: 0). Part of + /// the block's list row, above its view. + let spacingAbove: [CGFloat] var count: Int { heights.count } - static let empty = BlockHeightTable(heights: [], kinds: [], contentWidth: 0) + /// The space above row `row`'s block; 0 outside the table. + func spacing(_ row: Int) -> CGFloat { + row >= 0 && row < spacingAbove.count ? spacingAbove[row] : 0 + } + + /// The list row's height: the space above the block, then the block. + func rowHeight(_ row: Int) -> CGFloat { + row >= 0 && row < heights.count ? spacing(row) + heights[row] : 0 + } + + /// This table with row `row`'s block height replaced — a `.reported` row + /// measured itself. Only the block changes: its space above, and every + /// other row, stay as measured. + func replacingHeight(_ height: CGFloat, at row: Int) -> BlockHeightTable { + guard row >= 0, row < heights.count else { return self } + var heights = heights + heights[row] = height + return BlockHeightTable(heights: heights, kinds: kinds, contentWidth: contentWidth, + spacingAbove: spacingAbove) + } + + static let empty = BlockHeightTable(heights: [], kinds: [], contentWidth: 0, spacingAbove: []) } /// `measure`'s output: the height table plus the NSAttributedStrings that had to @@ -727,8 +758,20 @@ struct MeasuredBlocks: @unchecked Sendable { /// main actor before using the table for layout. Empty whenever `measure` /// got a real engine. let mathPendingRows: [Int] + /// Every row's input to `BlockLayout.Spacing` — what `table.spacingAbove` + /// was derived from, kept so `measureMathRows` can update the rows it + /// finishes and derive the column again without measuring anything else. + let rhythm: [BlockLayout.Spacing.Row] + + /// This measurement with row `row`'s block height replaced (see + /// `BlockHeightTable.replacingHeight`). + func replacingHeight(_ height: CGFloat, at row: Int) -> MeasuredBlocks { + MeasuredBlocks(table: table.replacingHeight(height, at: row), converted: converted, + tables: tables, mathPendingRows: mathPendingRows, rhythm: rhythm) + } - static let empty = MeasuredBlocks(table: .empty, converted: [:], tables: [:], mathPendingRows: []) + static let empty = MeasuredBlocks(table: .empty, converted: [:], tables: [:], mathPendingRows: [], + rhythm: []) } // MARK: - Measurer @@ -749,6 +792,42 @@ enum BlockHeightMeasurer { /// container height, `ensureLayout`, `ceil(usedRect.height)`. Anything else /// here breaks parity. static func exactHeight(text ns: NSAttributedString, width: CGFloat) -> CGFloat { + let (layoutManager, container, storage) = laidOut(ns, width: width) + return withExtendedLifetime(storage) { ceil(layoutManager.usedRect(for: container).height) } + } + + /// `exactHeight`, plus where the text's ink starts and ends — the edges + /// `BlockLayout.Spacing` measures optical gaps from. ONE layout pass: the + /// baselines are read from the layout `exactHeight` computes anyway. + static func textInk(text ns: NSAttributedString, width: CGFloat) -> BlockLayout.Spacing.TextInk { + let (layoutManager, container, storage) = laidOut(ns, width: width) + return withExtendedLifetime(storage) { + let height = ceil(layoutManager.usedRect(for: container).height) + let glyphCount = layoutManager.numberOfGlyphs + guard glyphCount > 0 else { + return .init(height: height, firstBaseline: 0, firstCapHeight: 0, lastBaseline: height) + } + func baseline(ofGlyph glyph: Int) -> CGFloat { + layoutManager.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil).minY + + layoutManager.location(forGlyphAt: glyph).y + } + var firstLine = NSRange() + _ = layoutManager.lineFragmentRect(forGlyphAt: 0, effectiveRange: &firstLine) + let firstCharacters = layoutManager.characterRange(forGlyphRange: firstLine, actualGlyphRange: nil) + var capHeight: CGFloat = 0 + storage.enumerateAttribute(.font, in: firstCharacters, options: []) { value, _, _ in + if let font = value as? NSFont { capHeight = max(capHeight, font.capHeight) } + } + return .init(height: height, firstBaseline: baseline(ofGlyph: 0), firstCapHeight: capHeight, + lastBaseline: baseline(ofGlyph: glyphCount - 1)) + } + } + + /// The TextKit 1 stack both functions above lay `ns` out on. The storage + /// is returned too, and the caller must keep it alive while it reads the + /// layout: the layout manager does not retain it. + private static func laidOut(_ ns: NSAttributedString, width: CGFloat) + -> (NSLayoutManager, NSTextContainer, NSTextStorage) { let storage = NSTextStorage(attributedString: ns) let layoutManager = NSLayoutManager() let container = NSTextContainer(size: NSSize(width: max(0, width), @@ -759,7 +838,7 @@ enum BlockHeightMeasurer { layoutManager.addTextContainer(container) storage.addLayoutManager(layoutManager) layoutManager.ensureLayout(for: container) - return ceil(layoutManager.usedRect(for: container).height) + return (layoutManager, container, storage) } /// Measures every block for one `(blocks, theme, fontScale, contentWidth)` @@ -823,30 +902,39 @@ enum BlockHeightMeasurer { var heights: [CGFloat] = [] var kinds: [RowKind] = [] + var rhythm: [BlockLayout.Spacing.Row] = [] var converted: [String: NSAttributedString] = [:] var tables: [String: TableText] = [:] var mathPendingRows: [Int] = [] heights.reserveCapacity(blocks.count) kinds.reserveCapacity(blocks.count) + rhythm.reserveCapacity(blocks.count) - for block in blocks { + for (row, block) in blocks.enumerated() { + // Text and headings have their edges measured below; every other + // kind's are fixed by its view (`boxRhythm`). + var textEdges: BlockLayout.Spacing.Edges? switch block.content { case .text(let attributed): let hasMath = BlockTextConverter.containsInlineMath(String(attributed.characters)) if hasMath, math == nil { // Building the string means rendering `$…$` segments through - // the math engine, which this thread may not touch. + // the math engine, which this thread may not touch. The + // edges are a placeholder too, until `measureMathRows`. mathPendingRows.append(heights.count) heights.append(pendingParagraphHeight) kinds.append(.exact) + textEdges = .box(top: 0, bottom: 0) } else { let ns = BlockTextConverter.makeNSAttributedString( from: attributed, hasInlineMath: hasMath, theme: theme, fontScale: fontScale, math: engine) converted[block.id] = ns - heights.append(exactHeight(text: ns, width: textWidth)) + let ink = textInk(text: ns, width: textWidth) + heights.append(ink.height) kinds.append(.exact) + textEdges = .text(ink) } case .blockquote(let content, let level): @@ -886,9 +974,11 @@ enum BlockHeightMeasurer { kinds.append(.exact) case .heading(let level, let title, _): - heights.append(headingEstimate(title: title, level: level, - renderer: renderer, contentWidth: contentWidth)) + let heading = headingRow(title: title, level: level, + renderer: renderer, contentWidth: contentWidth) + heights.append(heading.height) kinds.append(.reported) + textEdges = heading.edges case .table(let headers, let rows, let alignments, let style): // The string `TableTextView` installs (it is handed this very @@ -934,6 +1024,7 @@ enum BlockHeightMeasurer { + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) kinds.append(.reported) } + rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } ?? boxRhythm(block, row: row)) } #if DEBUG @@ -943,10 +1034,50 @@ enum BlockHeightMeasurer { #endif return MeasuredBlocks( - table: BlockHeightTable(heights: heights, kinds: kinds, contentWidth: contentWidth), + table: BlockHeightTable(heights: heights, kinds: kinds, contentWidth: contentWidth, + spacingAbove: BlockLayout.Spacing.column(rhythm, fontScale: fontScale)), converted: converted, tables: tables, - mathPendingRows: mathPendingRows) + mathPendingRows: mathPendingRows, + rhythm: rhythm) + } + + // MARK: - Rhythm inputs + + /// A text or heading row's rhythm input, from its measured edges. + private static func rhythmRow(_ block: MarkdownBlock, edges: BlockLayout.Spacing.Edges) + -> BlockLayout.Spacing.Row { + let role: BlockLayout.Spacing.Role + if case .heading(let level, _, _) = block.content { + role = .heading(level: level) + } else { + role = .text + } + return BlockLayout.Spacing.Row(role: role, edges: edges, joinsPrevious: block.joinsPrevious) + } + + /// The rhythm input of a row whose edges its view fixes: the vertical + /// padding between the row's edge and the visible box (or the picture). + /// `row` decides front matter: a properties table counts as front matter + /// only as the document's first block. + private static func boxRhythm(_ block: MarkdownBlock, row: Int) -> BlockLayout.Spacing.Row { + typealias Document = BlockLayout.Document + let pad: CGFloat + var role = BlockLayout.Spacing.Role.box + switch block.content { + case .blockquote: pad = BlockLayout.Quote.verticalPadding + case .alert: pad = Document.alertOuterVerticalPadding + case .codeBlock: pad = Document.codeOuterVerticalPadding + case .table(_, _, _, let style): + pad = Document.tableOuterVerticalPadding + if style == .properties, row == 0 { role = .frontMatter } + case .mathBlock: pad = Document.mathOuterVerticalPadding + BlockLayout.Math.verticalPadding + case .mermaidDiagram: pad = Document.mermaidOuterVerticalPadding + BlockLayout.Mermaid.verticalPadding + case .image, .svgImage: pad = Document.imageOuterVerticalPadding + case .text, .heading: pad = 0 // measured, never here + } + return BlockLayout.Spacing.Row(role: role, edges: .box(top: pad, bottom: pad), + joinsPrevious: block.joinsPrevious) } /// One `.codeBlock` row: the code laid out at `codeWidth` plus `chrome` @@ -1007,7 +1138,7 @@ enum BlockHeightMeasurer { reusing: [String: NSAttributedString] = [:]) -> MeasuredBlocks { guard !pending.isEmpty else { return MeasuredBlocks(table: measured.table, converted: measured.converted, - tables: measured.tables, mathPendingRows: []) + tables: measured.tables, mathPendingRows: [], rhythm: measured.rhythm) } #if DEBUG @@ -1020,6 +1151,11 @@ enum BlockHeightMeasurer { var heights = measured.table.heights var kinds = measured.table.kinds var converted = measured.converted + // Row for row with `blocks` whenever `measured` came from them; the + // guard only keeps a mismatched call from trapping. + var rhythm = measured.rhythm.count == blocks.count + ? measured.rhythm + : blocks.enumerated().map { boxRhythm($0.element, row: $0.offset) } let textWidth = BlockLayout.clampedWidth(contentWidth) for index in pending { @@ -1033,7 +1169,9 @@ enum BlockHeightMeasurer { // this string are still the right images and only the wrap // has to be recomputed. converted[block.id] = existing - heights[index] = exactHeight(text: existing, width: textWidth) + let ink = textInk(text: existing, width: textWidth) + heights[index] = ink.height + rhythm[index] = rhythmRow(block, edges: .text(ink)) continue } // Re-derive the flag rather than assuming it: the row is pending @@ -1044,7 +1182,9 @@ enum BlockHeightMeasurer { from: attributed, hasInlineMath: hasMath, theme: theme, fontScale: fontScale, math: math) converted[block.id] = ns - heights[index] = exactHeight(text: ns, width: textWidth) + let ink = textInk(text: ns, width: textWidth) + heights[index] = ink.height + rhythm[index] = rhythmRow(block, edges: .text(ink)) case .mathBlock(let latex): heights[index] = math.displayHeight(latex, theme, fontScale) @@ -1059,37 +1199,46 @@ enum BlockHeightMeasurer { codeWidth: BlockLayout.clampedWidth(contentWidth - 2 * BlockLayout.Code.horizontalPadding), chrome: BlockLayout.Code.verticalChrome(hasLanguage: !language.isEmpty, fontScale: fontScale)) kinds[index] = .exact + rhythm[index] = boxRhythm(block, row: index) default: continue } } + // The finished rows' edges may have changed (a formula on the first + // line, a formula that became a code row), and with them the space + // around them: derive the column again — arithmetic only. return MeasuredBlocks( table: BlockHeightTable(heights: heights, kinds: kinds, - contentWidth: measured.table.contentWidth), + contentWidth: measured.table.contentWidth, + spacingAbove: BlockLayout.Spacing.column(rhythm, fontScale: fontScale)), converted: converted, tables: measured.tables, - mathPendingRows: []) + mathPendingRows: [], + rhythm: rhythm) } - // MARK: - Estimates for the reported kinds - - /// `.heading` — TextKit's height for the title string at the title's width - /// (since v1.11 the title IS an NSTextView laid out with this string), - /// floored at the hover-to-copy button's own height. The view also - /// reproduces the old first-baseline alignment with the button - /// (`BlockLayout.Heading.titleGeometry`), which can add another point or - /// two for single-line H2–H6; the placed view corrects it (the row stays - /// `.reported`). - private static func headingEstimate(title: String, level: Int, - renderer: MarkdownRenderer, - contentWidth: CGFloat) -> CGFloat { + // MARK: - Headings + + /// `.heading` — the height `HeadingBlockView` lays out at and the edges of + /// its title. Since v1.11 the title IS an NSTextView laid out with this + /// string at the title's width, pushed down by the title inset that lines + /// its first baseline up with the hover-to-copy button's + /// (`BlockLayout.Heading.titleGeometry`), and the row is at least as tall + /// as the button's bottom edge. The row stays `.reported`: the placed view + /// still has the last word. + private static func headingRow(title: String, level: Int, renderer: MarkdownRenderer, + contentWidth: CGFloat) + -> (height: CGFloat, edges: BlockLayout.Spacing.Edges) { let attributed = renderer.renderHeader(title, level: level) let ns = (try? NSAttributedString(attributed, including: \.appKit)) ?? NSAttributedString(string: title) - let textWidth = BlockLayout.Heading.titleWrapWidth(rowWidth: contentWidth) - return max(exactHeight(text: ns, width: textWidth), BlockLayout.Heading.copyButtonHeight) + let geometry = BlockLayout.Heading.titleGeometry( + titleFirstBaseline: BlockLayout.Heading.titleFirstBaseline(font: renderer.headerAppKitFont(level: level))) + let ink = textInk(text: ns, width: BlockLayout.Heading.titleWrapWidth(rowWidth: contentWidth)) + let height = max(geometry.titleTopInset + ink.height, geometry.minimumHeight) + return (height, .text(ink, top: geometry.titleTopInset, rowHeight: height)) } } diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index a8ef11d..12bfeea 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -1402,14 +1402,8 @@ struct MarkdownView: View { // so a stale report can never overwrite an exact height). guard row < table.kinds.count, table.kinds[row] == .reported else { return } guard row < table.heights.count, abs(table.heights[row] - height) >= 0.5 else { return } - var heights = table.heights - heights[row] = height - measured = MeasuredBlocks( - table: BlockHeightTable(heights: heights, kinds: table.kinds, - contentWidth: table.contentWidth), - converted: measured.converted, - tables: measured.tables, - mathPendingRows: measured.mathPendingRows) + // The block's height only: its space above stays as measured. + measured = measured.replacingHeight(height, at: row) } /// Recompute focusedBlockId and focusedOccInBlock from currentMatchIndex diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift new file mode 100644 index 0000000..c6fec7e --- /dev/null +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -0,0 +1,222 @@ +// +// VerticalRhythm.swift +// QuickMD +// +// The space between blocks: one system for the document list and for +// print/PDF. Compiled into the app AND the test target (no view code). +// + +import AppKit + +// MARK: - The rhythm + +extension BlockLayout { + + /// How much space separates two blocks. + /// + /// Every gap is a multiple of the body size at the current zoom, + /// `u = 14 × fontScale`, so the whole column scales with the text. A gap + /// depends ONLY on the two blocks around it: never on whether the source + /// had a blank line between them (the parser keeps blank lines out of the + /// rows) and never on where a long block was cut into rows. Rows are + /// CommonMark blocks, so the same pair of blocks is the same distance apart + /// everywhere, in every document. + /// + /// A gap is measured one of two ways: + /// - **edge to edge** between two non-heading blocks: from the bottom of + /// the upper block's last line box, or its box edge (the background of a + /// code block, a quote, a table), to the top of the lower one's; + /// - **optically** around a heading: from the last BASELINE above it (or a + /// box edge) to the heading's CAP TOP, and from the heading's last + /// baseline to the next block's cap top or box edge. A heading then has + /// the same air under it whatever follows (a paragraph, a list, a code + /// block, a table) and the same air above it whatever precedes. + /// + /// A row carries its gap as space ABOVE its block (`BlockHeightTable + /// .spacingAbove`); the first row has none, and neither does a row that only + /// continues the one before it (`MarkdownBlock.joinsPrevious`). + enum Spacing { + + /// Body text size at 1.0 zoom: `MarkdownRenderer`'s paragraph font. + static let bodySize: CGFloat = 14 + + /// One unit of the rhythm at `fontScale`. + static func unit(fontScale: CGFloat) -> CGFloat { bodySize * fontScale } + + /// Between two non-heading blocks, edge to edge: paragraphs, lists, + /// rules and boxes alike. 24.5 pt at 100 %. + static let blockMargin: CGFloat = 1.75 + + /// Under every heading, optical: its last baseline to the next block's + /// cap top or box edge. 31.5 pt at 100 %. + static let headingBelow: CGFloat = 2.25 + + /// A heading directly under its parent (a deeper level right after a + /// shallower one), optical. A little tighter than `headingBelow`, so + /// the two titles read as one unit. 28 pt at 100 %. + static let headingUnderHeading: CGFloat = 2.0 + + /// Above a heading of level 1…6, optical: the last baseline (or box + /// edge) above it to its cap top. Larger for the higher levels, so a + /// section break reads stronger than a subsection break. + static let headingAbove: [CGFloat] = [4.75, 4.25, 3.75, 3.5, 3.5, 3.5] + + /// `headingAbove` for `level`, clamped to 1…6. + static func headingAbove(level: Int) -> CGFloat { + headingAbove[max(1, min(level, 6)) - 1] + } + + /// What a row is, as far as spacing goes. + enum Role: Equatable { + /// Paragraphs, lists, rules: line boxes. + case text + case heading(level: Int) + /// A block with a fixed edge: code, quote, alert, table, image, + /// formula, diagram. + case box + /// The front-matter properties table at the top of a document. A + /// heading right under it is the document's title, not a section + /// break, so it gets `blockMargin`, edge to edge. + case frontMatter + } + + /// Air inside a row, between the row's edges and what the reader sees. + struct Edges: Equatable { + /// Row top → cap top of the first line (text, heading), or the box edge. + var inkAbove: CGFloat + /// Last baseline (text, heading), or the box edge → row bottom. + var inkBelow: CGFloat + /// Row top → the first line box, or the box edge (0 for text). + var boxAbove: CGFloat + /// The last line box, or the box edge → row bottom (0 for text). + var boxBelow: CGFloat + + /// A box `top` / `bottom` points inside the row's edges: its ink + /// and its box are the same edge. + static func box(top: CGFloat, bottom: CGFloat) -> Edges { + Edges(inkAbove: top, inkBelow: bottom, boxAbove: top, boxBelow: bottom) + } + + /// Text laid out as `ink` describes, `top` points below the row's + /// top edge, in a row `rowHeight` tall (default: the text's own + /// height plus `top`). Its line boxes are the row's edges. + static func text(_ ink: TextInk, top: CGFloat = 0, rowHeight: CGFloat? = nil) -> Edges { + let height = rowHeight ?? (top + ink.height) + return Edges(inkAbove: top + ink.firstBaseline - ink.firstCapHeight, + inkBelow: height - top - ink.lastBaseline, + boxAbove: 0, boxBelow: 0) + } + } + + /// Where a laid-out string's ink starts and ends (TextKit 1, the same + /// layout `BlockHeightMeasurer.exactHeight` runs): see + /// `BlockHeightMeasurer.textInk`. + struct TextInk: Equatable { + /// `exactHeight` of the string. + let height: CGFloat + /// From the text's top to the first line's baseline. + let firstBaseline: CGFloat + /// The largest cap height among the first line's fonts. + let firstCapHeight: CGFloat + /// From the text's top to the last line's baseline. + let lastBaseline: CGFloat + } + + /// One row's input to the rhythm. + struct Row: Equatable { + let role: Role + let edges: Edges + /// `MarkdownBlock.joinsPrevious`. + let joinsPrevious: Bool + } + + /// The space to put above `row`'s block when `previous` is the row + /// before it (nil: the document's first row). + /// + /// The edges are subtracted from the target, so the reader sees the + /// target whatever air the two blocks already have inside their rows. + /// Whole points, never negative. + static func above(_ row: Row, after previous: Row?, fontScale: CGFloat) -> CGFloat { + guard let previous, !row.joinsPrevious else { return 0 } + let target: CGFloat + let optical: Bool + switch (previous.role, row.role) { + case (.heading(let upper), .heading(let lower)) where lower > upper: + target = headingUnderHeading + optical = true + case (.frontMatter, .heading): + target = blockMargin + optical = false + case (_, .heading(let level)): + target = headingAbove(level: level) + optical = true + case (.heading, _): + target = headingBelow + optical = true + default: + target = blockMargin + optical = false + } + let air = optical ? previous.edges.inkBelow + row.edges.inkAbove + : previous.edges.boxBelow + row.edges.boxAbove + return max(0, (target * unit(fontScale: fontScale) - air).rounded()) + } + + /// `above` for every row of a document, in order. + static func column(_ rows: [Row], fontScale: CGFloat) -> [CGFloat] { + var spacing: [CGFloat] = [] + spacing.reserveCapacity(rows.count) + for index in rows.indices { + spacing.append(above(rows[index], after: index > 0 ? rows[index - 1] : nil, + fontScale: fontScale)) + } + return spacing + } + } +} + +// MARK: - A row of the document list + +/// Where a block sits inside its row of `VirtualBlockList`. +/// +/// A row is the space above its block (`BlockHeightTable.spacing(_:)`) and +/// then the block, top-aligned. `NSTableView` is flipped, so `minY` is the +/// row's top edge. Every place that turns a row into a position goes through +/// here: a jump lands on the BLOCK, not on the space above it. +enum RowGeometry { + + /// The block's part of `rowRect`: everything below the space above. + static func contentRect(rowRect: NSRect, spacingAbove: CGFloat) -> NSRect { + let inset = min(max(0, spacingAbove), rowRect.height) + return NSRect(x: rowRect.minX, y: rowRect.minY + inset, + width: rowRect.width, height: rowRect.height - inset) + } + + /// The document y a `.top` jump puts at the top of the content area: the + /// block's top. The space above the block stays above the viewport, so + /// the content inset alone is the gap the reader sees — the same as above + /// the document's first block. + static func topTarget(rowRect: NSRect, spacingAbove: CGFloat) -> CGFloat { + contentRect(rowRect: rowRect, spacingAbove: spacingAbove).minY + } + + /// The document y a `.center` jump puts at the top of a content area + /// `visibleHeight` tall: the block's middle in the middle of it. + static func centerTarget(rowRect: NSRect, spacingAbove: CGFloat, visibleHeight: CGFloat) -> CGFloat { + contentRect(rowRect: rowRect, spacingAbove: spacingAbove).midY - visibleHeight / 2 + } + + /// Whether a point at `y` in a row belongs to the row BEFORE it: the upper + /// half of the space above a block is nearer the previous block, so a drag + /// that stops there ends after that block, not before this one. + static func belongsToPreviousRow(y: CGFloat, rowRect: NSRect, spacingAbove: CGFloat) -> Bool { + y < rowRect.minY + min(max(0, spacingAbove), rowRect.height) / 2 + } + + /// Whether `y` is in the upper half of the row's BLOCK — the space above + /// the block does not count. An atomic row (an image, a formula) is + /// selected once a drag passes its middle. + static func isInUpperHalf(y: CGFloat, rowRect: NSRect, spacingAbove: CGFloat) -> Bool { + y < contentRect(rowRect: rowRect, spacingAbove: spacingAbove).midY + } +} diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 4986bdc..aaafde5 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -126,14 +126,14 @@ struct VirtualBlockList: NSViewRepresentable { static let reading = LayoutStyle(maxContentWidth: Metrics.readingMaxContentWidth, verticalPadding: Metrics.readingContentVerticalPadding) - /// The scroll view's top/bottom content inset for this style. - /// - /// Not simply `verticalPadding`: `NSTableView` centres each row inside its - /// rect, which puts HALF the intercell spacing above the first row and - /// half below the last one (measured). Subtracting that half makes the - /// gap the reader sees exactly `verticalPadding`, and makes a `.top` jump - /// to any row — row 0 included — leave that same gap above its content. - var edgeInset: CGFloat { max(0, verticalPadding - Metrics.blockSpacing / 2) } + /// The scroll view's top/bottom content inset for this style: exactly + /// `verticalPadding`. The table has no intercell spacing (the space + /// between blocks is part of each row, above its block — see + /// `RowGeometry`), the first row has no space above its block, and a + /// `.top` jump to any row puts that row's BLOCK at the content top — so + /// the gap above the first block and above any jumped-to block is this + /// inset, in every style. + var edgeInset: CGFloat { verticalPadding } } /// Row content, in block order. @@ -230,9 +230,10 @@ struct VirtualBlockList: NSViewRepresentable { tableView.usesAlternatingRowBackgroundColors = false tableView.gridStyleMask = [] tableView.focusRingType = .none - // The 8 pt that `VStack(spacing:)` used to put between blocks. Row - // heights themselves therefore stay exactly what the measurer computed. - tableView.intercellSpacing = NSSize(width: 0, height: Metrics.blockSpacing) + // None: the space between blocks is part of each row, above its block + // (`BlockHeightTable.spacingAbove`, `BlockLayout.Spacing`), so a row + // rect is exactly the space above plus the block (`RowGeometry`). + tableView.intercellSpacing = NSSize(width: 0, height: 0) tableView.rowSizeStyle = .custom tableView.usesAutomaticRowHeights = false // The coordinator sets the single column's width to the clip view's width @@ -262,9 +263,8 @@ struct VirtualBlockList: NSViewRepresentable { // The 24 pt (48 in reading mode) that used to be `.padding(.vertical:)` // on the stack. As content insets they scroll with the document exactly // as before, but AppKit — not us — owns the arithmetic (see - // `contentTopY`). The half-spacing correction lives in - // `LayoutStyle.edgeInset`; the coordinator re-applies it when the style - // changes. + // `contentTopY`). `LayoutStyle.edgeInset` is the value; the coordinator + // re-applies it when the style changes. scrollView.automaticallyAdjustsContentInsets = false let edgeInset = layoutStyle.edgeInset scrollView.contentInsets = NSEdgeInsets(top: edgeInset, left: 0, @@ -414,6 +414,7 @@ struct VirtualBlockList: NSViewRepresentable { self.scrollView = scrollView self.tableView = tableView selection.attach(tableView: tableView) + selection.spacingAbove = { [weak self] row in self?.table.spacing(row) ?? 0 } tableView.selectionController = selection scrollView.selectionController = selection scrollView.onEndLiveResize = { [weak self] in @@ -448,13 +449,18 @@ struct VirtualBlockList: NSViewRepresentable { // MARK: - Model installation (D7 anchor preservation) /// The whole visible geometry of one scroll position: which row was at - /// the top of the content area, how far into it we were, and two - /// identities for finding that row again after the document changed shape - /// — its content fingerprint (primary) and the source line it started at - /// (fallback, D8). + /// the top of the content area, how far below its BLOCK's top we were, + /// and two identities for finding that row again after the document + /// changed shape — its content fingerprint (primary) and the source line + /// it started at (fallback, D8). + /// + /// The offset is from the block, not from the row's top edge: the space + /// above a block changes with zoom (and the block's neighbours), the + /// reader's place in the block does not. Negative when the content top + /// is in the space above the block. private struct CapturedAnchor { let row: Int - let offsetWithinRow: CGFloat + let offsetFromBlockTop: CGFloat let sourceLine: Int let signature: String } @@ -517,28 +523,16 @@ struct VirtualBlockList: NSViewRepresentable { #if DEBUG listSignpost.endInterval("reloadData", state) let elapsedMS = Double(DispatchTime.now().uptimeNanoseconds - started.uptimeNanoseconds) / 1_000_000 - listLog.debug("reloadData: \(newBlocks.count) rows, version=\(newVersion), width=\(newTable.contentWidth, format: .fixed(precision: 1)), anchor=\(anchor.map { "row \($0.row)+\($0.offsetWithinRow)" } ?? "none", privacy: .public), \(elapsedMS, format: .fixed(precision: 2)) ms") + listLog.debug("reloadData: \(newBlocks.count) rows, version=\(newVersion), width=\(newTable.contentWidth, format: .fixed(precision: 1)), anchor=\(anchor.map { "row \($0.row)+\($0.offsetFromBlockTop)" } ?? "none", privacy: .public), \(elapsedMS, format: .fixed(precision: 2)) ms") #endif } - /// The part of a row rect the hosted view actually occupies. - /// - /// `NSTableView` centres a row's content inside `rect(ofRow:)`, splitting - /// `intercellSpacing.height` half above and half below (measured: a 100 pt - /// row at 8 pt spacing gets rect height 108 with the cell at +4). - /// - /// Used for `.center` only, where the block's visual middle is what the - /// reader's eye goes to. `.top` and the scroll anchor deliberately use the - /// FULL row rect: its `minY` sits half a spacing above the content, which - /// — together with the reduced content inset — is what makes every jump - /// leave the same gap as the top of the document. - private func rowContentRect(_ row: Int) -> NSRect { - guard let tableView else { return .zero } - var rect = tableView.rect(ofRow: row) - let spacing = tableView.intercellSpacing.height - rect.origin.y += spacing / 2 - rect.size.height = max(0, rect.size.height - spacing) - return rect + /// The document y of row `row`'s BLOCK top: below the space above it. + /// What a `.top` jump scrolls to and what the scroll anchor is relative + /// to (`RowGeometry.topTarget`). + private func blockTopY(_ row: Int) -> CGFloat { + guard let tableView else { return 0 } + return RowGeometry.topTarget(rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)) } /// Where the reader is, or nil when that question has no answer. @@ -604,7 +598,7 @@ struct VirtualBlockList: NSViewRepresentable { } guard row >= 0, row < blocks.count else { return nil } return CapturedAnchor(row: row, - offsetWithinRow: top - tableView.rect(ofRow: row).minY, + offsetFromBlockTop: top - blockTopY(row), sourceLine: blocks[row].sourceLine, signature: blockSignature(blocks[row])) } @@ -649,9 +643,9 @@ struct VirtualBlockList: NSViewRepresentable { } guard row >= 0, row < tableView.numberOfRows else { return } #if DEBUG - listLog.debug("restoreAnchor: by \(matchedBy, privacy: .public), row \(anchor.row) → \(row), offset=\(anchor.offsetWithinRow, format: .fixed(precision: 1))") + listLog.debug("restoreAnchor: by \(matchedBy, privacy: .public), row \(anchor.row) → \(row), offset=\(anchor.offsetFromBlockTop, format: .fixed(precision: 1))") #endif - setContentTop(tableView.rect(ofRow: row).minY + anchor.offsetWithinRow, animated: false) + setContentTop(blockTopY(row) + anchor.offsetFromBlockTop, animated: false) } /// The row whose content fingerprint equals `signature`, closest to @@ -693,11 +687,9 @@ struct VirtualBlockList: NSViewRepresentable { // The coordinator's own table is patched SYNCHRONOUSLY so that a // second report for the same row in the same layout pass compares - // against this value rather than against the estimate. - var heights = table.heights - heights[row] = height - table = BlockHeightTable(heights: heights, kinds: table.kinds, - contentWidth: table.contentWidth) + // against this value rather than against the estimate. The block's + // height only: the space above it stays as measured. + table = table.replacingHeight(height, at: row) // Everything that touches AppKit waits a turn. This runs from // `onPreferenceChange`, i.e. from INSIDE a SwiftUI layout pass, and @@ -723,20 +715,23 @@ struct VirtualBlockList: NSViewRepresentable { // at, not against the value this particular report replaced: // several reports can coalesce into one turn, and the // compensation has to cancel the shift the table view is actually - // about to make. `rect(ofRow:)` includes the intercell spacing - // (see `rowContentRect`). - let applied = tableView.rect(ofRow: row).height - tableView.intercellSpacing.height + // about to make. `rect(ofRow:)` includes the space above the + // block, which the report does not change. + let applied = tableView.rect(ofRow: row).height - self.table.spacing(row) let delta = target - applied guard abs(delta) >= 0.5 else { self.onHeightReport(blockId, row, target) return } - // A row whose top is above the top of the content area slides the - // reader's content when it changes height — the row STRADDLING - // the viewport top included, which is why this is geometry and - // not `row < firstVisibleRow` (that row is "visible", yet growing - // it pushes everything the reader can see downwards). - let startsAboveViewport = tableView.rect(ofRow: row).minY < self.contentTopY() + // A block whose top is above the top of the content area slides + // the reader's content when it changes height — the block + // STRADDLING the viewport top included, which is why this is + // geometry and not `row < firstVisibleRow` (that row is + // "visible", yet growing it pushes everything the reader can see + // downwards). The BLOCK's top, not the row's: with the content + // top in the space above a block, the whole block is in view and + // grows downwards like any block below the viewport top. + let startsAboveViewport = self.blockTopY(row) < self.contentTopY() NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0 @@ -766,15 +761,19 @@ struct VirtualBlockList: NSViewRepresentable { let target: CGFloat switch request.anchor { case .top: - // Full row rect: with the reduced content inset this leaves the - // same gap above the heading as the top of the document has. - target = tableView.rect(ofRow: row).minY + // The block's top, below the space above it: the content inset + // is then the gap above it, as above the document's first block. + target = blockTopY(row) case .center: - // Centre of the CONTENT area (between the insets), not of the - // clip view — with equal top/bottom insets these coincide. + // The BLOCK's middle (not its row's, which includes the space + // above it) at the centre of the CONTENT area (between the + // insets), not of the clip view — with equal top/bottom insets + // these coincide. let insets = scrollView.contentInsets let contentHeight = scrollView.contentView.bounds.height - insets.top - insets.bottom - target = rowContentRect(row).midY - contentHeight / 2 + target = RowGeometry.centerTarget(rowRect: tableView.rect(ofRow: row), + spacingAbove: table.spacing(row), + visibleHeight: contentHeight) } #if DEBUG listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(request.anchor == .top ? "top" : "center", privacy: .public) target=\(target, format: .fixed(precision: 1))") @@ -914,8 +913,7 @@ struct VirtualBlockList: NSViewRepresentable { // (a 24 pt jolt on every toggle); keeping the raw clip origin // means restoring the document offset minus that delta. let delta = layoutStyle.edgeInset - previousInset - let top = tableView.map { $0.rect(ofRow: anchor.row).minY } ?? 0 - setContentTop(top + anchor.offsetWithinRow - delta, animated: false) + setContentTop(blockTopY(anchor.row) + anchor.offsetFromBlockTop - delta, animated: false) } } #if DEBUG @@ -1027,12 +1025,12 @@ struct VirtualBlockList: NSViewRepresentable { /// row's height can never influence the height reported for it. fileprivate func rootView(for row: Int) -> AnyView { let block = blocks[row] - // `blockSpacing`, not 0: a block view whose body is a TUPLE — today - // only `ImageBlockView` (image + italic alt caption) — has its + // `captionSpacing`, not 0: a block view whose body is a TUPLE — + // today only `ImageBlockView` (image + italic alt caption) — has its // elements flattened into whatever stack contains it, and in 1.8.0 // that stack was the document's `VStack(spacing: 8)`. Single-view // bodies, which is every other kind, are unaffected by the spacing. - let base = VStack(alignment: .leading, spacing: Metrics.blockSpacing) { + let base = VStack(alignment: .leading, spacing: BlockLayout.ImageBlock.captionSpacing) { content(block) } // The hosting view is an environment boundary (same reason @@ -1066,6 +1064,10 @@ struct VirtualBlockList: NSViewRepresentable { .frame(maxWidth: cellCap, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .center) + // The space above the block (`BlockLayout.Spacing`) is the row's + // top padding, OUTSIDE the height reporter: a report is the block's + // height alone, like every height in the table. + let spacing = table.spacing(row) let reports = row < table.kinds.count && table.kinds[row] == .reported if reports { return AnyView( @@ -1074,6 +1076,7 @@ struct VirtualBlockList: NSViewRepresentable { report: { [weak self] id, height in self?.reportHeight(blockId: id, height: height) })) + .padding(.top, spacing) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(.horizontal, Metrics.contentHorizontalPadding) .id(block.id) @@ -1081,6 +1084,7 @@ struct VirtualBlockList: NSViewRepresentable { } return AnyView( column + .padding(.top, spacing) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(.horizontal, Metrics.contentHorizontalPadding) .id(block.id) @@ -1099,6 +1103,7 @@ struct VirtualBlockList: NSViewRepresentable { /// the column its tint spans and whether its row is tinted right now. private func configureSelectionTint(_ cell: BlockHostingCell, row: Int) { cell.selectionTintColumnCap = cellColumnCap + cell.spacingAbove = table.spacing(row) cell.isSelectionTinted = selection.wantsTint(row: row) } @@ -1131,8 +1136,9 @@ struct VirtualBlockList: NSViewRepresentable { func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { guard row >= 0, row < table.heights.count else { return 1 } - // AppKit rejects non-positive row heights. - return max(1, table.heights[row]) + // The space above the block, then the block. AppKit rejects + // non-positive row heights. + return max(1, table.rowHeight(row)) } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, @@ -1500,6 +1506,12 @@ final class BlockHostingCell: NSTableCellView { didSet { if selectionTintColumnCap != oldValue { needsLayout = true } } } + /// The row's space above its block (`BlockHeightTable.spacing(_:)`): the + /// tint covers the block, not the gap above it. + var spacingAbove: CGFloat = 0 { + didSet { if spacingAbove != oldValue { needsLayout = true } } + } + init(rootView: AnyView) { hostingView = NSHostingView(rootView: rootView) super.init(frame: .zero) @@ -1521,8 +1533,13 @@ final class BlockHostingCell: NSTableCellView { // padding on both sides, then the column cap, centred. let padding = BlockLayout.Document.contentHorizontalPadding let width = max(0, min(bounds.width - 2 * padding, selectionTintColumnCap)) - tintView.frame = NSRect(x: (bounds.width - width) / 2, y: 0, - width: width, height: bounds.height) + // The block's part of the cell: below the space above it. The + // cell is not flipped, so "below" is the lower part of `bounds`. + var block = RowGeometry.contentRect(rowRect: NSRect(origin: .zero, size: bounds.size), + spacingAbove: spacingAbove) + if !isFlipped { block.origin.y = 0 } + tintView.frame = NSRect(x: (bounds.width - width) / 2, y: block.minY, + width: width, height: block.height) } } @@ -1608,6 +1625,9 @@ final class SelectionController: NSObject { var selectableTable: (MarkdownBlock, Int) -> TableText? = { _, _ in nil } /// See `VirtualBlockList.onCopySelection`. var onCopy: (DocumentCopyOutput) -> Void = { _ in } + /// The space above row N's block — part of its row rect (`RowGeometry`). + /// Set once by the coordinator, which owns the height table. + var spacingAbove: (Int) -> CGFloat = { _ in 0 } // MARK: Model @@ -1957,11 +1977,15 @@ final class SelectionController: NSObject { /// through the view that received the mouse-down, which a drag can scroll /// out of the prepared area and the table can recycle for another row. /// - /// Points between rows belong to a row (`rect(ofRow:)` includes half the - /// intercell spacing on each side); points above or below a row's text - /// (the gap, an alert's title, a code block's padding) map to the start or - /// the end of that text; points left or right of it to the line's start or - /// end. Above the first row / below the last: the document's ends. + /// Points between two blocks belong to the NEARER one: a row rect is the + /// space above its block plus the block (`RowGeometry`), and the upper half + /// of that space maps to the end of the row before, the lower half to the + /// start of this row — as when half the gap was in each row rect. Points + /// above or below a row's text (an alert's title, a code block's padding) + /// map to the start or the end of that text; points left or right of it to + /// the line's start or end. Above the first row (its rect starts at the + /// table's top; the first row has no space above) / below the last: the + /// document's ends. private func hit(atWindowPoint windowPoint: NSPoint) -> Hit? { guard let tableView, !blocks.isEmpty, tableView.numberOfRows == blocks.count else { return nil } let point = tableView.convert(windowPoint, from: nil) @@ -1977,13 +2001,20 @@ final class SelectionController: NSObject { textView: nil, pointInTextView: nil) } - let block = blocks[row] let rowRect = tableView.rect(ofRow: row) + let spacing = spacingAbove(row) + if row > 0, RowGeometry.belongsToPreviousRow(y: point.y, rowRect: rowRect, spacingAbove: spacing) { + return Hit(point: SelectionPoint(row: row - 1, offset: rowLength(row - 1)), isAtomic: false, + textView: nil, pointInTextView: nil) + } + let block = blocks[row] + let upperHalf = RowGeometry.isInUpperHalf(y: point.y, rowRect: rowRect, spacingAbove: spacing) if SelectionRows.isAtomic(block) { - // Upper half = before the row, lower half = after it: dragging - // DOWN selects the row once the pointer is past its middle, and so - // does dragging UP — symmetrical, like a very tall character. - return Hit(point: SelectionPoint(row: row, offset: point.y < rowRect.midY ? 0 : 1), + // Upper half of the BLOCK = before the row, lower half = after it: + // dragging DOWN selects the row once the pointer is past its + // middle, and so does dragging UP — symmetrical, like a very tall + // character. + return Hit(point: SelectionPoint(row: row, offset: upperHalf ? 0 : 1), isAtomic: true, textView: nil, pointInTextView: nil) } @@ -1999,7 +2030,7 @@ final class SelectionController: NSObject { // A text-kind row with no registered text view to ask (not yet // registered): its whole string, by half. - return Hit(point: SelectionPoint(row: row, offset: point.y < rowRect.midY ? 0 : rowLength(row)), + return Hit(point: SelectionPoint(row: row, offset: upperHalf ? 0 : rowLength(row)), isAtomic: false, textView: nil, pointInTextView: nil) } From fc7c9483a7f5f67e72fbab82049e82f5700d9734 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 17:40:51 +0100 Subject: [PATCH 005/105] Copy: a block cut into rows copies as one block Rows are blocks now, but a long block is still cut every 30 lines and the nested groups of one `>` run are separate rows. A copy joined every row with a blank line, so a selection across such a cut gained a blank line the document does not have. SelectionRows.parts marks a piece whose row continues the row of the piece before it (MarkdownBlock.joinsPrevious), and DocumentCopyBuilder.build(parts:) joins it with a single line break. build(_ pieces:) keeps the old behaviour for callers without rows. Source Edit's selection hint: selectionHintMaxRows 3 -> 8. A row is a single block now, so a few short paragraphs are already several rows; the 1000-character cap is what bounds the hint's text. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/DocumentSelection.swift | 28 +++++-- QuickMD/QuickMD/SelectionRows.swift | 77 ++++++++++++-------- QuickMD/QuickMD/SourceEditSession.swift | 5 +- QuickMD/QuickMD/Views/VirtualBlockList.swift | 10 +-- 4 files changed, 78 insertions(+), 42 deletions(-) diff --git a/QuickMD/QuickMD/DocumentSelection.swift b/QuickMD/QuickMD/DocumentSelection.swift index ae164d1..904dbaa 100644 --- a/QuickMD/QuickMD/DocumentSelection.swift +++ b/QuickMD/QuickMD/DocumentSelection.swift @@ -329,14 +329,32 @@ enum DocumentCopyBuilder { /// Rows are joined with one blank line, like Markdown paragraphs. static let rowSeparator = "\n\n" + /// A row that only continues the row before it (`MarkdownBlock + /// .joinsPrevious`) is joined with the line break the cut replaced. + static let continuationSeparator = "\n" + + /// One covered row's piece, and whether its row continues the row of the + /// piece before it (`SelectionRows.parts`). + struct Part { + let piece: SelectionPiece + let continuesPrevious: Bool + } + /// Pieces of separate blocks: every row joined with `rowSeparator`. static func build(_ pieces: [SelectionPiece]) -> DocumentCopyOutput { + build(parts: pieces.map { Part(piece: $0, continuesPrevious: false) }) + } + + static func build(parts: [Part]) -> DocumentCopyOutput { let result = NSMutableAttributedString() - for piece in pieces { - let part = attributed(for: piece) - guard part.length > 0 else { continue } - if result.length > 0 { result.append(NSAttributedString(string: rowSeparator)) } - result.append(part) + for part in parts { + let text = attributed(for: part.piece) + guard text.length > 0 else { continue } + if result.length > 0 { + result.append(NSAttributedString(string: part.continuesPrevious ? continuationSeparator + : rowSeparator)) + } + result.append(text) } let full = NSRange(location: 0, length: result.length) // Colours come from the theme the document is displayed in, not from diff --git a/QuickMD/QuickMD/SelectionRows.swift b/QuickMD/QuickMD/SelectionRows.swift index 47ded89..79c72e4 100644 --- a/QuickMD/QuickMD/SelectionRows.swift +++ b/QuickMD/QuickMD/SelectionRows.swift @@ -32,42 +32,57 @@ enum SelectionRows { static func pieces(of selection: DocumentSelection, blocks: [MarkdownBlock], text: (MarkdownBlock) -> NSAttributedString?, table: (MarkdownBlock) -> TableText?) -> [SelectionPiece] { + parts(of: selection, blocks: blocks, text: text, table: table).map(\.piece) + } + + /// `pieces`, each marked with whether it CONTINUES the piece before it: + /// its row only continues the previous row (`MarkdownBlock.joinsPrevious` + /// — a long block cut into rows, a nested quote group) and that row gave + /// a piece too. `DocumentCopyBuilder.build(parts:)` joins those with a + /// line break instead of a blank line. + static func parts(of selection: DocumentSelection, blocks: [MarkdownBlock], + text: (MarkdownBlock) -> NSAttributedString?, + table: (MarkdownBlock) -> TableText?) -> [DocumentCopyBuilder.Part] { guard let span = selection.rowSpan else { return [] } - var pieces: [SelectionPiece] = [] - pieces.reserveCapacity(span.count) + var parts: [DocumentCopyBuilder.Part] = [] + parts.reserveCapacity(span.count) + var lastRowWithPiece: Int? for row in span where row < blocks.count { let block = blocks[row] - switch block.content { - case .table: - guard let built = table(block), - let covered = selection.range(inRow: row, rowLength: built.string.length), - let piece = tablePiece(built, covered: covered) else { continue } - pieces.append(piece) - case .image(_, let alt, _): - guard selection.range(inRow: row, rowLength: 1) != nil else { continue } - pieces.append(.image(alt: alt)) - case .svgImage: - guard selection.range(inRow: row, rowLength: 1) != nil else { continue } - pieces.append(.image(alt: "")) - case .mathBlock(let latex): - guard selection.range(inRow: row, rowLength: 1) != nil else { continue } - pieces.append(.displayMath(latex: latex)) - case .mermaidDiagram(let source): - guard selection.range(inRow: row, rowLength: 1) != nil else { continue } - pieces.append(.mermaid(source: source)) - case .codeBlock, .text, .blockquote, .alert, .heading: - guard let string = text(block), - let covered = selection.range(inRow: row, rowLength: string.length) else { continue } - let part = string.attributedSubstring(from: covered) - if case .codeBlock = block.content { - pieces.append(.code(part)) - } else { - pieces.append(.text(part)) - } - } + guard let piece = piece(of: selection, row: row, block: block, text: text, table: table) else { continue } + parts.append(DocumentCopyBuilder.Part( + piece: piece, continuesPrevious: block.joinsPrevious && lastRowWithPiece == row - 1)) + lastRowWithPiece = row + } + return parts + } + + /// The covered part of ONE row as a copy piece; nil when the selection + /// covers nothing of it. + private static func piece(of selection: DocumentSelection, row: Int, block: MarkdownBlock, + text: (MarkdownBlock) -> NSAttributedString?, + table: (MarkdownBlock) -> TableText?) -> SelectionPiece? { + switch block.content { + case .table: + guard let built = table(block), + let covered = selection.range(inRow: row, rowLength: built.string.length) else { return nil } + return tablePiece(built, covered: covered) + case .image(_, let alt, _): + return selection.range(inRow: row, rowLength: 1) == nil ? nil : .image(alt: alt) + case .svgImage: + return selection.range(inRow: row, rowLength: 1) == nil ? nil : .image(alt: "") + case .mathBlock(let latex): + return selection.range(inRow: row, rowLength: 1) == nil ? nil : .displayMath(latex: latex) + case .mermaidDiagram(let source): + return selection.range(inRow: row, rowLength: 1) == nil ? nil : .mermaid(source: source) + case .codeBlock, .text, .blockquote, .alert, .heading: + guard let string = text(block), + let covered = selection.range(inRow: row, rowLength: string.length) else { return nil } + let part = string.attributedSubstring(from: covered) + if case .codeBlock = block.content { return .code(part) } + return .text(part) } - return pieces } /// The piece for the `covered` part of a table's string (TS-D8): diff --git a/QuickMD/QuickMD/SourceEditSession.swift b/QuickMD/QuickMD/SourceEditSession.swift index 67b938e..4c6cb80 100644 --- a/QuickMD/QuickMD/SourceEditSession.swift +++ b/QuickMD/QuickMD/SourceEditSession.swift @@ -247,7 +247,10 @@ final class SourceEditSession: ObservableObject { /// A carry-over is for "select a typo, fix it": a selection over more /// rows than this is not looked for (and its text is never built — ⌘A on /// a large document must not produce megabytes that cannot match). - static let selectionHintMaxRows = 3 + /// Rows are single blocks (one paragraph, one list, one loose-list item), + /// so a few short paragraphs are already several rows; the length cap + /// below is what bounds the text. + static let selectionHintMaxRows = 8 /// Longer hint texts are ignored, for the same reason. static let selectionHintMaxLength = 1000 /// Rendered characters of context on each side of the selection. diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index aaafde5..cd9b1a8 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -1883,11 +1883,11 @@ final class SelectionController: NSObject { func selectionOutput() -> DocumentCopyOutput? { guard let selection else { return nil } let version = contentVersion - let pieces = SelectionRows.pieces(of: selection, blocks: blocks, - text: { selectableText($0, version) }, - table: { selectableTable($0, version) }) - guard !pieces.isEmpty else { return nil } - let output = DocumentCopyBuilder.build(pieces) + let parts = SelectionRows.parts(of: selection, blocks: blocks, + text: { selectableText($0, version) }, + table: { selectableTable($0, version) }) + guard !parts.isEmpty else { return nil } + let output = DocumentCopyBuilder.build(parts: parts) return output.plain.isEmpty ? nil : output } From 3753ea8f122912cc7220ec91f910615065ebc40b Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 17:41:04 +0100 Subject: [PATCH 006/105] Print/PDF: the same spacing system as the screen The PDF put a flat 8 pt between every two blocks and relied on the blank-line paragraphs inside text blocks for the rest, so a printed document had none of the screen's rhythm. The dead whole-document MarkdownPrintableView (never instantiated) went the same way. PrintRhythm.row reads each printed block's edges the way MarkdownPrintableBlockView draws it (text and headings through SwiftUI Text, measured without paragraph styles; boxes from their frame edge; a display formula with its padding) and BlockLayout.Spacing.column turns them into the same gaps as on screen, at 1.0 zoom. PrintRhythm.paginate places the segments: a block's space above is dropped at the top of a page, and the later slices of a block taller than a page get none. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownExport.swift | 118 +++++++-------------------- QuickMD/QuickMD/VerticalRhythm.swift | 86 +++++++++++++++++++ 2 files changed, 115 insertions(+), 89 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index 67ef152..aa66e47 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -170,79 +170,6 @@ extension FocusedValues { } } -// MARK: - Printable View (Light mode, white background) -// NOTE: ImageRenderer uses DEFAULT environment, not app's environment -// So we must explicitly set ALL colors, not rely on colorScheme - -struct MarkdownPrintableView: View { - let documentText: String - /// Base for relative image paths (nil for an untitled buffer). - let documentURL: URL? - - /// Parsed blocks with light theme - computed once on init - private let lightBlocks: [MarkdownBlock] - - init(documentText: String, documentURL: URL? = nil) { - self.documentText = documentText - self.documentURL = documentURL - self.lightBlocks = MarkdownBlockParser(colorScheme: .light).parse(documentText) - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - ForEach(lightBlocks) { block in - switch block.content { - case .text(let attributedString): - // Force black text color; inline $math$ as images (PrintableMath) - PrintableMath.text(from: attributedString) - .foregroundColor(.black) - - case .table(let headers, let rows, let alignments, let style): - PrintableTableView(headers: headers, rows: rows, alignments: alignments, style: style) - .padding(.vertical, 8) - - case .codeBlock(let code, let language): - PrintableCodeBlockView(code: code, language: language) - .padding(.vertical, 4) - - case .image(let url, let alt, let width): - PrintableImageView(url: url, alt: alt, width: width, documentURL: documentURL) - .padding(.vertical, 8) - - case .blockquote(let content, let level): - PrintableBlockquoteView(content: content, level: level) - .padding(.vertical, 4) - - case .alert(let kind, let content): - PrintableAlertView(kind: kind, content: content) - .padding(.vertical, 4) - - case .heading(let level, let title, _): - let renderer = MarkdownRenderer(colorScheme: .light) - Text(renderer.renderHeader(title, level: level)) - .foregroundColor(.black) - - case .mathBlock(let latex): - PrintableMathBlockView(latex: latex) - .padding(.vertical, 4) - - case .mermaidDiagram(let source): - // Graceful degradation: render as styled code block in PDF - PrintableCodeBlockView(code: source, language: "mermaid") - .padding(.vertical, 4) - - case .svgImage(let source): - PrintableSVGView(source: source) - .padding(.vertical, 4) - } - } - } - .padding(40) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.white) - } -} - // MARK: - Table Alignment Protocol /// Column alignment for the SwiftUI table (`PrintableTableView`). The on-screen @@ -534,6 +461,10 @@ enum PrintFolderAccess { } // MARK: - Single Block Printable View (for per-block PDF rendering) +// NOTE: ImageRenderer uses the DEFAULT environment, not the app's, so every +// printable view sets ALL its colours explicitly rather than relying on +// colorScheme. Blocks carry no outer padding: the space between them is +// `BlockLayout.Spacing`, applied by `PDFExportManager` (`PrintRhythm`). struct MarkdownPrintableBlockView: View { let block: MarkdownBlock @@ -810,25 +741,34 @@ class PDFExportManager { return nil } - // Paginate: distribute blocks/segments across pages - var pages: [[PlacedSegment]] = [[]] - var currentY: CGFloat = 0 - - for (block, size) in measuredBlocks { - let segments = sliceBlockIfOversized(block: block, size: size, maxHeight: contentHeight) - for segment in segments { - let blockHeight = segment.sliceHeight - - // If block doesn't fit on current page and page isn't empty, start new page - if currentY + blockHeight > contentHeight && !pages[pages.count - 1].isEmpty { - pages.append([]) - currentY = 0 - } + // The screen's vertical rhythm (`BlockLayout.Spacing`), from the edges + // of the PRINTED views, at 1.0 zoom — print never follows the window's. + let printRenderer = MarkdownRenderer(colorScheme: .light) + let rhythm = measuredBlocks.enumerated().map { index, measured in + PrintRhythm.row(for: measured.block, isFirst: index == 0, + renderer: printRenderer, width: contentWidth) + } + let spacing = BlockLayout.Spacing.column(rhythm, fontScale: 1) - pages[pages.count - 1].append(PlacedSegment(segment: segment, yOffset: currentY)) - currentY += blockHeight + 8 // 8pt spacing between blocks + // Paginate: a block taller than a page is sliced; only its first slice + // has the block's space above it. + var segments: [BlockSegment] = [] + var segmentSpacing: [CGFloat] = [] + for (index, measured) in measuredBlocks.enumerated() { + let slices = sliceBlockIfOversized(block: measured.block, size: measured.size, maxHeight: contentHeight) + for (slice, segment) in slices.enumerated() { + segments.append(segment) + segmentSpacing.append(slice == 0 ? spacing[index] : 0) } } + let placements = PrintRhythm.paginate( + zip(segments, segmentSpacing).map { (height: $0.sliceHeight, spacingAbove: $1) }, + pageHeight: contentHeight) + var pages: [[PlacedSegment]] = [[]] + for (segment, placement) in zip(segments, placements) { + while pages.count <= placement.page { pages.append([]) } + pages[placement.page].append(PlacedSegment(segment: segment, yOffset: placement.y)) + } let pdfData = NSMutableData() var mediaBox = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index c6fec7e..6129b47 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -220,3 +220,89 @@ enum RowGeometry { y < contentRect(rowRect: rowRect, spacingAbove: spacingAbove).midY } } + +// MARK: - Print / PDF + +/// The rhythm for print and PDF (`PDFExportManager.generateMultiPagePDF`): +/// the same `BlockLayout.Spacing` rules, applied to the edges of the PRINTED +/// views (`MarkdownPrintableBlockView`), which have no outer paddings and no +/// heading copy button. +enum PrintRhythm { + + /// Role and edges of `block` as `MarkdownPrintableBlockView` prints it at + /// `width`, at 1.0 zoom. + /// + /// Text and headings print through SwiftUI `Text`, which ignores paragraph + /// styles, so their ink is measured without them. Every box (code, quote, + /// alert, table, image, diagram) prints its background or its picture from + /// its frame's edge; a display formula has its vertical padding. + /// + /// - Parameter isFirst: the block is the document's first (front matter). + static func row(for block: MarkdownBlock, isFirst: Bool, + renderer: MarkdownRenderer, width: CGFloat) -> BlockLayout.Spacing.Row { + typealias Spacing = BlockLayout.Spacing + let role: Spacing.Role + let edges: Spacing.Edges + switch block.content { + case .text(let attributed): + role = .text + edges = .text(BlockHeightMeasurer.textInk(text: printed(attributed), width: width)) + case .heading(let level, let title, _): + role = .heading(level: level) + edges = .text(BlockHeightMeasurer.textInk(text: printed(renderer.renderHeader(title, level: level)), + width: width)) + case .table(_, _, _, let style): + role = style == .properties && isFirst ? .frontMatter : .box + edges = .box(top: 0, bottom: 0) + case .mathBlock: + role = .box + edges = .box(top: BlockLayout.Math.verticalPadding, bottom: BlockLayout.Math.verticalPadding) + case .codeBlock, .blockquote, .alert, .image, .svgImage, .mermaidDiagram: + role = .box + edges = .box(top: 0, bottom: 0) + } + return Spacing.Row(role: role, edges: edges, joinsPrevious: block.joinsPrevious) + } + + /// The string as SwiftUI `Text` lays it out: no paragraph styles. + private static func printed(_ attributed: AttributedString) -> NSAttributedString { + let ns = NSMutableAttributedString(attributedString: + (try? NSAttributedString(attributed, including: \.appKit)) + ?? NSAttributedString(string: String(attributed.characters))) + ns.removeAttribute(.paragraphStyle, range: NSRange(location: 0, length: ns.length)) + return ns + } + + /// Where a segment lands: its page and its y from the top of the page's + /// content area. + struct Placement: Equatable { + let page: Int + let y: CGFloat + } + + /// Lays `segments` (a block, or one slice of a block taller than a page) + /// out top to bottom on pages `pageHeight` tall. + /// + /// A segment gets its `spacingAbove` unless it is first on its page — the + /// page's margin already separates it from the page above. A segment that + /// does not fit below the last one starts a new page. + static func paginate(_ segments: [(height: CGFloat, spacingAbove: CGFloat)], + pageHeight: CGFloat) -> [Placement] { + var placements: [Placement] = [] + placements.reserveCapacity(segments.count) + var page = 0 + var bottom: CGFloat = 0 + var pageIsEmpty = true + for segment in segments { + var y = pageIsEmpty ? 0 : bottom + segment.spacingAbove + if !pageIsEmpty, y + segment.height > pageHeight { + page += 1 + y = 0 + } + placements.append(Placement(page: page, y: y)) + bottom = y + segment.height + pageIsEmpty = false + } + return placements + } +} From 163311b8f65387118b1636d3866b24ef50d81598 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 17:41:14 +0100 Subject: [PATCH 007/105] Tests: vertical rhythm VerticalRhythmTests checks every gap as the reader sees it (the space above a row plus the air inside the two rows) against the approved targets, restated in the test rather than read from the code: - blank-line neutrality (`## H` with and without a blank line, a list after a paragraph with and without one) and no blank or trailing lines in text rows; rows are CommonMark blocks with the right source lines; - the same optical gap under a heading for every following block kind, and above one for every preceding kind; per-level targets, heading under its parent, front matter to the title; - edge-to-edge margins between non-heading blocks, first row 0, and every gap at fontScale 1.0, 1.25 and 1.5; - a 70-item list cut into three rows with no space at the cuts and a copy across them without blank lines; nested quote groups joining; - a loose ordered list split into ten rows keeping one head indent; - measureMathRows deriving the same table and rhythm as a full measure, and a height report leaving the spacing alone; - RowGeometry: content rect, `.top` and `.center` targets, the gap's upper half belonging to the previous row, an atomic block's middle; - print and screen landing every gap on the same target, and pagination dropping the space at a page top. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 4 + .../QuickMDTests/VerticalRhythmTests.swift | 409 ++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 QuickMD/QuickMDTests/VerticalRhythmTests.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index f44c862..3e3b590 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ A1B2C3D401000068 /* ChromeHoverState.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000066 /* ChromeHoverState.swift */; }; A1B2C3D401000069 /* BlockHeightMeasurer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000069 /* BlockHeightMeasurer.swift */; }; A1B2C3D40100006A /* BlockHeightMeasurerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000006A /* BlockHeightMeasurerTests.swift */; }; + A1B2C3D4010002A2 /* VerticalRhythmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A1 /* VerticalRhythmTests.swift */; }; A1B2C3D40100006B /* BlockHeightMeasurer.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000069 /* BlockHeightMeasurer.swift */; }; A1B2C3D40100006C /* VirtualBlockList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000006C /* VirtualBlockList.swift */; }; A1B2C3D40100006D /* SVGBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000006D /* SVGBlockView.swift */; }; @@ -175,6 +176,7 @@ A1B2C3D400000064 /* FontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontTests.swift; sourceTree = ""; }; A1B2C3D400000069 /* BlockHeightMeasurer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockHeightMeasurer.swift; sourceTree = ""; }; A1B2C3D40000006A /* BlockHeightMeasurerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockHeightMeasurerTests.swift; sourceTree = ""; }; + A1B2C3D4000002A1 /* VerticalRhythmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythmTests.swift; sourceTree = ""; }; A1B2C3D40000006C /* VirtualBlockList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualBlockList.swift; sourceTree = ""; }; A1B2C3D40000006D /* SVGBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGBlockView.swift; sourceTree = ""; }; A1B2C3D40000006E /* SVGImageDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGImageDecoder.swift; sourceTree = ""; }; @@ -337,6 +339,7 @@ A1B2C3D400000064 /* FontTests.swift */, A1B2C3D400000067 /* ChromeHoverTests.swift */, A1B2C3D40000006A /* BlockHeightMeasurerTests.swift */, + A1B2C3D4000002A1 /* VerticalRhythmTests.swift */, A1B2C3D400000082 /* ImageLoadingTests.swift */, A1B2C3D4000000E1 /* HTMLImageTests.swift */, A1B2C3D4000000F0 /* HeadingLayoutTests.swift */, @@ -720,6 +723,7 @@ A1B2C3D401000067 /* ChromeHoverTests.swift in Sources */, A1B2C3D401000068 /* ChromeHoverState.swift in Sources (Tests) */, A1B2C3D40100006A /* BlockHeightMeasurerTests.swift in Sources */, + A1B2C3D4010002A2 /* VerticalRhythmTests.swift in Sources */, A1B2C3D40100006B /* BlockHeightMeasurer.swift in Sources (Tests) */, A1B2C3D4010001B1 /* TableTextBuilder.swift in Sources (Tests) */, A1B2C3D401000221 /* FrontMatter.swift in Sources (Tests) */, diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift new file mode 100644 index 0000000..f475f20 --- /dev/null +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -0,0 +1,409 @@ +import XCTest +import SwiftUI +import AppKit + +/// The vertical rhythm (`BlockLayout.Spacing`, VerticalRhythm.swift): rows are +/// CommonMark blocks, blank lines never add space, every gap is a multiple of +/// the body size, and print/PDF uses the same rules. +/// +/// Gaps are checked as the READER sees them — the space above a row PLUS the +/// air the two blocks already have inside their rows (`realizedGap`) — against +/// the rules restated here independently (`expectedTarget`), not against +/// `BlockLayout.Spacing.above` itself. +final class VerticalRhythmTests: XCTestCase { + + // MARK: - Fixtures + + private typealias Spacing = BlockLayout.Spacing + private var theme: MarkdownTheme { MarkdownTheme.cached(for: .light) } + /// Rounding to whole points can move a gap by up to half a point. + private let tolerance: CGFloat = 0.51 + + /// Same deterministic stand-in for SwiftMath as `BlockHeightMeasurerTests`. + private let math = MathRendering( + inlineImage: { _, _, _ in NSImage(size: NSSize(width: 24, height: 12)) }, + displayHeight: { _, _, _ in 40 } + ) + + private func parse(_ markdown: String, fontScale: CGFloat = 1.0) -> [MarkdownBlock] { + MarkdownBlockParser(theme: theme, fontScale: fontScale).parse(markdown) + } + + private func measure(_ blocks: [MarkdownBlock], fontScale: CGFloat = 1.0, + width: CGFloat = 640) -> MeasuredBlocks { + BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: fontScale, + contentWidth: width, math: math) + } + + /// What the reader sees between row `index - 1` and row `index`: optical + /// (baseline / cap top) around a heading unless the heading follows front + /// matter, edge to edge otherwise. + private func realizedGap(_ rows: [Spacing.Row], _ spacing: [CGFloat], _ index: Int) -> CGFloat { + let previous = rows[index - 1], next = rows[index] + if isOptical(previous.role, next.role) { + return previous.edges.inkBelow + spacing[index] + next.edges.inkAbove + } + return previous.edges.boxBelow + spacing[index] + next.edges.boxAbove + } + + private func isOptical(_ previous: Spacing.Role, _ next: Spacing.Role) -> Bool { + if case .frontMatter = previous { return false } + if case .heading = previous { return true } + if case .heading = next { return true } + return false + } + + /// The approved values, in body-size units. + private func expectedTarget(_ previous: Spacing.Role, _ next: Spacing.Role) -> CGFloat { + switch (previous, next) { + case (.heading(let upper), .heading(let lower)) where lower > upper: return 2.0 + case (.frontMatter, .heading): return 1.75 + case (_, .heading(let level)): return [4.75, 4.25, 3.75, 3.5, 3.5, 3.5][level - 1] + case (.heading, _): return 2.25 + default: return 1.75 + } + } + + /// Every gap of `markdown` at `fontScale` equals its target. + private func assertEveryGapOnTarget(_ markdown: String, fontScale: CGFloat = 1.0, + file: StaticString = #filePath, line: UInt = #line) { + let measured = measure(parse(markdown, fontScale: fontScale), fontScale: fontScale) + let rows = measured.rhythm + let spacing = measured.table.spacingAbove + XCTAssertEqual(spacing.first, 0, "first row", file: file, line: line) + let u = 14 * fontScale + for index in rows.indices.dropFirst() where !rows[index].joinsPrevious { + let expected = expectedTarget(rows[index - 1].role, rows[index].role) * u + XCTAssertEqual(realizedGap(rows, spacing, index), expected, accuracy: tolerance, + "row \(index - 1) \(rows[index - 1].role) → row \(index) \(rows[index].role) at \(fontScale)", + file: file, line: line) + } + } + + private func plain(_ block: MarkdownBlock) -> String { + guard case .text(let attributed) = block.content else { return "" } + return String(attributed.characters) + } + + // MARK: - Blank lines + + func testBlankLinesBetweenBlocksAreNeutral() { + let pairs: [(String, String)] = [ + ("## Heading\n\nParagraph text.", "## Heading\nParagraph text."), + ("# Title\n\n\n\nParagraph text.", "# Title\nParagraph text."), + ("Paragraph text.\n\n- one\n- two", "Paragraph text.\n- one\n- two"), + ("Paragraph text.\n\n1. one\n2. two", "Paragraph text.\n1. one\n2. two"), + ("```\ncode\n```\n\nParagraph text.", "```\ncode\n```\nParagraph text."), + ("Paragraph text.\n\n---\n\nMore text.", "Paragraph text.\n\n---\nMore text."), + ] + for (loose, tight) in pairs { + let a = parse(loose), b = parse(tight) + XCTAssertEqual(a.count, b.count, loose) + XCTAssertEqual(a.map(plain), b.map(plain), loose) + let ma = measure(a), mb = measure(b) + XCTAssertEqual(ma.table.heights, mb.table.heights, loose) + XCTAssertEqual(ma.table.spacingAbove, mb.table.spacingAbove, loose) + } + } + + func testTextRowsCarryNoBlankLinesOrTrailingBreak() { + let blocks = parse("First paragraph.\n\n\n\nSecond paragraph.\n\n") + XCTAssertEqual(blocks.map(plain), ["First paragraph.", "Second paragraph."]) + XCTAssertEqual(blocks.map(\.sourceLine), [0, 4]) + } + + func testRowsAreCommonMarkBlocks() { + let blocks = parse("Intro line one\nintro line two.\n- item a\n- item b\n\nOutro.\n***\nAfter the rule.") + XCTAssertEqual(blocks.map(plain).count, 5, "\(blocks.map(plain))") + XCTAssertEqual(blocks.map(\.sourceLine), [0, 2, 5, 6, 7]) + XCTAssertTrue(blocks.allSatisfy { !$0.joinsPrevious }) + } + + // MARK: - Headings + + func testEveryBlockUnderAHeadingGetsTheSameOpticalGap() { + let followers = [ + "Paragraph text.", + "- bullet one\n- bullet two", + "1. first\n2. second", + "```swift\nlet x = 1\n```", + "> A quote.", + "> [!NOTE]\n> An alert.", + "| a | b |\n|---|---|\n| 1 | 2 |", + "$$\nx^2\n$$", + "```mermaid\ngraph TD; A-->B\n```", + "![alt](missing.png)", + "***", + ] + for follower in followers { + let measured = measure(parse("## Heading\n\n" + follower)) + XCTAssertEqual(measured.rhythm.count, 2, follower) + XCTAssertEqual(realizedGap(measured.rhythm, measured.table.spacingAbove, 1), 2.25 * 14, + accuracy: tolerance, follower) + // …and the same air above a heading, whatever precedes it. + let above = measure(parse(follower + "\n\n## Heading")) + XCTAssertEqual(realizedGap(above.rhythm, above.table.spacingAbove, above.rhythm.count - 1), 4.25 * 14, + accuracy: tolerance, follower) + } + } + + func testHeadingTargetsPerLevelAndUnderAParent() { + for level in 1...6 { + let hashes = String(repeating: "#", count: level) + assertEveryGapOnTarget("Paragraph text.\n\n\(hashes) Heading \(level)\n\nParagraph text.") + } + assertEveryGapOnTarget("# Title\n## Section\n### Subsection\nText.\n## Next section\n### Deeper\n#### Deepest") + let under = measure(parse("# Title\n## Section")) + XCTAssertEqual(realizedGap(under.rhythm, under.table.spacingAbove, 1), 2.0 * 14, accuracy: tolerance) + } + + func testFrontMatterToTitleIsOneMarginEdgeToEdge() { + let measured = measure(parse("---\ntitle: Notes\ntags: [a, b]\n---\n# Notes\n\nText.")) + XCTAssertEqual(measured.rhythm.first?.role, .frontMatter) + XCTAssertEqual(measured.rhythm.count, 3) + XCTAssertEqual(realizedGap(measured.rhythm, measured.table.spacingAbove, 1), 1.75 * 14, accuracy: tolerance) + } + + // MARK: - Between other blocks + + func testNonHeadingPairsAreOneMarginApart() { + assertEveryGapOnTarget(""" + First paragraph. + + Second paragraph. + - a list + - of items + + ``` + code + ``` + > A quote. + + | a | b | + |---|---| + | 1 | 2 | + + > [!TIP] + > An alert. + + $$ + y = x + $$ + + *** + Last paragraph. + """) + } + + func testGapsScaleWithFontScale() { + let document = """ + # Title + Intro paragraph. + + - list one + - list two + + ``` + code + ``` + + ## Section + | a | b | + |---|---| + | 1 | 2 | + + ### Subsection + > A quote. + """ + for fontScale: CGFloat in [1.0, 1.25, 1.5] { + assertEveryGapOnTarget(document, fontScale: fontScale) + } + let at100 = measure(parse(document)).table.spacingAbove + let at150 = measure(parse(document, fontScale: 1.5), fontScale: 1.5).table.spacingAbove + XCTAssertGreaterThan(at150.reduce(0, +), at100.reduce(0, +) * 1.4) + } + + func testFirstRowHasNoSpaceAbove() { + for markdown in ["# Title", "Text.", "```\ncode\n```", "> quote", "---\ntitle: x\n---\nText."] { + XCTAssertEqual(measure(parse(markdown)).table.spacingAbove.first, 0, markdown) + } + } + + // MARK: - Cuts + + func testLongListIsCutSeamlessly() { + let list = (1...70).map { "- item \($0)" }.joined(separator: "\n") + let blocks = parse("Intro.\n\n" + list) + XCTAssertEqual(blocks.count, 4) + XCTAssertEqual(blocks.map(\.joinsPrevious), [false, false, true, true]) + let limit = MarkdownBlockParser.textChunkLineLimit + XCTAssertEqual(blocks.map(\.sourceLine), [0, 2, 2 + limit, 2 + 2 * limit]) + let measured = measure(blocks) + XCTAssertEqual(measured.table.spacingAbove[2], 0) + XCTAssertEqual(measured.table.spacingAbove[3], 0) + XCTAssertGreaterThan(measured.table.spacingAbove[1], 0) + + // A copy across the cuts reads as one list: a line break at each cut. + let selection = DocumentSelection(anchor: SelectionPoint(row: 1, offset: 0), + focus: SelectionPoint(row: 3, offset: measured.converted[blocks[3].id]!.length)) + let output = DocumentCopyBuilder.build(parts: SelectionRows.parts( + of: selection, blocks: blocks, text: { measured.converted[$0.id] }, table: { _ in nil })) + XCTAssertFalse(output.plain.contains("\n\n")) + XCTAssertEqual(output.plain.components(separatedBy: "\n").count, 70) + } + + func testNestedQuoteGroupsJoinTheirQuote() { + let blocks = parse("Text.\n\n> outer\n>> inner\n> outer again\n\n> separate quote") + XCTAssertEqual(blocks.count, 5) + XCTAssertEqual(blocks.map(\.joinsPrevious), [false, false, true, true, false]) + let measured = measure(blocks) + XCTAssertEqual(Array(measured.table.spacingAbove[2...3]), [0, 0]) + XCTAssertGreaterThan(measured.table.spacingAbove[4], 0) + } + + func testCopyJoinsSeparateBlocksWithABlankLine() { + let blocks = parse("One.\n\nTwo.") + let measured = measure(blocks) + let selection = DocumentSelection(anchor: SelectionPoint(row: 0, offset: 0), + focus: SelectionPoint(row: 1, offset: 4)) + let output = DocumentCopyBuilder.build(parts: SelectionRows.parts( + of: selection, blocks: blocks, text: { measured.converted[$0.id] }, table: { _ in nil })) + XCTAssertEqual(output.plain, "One.\n\nTwo.") + } + + // MARK: - Ordered lists split into rows + + func testLooseOrderedListKeepsOneTextEdge() { + let loose = parse((1...10).map { "\($0). item \($0)" }.joined(separator: "\n\n")) + let tight = parse((1...10).map { "\($0). item \($0)" }.joined(separator: "\n")) + XCTAssertEqual(loose.count, 10) + XCTAssertEqual(tight.count, 1) + let measured = measure(loose) + let headIndents = loose.map { block -> CGFloat in + let style = measured.converted[block.id]?.attribute(.paragraphStyle, at: 0, effectiveRange: nil) + return (style as? NSParagraphStyle)?.headIndent ?? -1 + } + let tightString = measure(tight).converted[tight[0].id]! + let tightIndent = (tightString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle)?.headIndent + XCTAssertEqual(Set(headIndents).count, 1, "\(headIndents)") + XCTAssertEqual(headIndents.first, tightIndent) + } + + // MARK: - Partial re-measures carry the column + + @MainActor + func testMeasureMathRowsDerivesTheSameColumnAsAFullMeasure() { + let blocks = parse("## Heading\n\nInline $x^2$ math on the first line.\n\n$$\ny = x\n$$\n\nAfter.\n\n### Next\nText.") + let deferred = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1.25, + contentWidth: 640, math: nil) + XCTAssertFalse(deferred.mathPendingRows.isEmpty) + let finished = BlockHeightMeasurer.measureMathRows(deferred.mathPendingRows, in: deferred, blocks: blocks, + theme: theme, fontScale: 1.25, contentWidth: 640, + math: math) + let direct = measure(blocks, fontScale: 1.25) + XCTAssertEqual(finished.table, direct.table) + XCTAssertEqual(finished.rhythm, direct.rhythm) + } + + func testHeightReportChangesTheBlockNotItsSpacing() { + let measured = measure(parse("Text.\n\n## Heading\n\nMore text.")) + let patched = measured.replacingHeight(123, at: 1) + XCTAssertEqual(patched.table.heights[1], 123) + XCTAssertEqual(patched.table.spacingAbove, measured.table.spacingAbove) + XCTAssertEqual(patched.rhythm, measured.rhythm) + XCTAssertEqual(patched.table.rowHeight(1), measured.table.spacing(1) + 123) + XCTAssertEqual(measured.replacingHeight(5, at: 99).table, measured.table) + } + + // MARK: - Row geometry (VirtualBlockList) + + func testRowGeometryPutsJumpsAndHitsOnTheBlock() { + let row = NSRect(x: 0, y: 100, width: 500, height: 60) + XCTAssertEqual(RowGeometry.contentRect(rowRect: row, spacingAbove: 20), + NSRect(x: 0, y: 120, width: 500, height: 40)) + XCTAssertEqual(RowGeometry.contentRect(rowRect: row, spacingAbove: 0), row) + XCTAssertEqual(RowGeometry.contentRect(rowRect: row, spacingAbove: 90).height, 0) + // `.top`: the block's top, not the row's. + XCTAssertEqual(RowGeometry.topTarget(rowRect: row, spacingAbove: 20), 120) + // `.center`: the block's middle (140) at the middle of a 400 pt area. + XCTAssertEqual(RowGeometry.centerTarget(rowRect: row, spacingAbove: 20, visibleHeight: 400), -60) + // The gap above the block: upper half → the previous row. + XCTAssertTrue(RowGeometry.belongsToPreviousRow(y: 109.9, rowRect: row, spacingAbove: 20)) + XCTAssertFalse(RowGeometry.belongsToPreviousRow(y: 110, rowRect: row, spacingAbove: 20)) + XCTAssertFalse(RowGeometry.belongsToPreviousRow(y: 100, rowRect: row, spacingAbove: 0)) + // An atomic block splits at ITS middle. + XCTAssertTrue(RowGeometry.isInUpperHalf(y: 139.9, rowRect: row, spacingAbove: 20)) + XCTAssertFalse(RowGeometry.isInUpperHalf(y: 140, rowRect: row, spacingAbove: 20)) + } + + func testTableRowHeightIsSpacingPlusBlock() { + let measured = measure(parse("# Title\n\nText.\n\n```\ncode\n```")) + let table = measured.table + for row in 0.. A quote. + + $$ + y = x + $$ + - one + - two + """ + let printTheme = MarkdownTheme.exportTheme(for: .light) + let blocks = MarkdownBlockParser(theme: printTheme, fontScale: 1).parse(markdown) + let renderer = MarkdownRenderer(colorScheme: .light) + let printRows = blocks.enumerated().map { + PrintRhythm.row(for: $0.element, isFirst: $0.offset == 0, renderer: renderer, width: 532) + } + let printSpacing = Spacing.column(printRows, fontScale: 1) + let screen = BlockHeightMeasurer.measure(blocks: blocks, theme: printTheme, fontScale: 1, + contentWidth: 532, math: math) + XCTAssertEqual(printRows.map(\.role), screen.rhythm.map(\.role)) + XCTAssertEqual(printSpacing.first, 0) + for index in blocks.indices.dropFirst() { + let expected = expectedTarget(printRows[index - 1].role, printRows[index].role) * 14 + XCTAssertEqual(realizedGap(printRows, printSpacing, index), expected, accuracy: tolerance, "print \(index)") + XCTAssertEqual(realizedGap(screen.rhythm, screen.table.spacingAbove, index), expected, + accuracy: tolerance, "screen \(index)") + } + } + + func testPaginationDropsTheSpaceAtAPageTop() { + let placements = PrintRhythm.paginate([(100, 0), (50, 30), (600, 30), (40, 30), (700, 30), (10, 0)], + pageHeight: 700) + XCTAssertEqual(placements, [ + .init(page: 0, y: 0), + .init(page: 0, y: 130), + .init(page: 1, y: 0), // 180 + 30 + 600 > 700: new page, no space above + .init(page: 1, y: 630), // 600 + 30 + 40 = 670 fits + .init(page: 2, y: 0), // a full page (a slice of a taller block) + .init(page: 3, y: 0), // the next slice: nothing fits below a full page + ]) + } +} From 4b1bce349196fef3199b3e3813ccd67082c46a99 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 18:56:07 +0100 Subject: [PATCH 008/105] Scroll anchor: keep the reader's block across toggles and reports Three ways the content top drifted after a layout change, all in the list's anchor bookkeeping: - Re-anchoring on a different block. The Reading Mode toggle moves the content top by the inset delta (24 pt), which can cross into the row above; the next install then anchored on THAT block, and the way back re-anchored on the original one. The round trip drifted by any difference between the two layouts in between (the space between the two blocks, a block that wraps differently, a placeholder). The list now remembers the anchor it last put the content top on (restore, toggle, .top jump) and keeps it while the content top is still there. - Compensating the anchored block's own report. A restore puts the reader at an offset measured at the block's real height while the block sits at an estimate (an image placeholder, the diagram default). When it reported, the block straddled the content top and was compensated by the estimate's error, moving the reader a whole image. The anchored block's report now leaves the scroll offset alone. - Double compensation at the end of the document. The content top was read after noteHeightOfRows, which had already clamped the clip origin when the table shrank; the shrink was applied twice. The anchor math and the report rule live in RowGeometry; tests cover the Reading Mode round trip (with a different gap between the two blocks in the narrow layout), an anchor inside a placeholder across two re-measures, and the report rule. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/VerticalRhythm.swift | 46 ++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 78 +++++++-- .../QuickMDTests/VerticalRhythmTests.swift | 149 ++++++++++++++++++ 3 files changed, 256 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 6129b47..d3d5a64 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -206,6 +206,52 @@ enum RowGeometry { contentRect(rowRect: rowRect, spacingAbove: spacingAbove).midY - visibleHeight / 2 } + /// The scroll anchor: how far below the row's BLOCK top the content top + /// is. Negative when the content top is in the space above the block. + /// Relative to the block, not to the row's edge, because that space + /// changes with zoom and with the block's neighbours; the reader's place + /// in the block does not. + static func anchorOffset(contentTop: CGFloat, rowRect: NSRect, spacingAbove: CGFloat) -> CGFloat { + contentTop - topTarget(rowRect: rowRect, spacingAbove: spacingAbove) + } + + /// The content top that puts the reader back at `anchorOffset` below the + /// row's block top — in whatever layout the row has now. + static func contentTop(anchorOffset: CGFloat, rowRect: NSRect, spacingAbove: CGFloat) -> CGFloat { + topTarget(rowRect: rowRect, spacingAbove: spacingAbove) + anchorOffset + } + + /// Whether the content top is still exactly where `anchorOffset` below the + /// row's block top puts it (to half a point) — i.e. nothing has scrolled + /// since the anchor was set, other than compensations that moved the block + /// and the content top together. + static func isAt(anchorOffset: CGFloat, contentTop: CGFloat, rowRect: NSRect, spacingAbove: CGFloat) -> Bool { + abs(contentTop - self.contentTop(anchorOffset: anchorOffset, rowRect: rowRect, + spacingAbove: spacingAbove)) < 0.5 + } + + /// Where the content top goes after a `.reported` row's block changed + /// height by `delta`; nil leaves the scroll offset alone. + /// + /// A block changes height at its BOTTOM (rows are top-aligned), so the + /// change moves everything below that bottom. A block whose top is above + /// the content top — the one straddling it included — is compensated by + /// the same amount, so what the reader sees below it stays put. A block + /// whose top is in view needs nothing. + /// + /// Except the block the reader is anchored on: `anchoredOffset` is the + /// list's last scroll anchor when it is in this very block, and if the + /// content top is still where that anchor put it, the offset was measured + /// at the block's previous real height while the block sat at an estimate + /// (a restore after a zoom or a re-measure) — compensating would move the + /// reader by the estimate's error, a whole image. `contentTop` must be + /// read BEFORE the table applies the change. + static func contentTopAfterReport(contentTop: CGFloat, blockTop: CGFloat, delta: CGFloat, + anchoredOffset: CGFloat?) -> CGFloat? { + if let anchoredOffset, abs(contentTop - (blockTop + anchoredOffset)) < 0.5 { return nil } // see `isAt` + return blockTop < contentTop ? contentTop + delta : nil + } + /// Whether a point at `y` in a row belongs to the row BEFORE it: the upper /// half of the space above a block is nearer the previous block, so a drag /// that stops there ends after that block, not before this one. diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index cd9b1a8..a4868ef 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -366,6 +366,20 @@ struct VirtualBlockList: NSViewRepresentable { /// re-measure even when its natural height happens to be unchanged. private(set) var generation: Int = 0 private var rowForBlockId: [String: Int] = [:] + /// The anchor this list last put the content top on (a restore, the + /// reading-mode toggle, a `.top` jump), by block id. While the content + /// top is still exactly there it IS the reader's place, and two things + /// read it instead of the geometry: + /// - `captureAnchor`: a style toggle or a restore can move the content + /// top across a row boundary (24 pt of inset, an offset into the gap + /// above the anchored block), and re-anchoring on whatever row is + /// under it now would tie the next layout to a DIFFERENT block — the + /// round trip then drifts by any difference between the two layouts + /// in between (the space between those two blocks, a placeholder + /// height). Keeping the block keeps every round trip exact. + /// - `reportHeight`: the anchored block's own report is not + /// compensated (`RowGeometry.contentTopAfterReport`). + private var lastAnchor: (blockId: String, offsetFromBlockTop: CGFloat)? // MARK: Inputs refreshed per update @@ -477,6 +491,7 @@ struct VirtualBlockList: NSViewRepresentable { let anchor = captureAnchor() let previousCount = blocks.count + lastAnchor = nil blocks = newBlocks table = newTable @@ -546,7 +561,7 @@ struct VirtualBlockList: NSViewRepresentable { /// for reasons that have nothing to do with the scroll position — /// which is how a hidden tab used to end up anchored on its LAST row and /// scrolled to the end of its document. - private func captureAnchor() -> CapturedAnchor? { + private func captureAnchor(preferringLastAnchor: Bool = true) -> CapturedAnchor? { guard let scrollView, let tableView, !blocks.isEmpty, tableView.numberOfRows > 0 else { #if DEBUG @@ -578,6 +593,14 @@ struct VirtualBlockList: NSViewRepresentable { } let top = contentTopY() + if preferringLastAnchor, let last = lastAnchor, let row = rowForBlockId[last.blockId], + row < tableView.numberOfRows, row < blocks.count, + RowGeometry.isAt(anchorOffset: last.offsetFromBlockTop, contentTop: top, + rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)) { + return CapturedAnchor(row: row, offsetFromBlockTop: last.offsetFromBlockTop, + sourceLine: blocks[row].sourceLine, + signature: blockSignature(blocks[row])) + } var row = tableView.row(at: NSPoint(x: 0, y: max(0, top))) if row < 0 { // −1 means "no row at that point", which is only *legitimately* @@ -598,7 +621,9 @@ struct VirtualBlockList: NSViewRepresentable { } guard row >= 0, row < blocks.count else { return nil } return CapturedAnchor(row: row, - offsetFromBlockTop: top - blockTopY(row), + offsetFromBlockTop: RowGeometry.anchorOffset( + contentTop: top, rowRect: tableView.rect(ofRow: row), + spacingAbove: table.spacing(row)), sourceLine: blocks[row].sourceLine, signature: blockSignature(blocks[row])) } @@ -608,7 +633,7 @@ struct VirtualBlockList: NSViewRepresentable { /// answers nil (plain file open) instead of a placeholder row. func readingEditorLine() -> Int? { DocumentReadingPosition.editorLine(selection: selection.selection, - topRow: captureAnchor()?.row, + topRow: captureAnchor(preferringLastAnchor: false)?.row, blocks: blocks) } @@ -645,7 +670,21 @@ struct VirtualBlockList: NSViewRepresentable { #if DEBUG listLog.debug("restoreAnchor: by \(matchedBy, privacy: .public), row \(anchor.row) → \(row), offset=\(anchor.offsetFromBlockTop, format: .fixed(precision: 1))") #endif - setContentTop(blockTopY(row) + anchor.offsetFromBlockTop, animated: false) + setContentTop(contentTop(anchorOffset: anchor.offsetFromBlockTop, row: row), animated: false) + noteAnchor(row: row, offsetFromBlockTop: anchor.offsetFromBlockTop) + } + + /// The content top that puts the reader `anchorOffset` below row + /// `row`'s block top in the CURRENT layout (`RowGeometry`). + private func contentTop(anchorOffset: CGFloat, row: Int) -> CGFloat { + guard let tableView else { return 0 } + return RowGeometry.contentTop(anchorOffset: anchorOffset, rowRect: tableView.rect(ofRow: row), + spacingAbove: table.spacing(row)) + } + + /// Records the anchor the content top was just put on (`lastAnchor`). + private func noteAnchor(row: Int, offsetFromBlockTop: CGFloat) { + lastAnchor = row >= 0 && row < blocks.count ? (blocks[row].id, offsetFromBlockTop) : nil } /// The row whose content fingerprint equals `signature`, closest to @@ -723,27 +762,28 @@ struct VirtualBlockList: NSViewRepresentable { self.onHeightReport(blockId, row, target) return } - // A block whose top is above the top of the content area slides - // the reader's content when it changes height — the block - // STRADDLING the viewport top included, which is why this is - // geometry and not `row < firstVisibleRow` (that row is - // "visible", yet growing it pushes everything the reader can see - // downwards). The BLOCK's top, not the row's: with the content - // top in the space above a block, the whole block is in view and - // grows downwards like any block below the viewport top. - let startsAboveViewport = self.blockTopY(row) < self.contentTopY() + // Read BEFORE `noteHeightOfRows`: at the end of the document a + // shrinking row shortens the table and AppKit clamps the clip + // origin by itself right there — reading the content top after + // that applied the shrink twice (the last line ended up on the + // window's bottom edge). Which reports move the scroll offset: + // `RowGeometry.contentTopAfterReport`. + let top = self.contentTopY() + let anchored = self.lastAnchor.flatMap { $0.blockId == blockId ? $0.offsetFromBlockTop : nil } + let newTop = RowGeometry.contentTopAfterReport(contentTop: top, blockTop: self.blockTopY(row), + delta: delta, anchoredOffset: anchored) NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0 tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) NSAnimationContext.endGrouping() - if startsAboveViewport { - self.setContentTop(self.contentTopY() + delta, animated: false) + if let newTop { + self.setContentTop(newTop, animated: false) } #if DEBUG - listLog.debug("reportHeight: row \(row) (\(blockId, privacy: .public)) \(applied, format: .fixed(precision: 1)) → \(target, format: .fixed(precision: 1))\(startsAboveViewport ? " (compensated)" : "")") + listLog.debug("reportHeight: row \(row) (\(blockId, privacy: .public)) \(applied, format: .fixed(precision: 1)) → \(target, format: .fixed(precision: 1)) at top \(top, format: .fixed(precision: 1))\(newTop != nil ? " (compensated)" : anchored != nil ? " (anchor inside)" : "")") #endif // The parent holds the authoritative table (it feeds the next @@ -764,6 +804,7 @@ struct VirtualBlockList: NSViewRepresentable { // The block's top, below the space above it: the content inset // is then the gap above it, as above the document's first block. target = blockTopY(row) + noteAnchor(row: row, offsetFromBlockTop: 0) case .center: // The BLOCK's middle (not its row's, which includes the space // above it) at the centre of the CONTENT area (between the @@ -774,6 +815,7 @@ struct VirtualBlockList: NSViewRepresentable { target = RowGeometry.centerTarget(rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row), visibleHeight: contentHeight) + lastAnchor = nil } #if DEBUG listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(request.anchor == .top ? "top" : "center", privacy: .public) target=\(target, format: .fixed(precision: 1))") @@ -913,7 +955,9 @@ struct VirtualBlockList: NSViewRepresentable { // (a 24 pt jolt on every toggle); keeping the raw clip origin // means restoring the document offset minus that delta. let delta = layoutStyle.edgeInset - previousInset - setContentTop(blockTopY(anchor.row) + anchor.offsetFromBlockTop - delta, animated: false) + let offset = anchor.offsetFromBlockTop - delta + setContentTop(contentTop(anchorOffset: offset, row: anchor.row), animated: false) + noteAnchor(row: anchor.row, offsetFromBlockTop: offset) } } #if DEBUG diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index f475f20..c4392a8 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -336,6 +336,155 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertFalse(RowGeometry.isInUpperHalf(y: 140, rowRect: row, spacingAbove: 20)) } + // MARK: - Scroll anchor across layouts (Reading Mode, width, zoom) + + /// A column of rows as `NSTableView` lays it out with no intercell + /// spacing: each row is its space above plus its block. + private struct Column { + let rows: [(spacing: CGFloat, height: CGFloat)] + var rects: [NSRect] { + var y: CGFloat = 0 + return rows.map { row in + defer { y += row.spacing + row.height } + return NSRect(x: 0, y: y, width: 500, height: row.spacing + row.height) + } + } + func blockTop(_ row: Int) -> CGFloat { + RowGeometry.topTarget(rowRect: rects[row], spacingAbove: rows[row].spacing) + } + } + + /// `VirtualBlockList.Coordinator`'s anchor bookkeeping over a `Column`: + /// `captureAnchor` (the last anchor while the content top is still on it, + /// else the row under the content top), `restore`, the reading-mode toggle + /// and the install of a table measured at another width. + private struct Scroller { + var column: Column + var contentTop: CGFloat + /// Off = re-anchor on the row under the content top every time (the + /// first version of the branch). + let sticky: Bool + var last: (row: Int, offset: CGFloat)? + + mutating func capture() -> (row: Int, offset: CGFloat) { + let rects = column.rects + if sticky, let last, RowGeometry.isAt(anchorOffset: last.offset, contentTop: contentTop, + rowRect: rects[last.row], + spacingAbove: column.rows[last.row].spacing) { + return last + } + let row = rects.firstIndex { contentTop >= $0.minY && contentTop < $0.maxY } ?? rects.count - 1 + return (row, RowGeometry.anchorOffset(contentTop: contentTop, rowRect: rects[row], + spacingAbove: column.rows[row].spacing)) + } + mutating func restore(_ anchor: (row: Int, offset: CGFloat)) { + contentTop = RowGeometry.contentTop(anchorOffset: anchor.offset, rowRect: column.rects[anchor.row], + spacingAbove: column.rows[anchor.row].spacing) + last = anchor + } + /// `applyLayoutStyle`: the anchor minus the inset delta. + mutating func toggleStyle(insetDelta: CGFloat) { + let anchor = capture() + restore((anchor.row, anchor.offset - insetDelta)) + } + /// `install` of the table measured at the new width. + mutating func install(_ new: Column) { + let anchor = capture() + column = new + restore(anchor) + } + } + + /// Reading Mode on and off, step by step as the coordinator runs it: the + /// style toggle restores the anchor minus the inset delta (a 24 pt move of + /// the content top, across a row boundary here), then the table measured + /// at the new width is installed and the anchor restored again. The + /// narrow layout has taller rows above the reader AND less space between + /// the two blocks the content top moves between (a block's edges can + /// change with the wrap); the content top starts inside the gap. + func testReadingModeRoundTripReturnsToTheSameBlockOffset() { + let wide = Column(rows: [(0, 39), (19, 17), (25, 17), (25, 17), (21, 66), (21, 17), (50, 33), (12, 186), (5, 79)]) + let narrow = Column(rows: [(0, 39), (19, 34), (25, 34), (25, 34), (21, 66), (13, 17), (50, 33), (12, 186), (5, 79)]) + let standardInset: CGFloat = 24, readingInset: CGFloat = 48 + var drifted = false + + for start in stride(from: CGFloat(150), through: 330, by: 7.5) { + for sticky in [true, false] { + var list = Scroller(column: wide, contentTop: start, sticky: sticky) + list.toggleStyle(insetDelta: readingInset - standardInset) + // The block the reader was anchored on, and where they saw it. + let block = list.last?.row ?? 0 + let before = wide.blockTop(block) - start + standardInset + + list.install(narrow) + if sticky { + XCTAssertEqual(narrow.blockTop(block) - list.contentTop + readingInset, before + 48, + accuracy: 0.001, "entering at \(start)") + } + + list.toggleStyle(insetDelta: standardInset - readingInset) + list.install(wide) + if sticky { + XCTAssertEqual(list.contentTop, start, accuracy: 0.001, "round trip from \(start)") + } else if abs(list.contentTop - start) > 0.5 { + drifted = true + } + } + } + // Why the anchor is sticky: re-anchoring on the row under the content + // top lands on block 4 after the toggle and comes back by block 5 — so + // the round trip moves by the change in the space between them. + XCTAssertTrue(drifted) + } + + /// A zoom (or any re-measure) while the reader is inside an image: the + /// image comes back at its placeholder height and the restore puts the + /// content top past it; a second re-measure before the image reported + /// must not anchor on whatever row is now under the content top. + func testAnchorInsideAPlaceholderSurvivesAnotherReinstall() { + let loaded = Column(rows: [(0, 39), (16, 640), (17, 17), (50, 33), (19, 17), (25, 17), (25, 17)]) + let placeholder = Column(rows: [(0, 39), (16, 116), (17, 17), (50, 33), (19, 17), (25, 17), (25, 17)]) + let start = loaded.blockTop(1) + 550 + var list = Scroller(column: loaded, contentTop: start, sticky: true) + list.install(placeholder) // re-parse: the image is a placeholder again + list.install(placeholder) // width change before it reported + list.install(loaded) // the image reported (no compensation: see the report tests) + XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + } + + func testAnchorIsRelativeToTheBlockNotTheRowEdge() { + let rect = NSRect(x: 0, y: 1000, width: 500, height: 80) + // In the gap above the block: negative, and it stays the same distance + // above the block when the gap itself changes (zoom, new neighbours). + let offset = RowGeometry.anchorOffset(contentTop: 1010, rowRect: rect, spacingAbove: 30) + XCTAssertEqual(offset, -20) + let regapped = NSRect(x: 0, y: 2000, width: 500, height: 95) + XCTAssertEqual(RowGeometry.contentTop(anchorOffset: offset, rowRect: regapped, spacingAbove: 45), 2025) + } + + // MARK: - Height reports + + func testReportAboveTheViewportIsCompensated() { + // A block wholly above, and one straddling the content top. + XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 100, delta: 24, anchoredOffset: nil), 524) + XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 480, delta: -37.5, anchoredOffset: nil), 462.5) + // A block in view grows below the reader: nothing to do. + XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 500, delta: 24, anchoredOffset: nil)) + XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 620, delta: 24, anchoredOffset: nil)) + } + + func testReportOfTheAnchoredBlockKeepsTheReaderInIt() { + // After a zoom the reader was 550 pt into an image that came back at a + // 116 pt placeholder: the restore put the content top 550 below its + // block top. The image then reports its real height — the reader must + // stay 550 pt into it, not move on by the placeholder's error. + XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 1550, blockTop: 1000, delta: 524, anchoredOffset: 550)) + // A diagram that came back TALLER than it is (the 200 pt default). + XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 1150, blockTop: 1000, delta: -37.5, anchoredOffset: 150)) + // The reader scrolled since the restore: an ordinary straddling block. + XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 1300, blockTop: 1000, delta: 524, anchoredOffset: 550), 1824) + } + func testTableRowHeightIsSpacingPlusBlock() { let measured = measure(parse("# Title\n\nText.\n\n```\ncode\n```")) let table = measured.table From b06ed240f7cae81d3825b58b7d4c5f16e3e69832 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:08:47 +0100 Subject: [PATCH 009/105] Scroll anchor: keep the reader in a block that is still an estimate The debug log of the Reading Mode round trip shows the anchor as `row 21 (image) + 579.5` on the way in and `row 29 + 0.0` on the way out; a zoom round trip with the viewport top in the image comes back on "Diagram". Every new table (zoom, width) has the image at its 116 pt placeholder, so restoring the reader 579.5 pt into it put the content top far past the block: the image was off screen, its view was never built, it never reported its real height, and the next capture took whatever block the placeholder had left under the content top. - The anchor keeps the reader's offset and where the content top was actually put. Past the end of a `.reported` block whose height is still an estimate (no report since the table was installed), the content top goes to the block's last point, so the block is on screen and reports; the anchor is then placed again at the reader's offset. A capture while the content top is still there returns the reader's offset, so the round trip is exact even through the placeholder. - A jump (search, ToC) anchors on its target. Blocks above the target that report while the scroll is in flight re-aim it, and after it landed they shift the content top with them, so a search hit stays in the middle while the images above it load (it ended at the bottom of the window, in 1.14.4 as well). - The Mermaid snapshot path seeds the height cache like the web view path does, so a re-measure after a re-parse no longer measures the diagram at the 200 pt default. The decisions are RowGeometry.anchorPlacement and reportAction; the tests replay the coordinator's flow on a model of fixture.md (zoom round trip and Reading Mode round trip with the reader inside the image, the reader in a gap, a re-measure before the image reported, a search jump while the blocks above report), each with the previous rules as a control that must fail. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/VerticalRhythm.swift | 64 ++- QuickMD/QuickMD/Views/MermaidBlockView.swift | 9 +- QuickMD/QuickMD/Views/VirtualBlockList.swift | 175 +++++--- .../QuickMDTests/VerticalRhythmTests.swift | 391 +++++++++++++----- 4 files changed, 472 insertions(+), 167 deletions(-) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index d3d5a64..192e65e 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -230,26 +230,54 @@ enum RowGeometry { spacingAbove: spacingAbove)) < 0.5 } - /// Where the content top goes after a `.reported` row's block changed - /// height by `delta`; nil leaves the scroll offset alone. + /// Where to put the content top for a reader `offset` below a block whose + /// height in the current layout is `blockHeight`: at `offset`, unless the + /// block would then be wholly above the visible area (`topInset`, the top + /// content inset, still shows content) while its height is only an + /// estimate — an image placeholder, the diagram default. Its view would + /// never be built, never report its real height, and the reader would + /// stay in whatever follows the estimate. The content top then goes to the + /// block's last point instead, so the block is on screen and reports, and + /// the anchor is placed again at `offset` when it does (`reportAction`). + static func anchorPlacement(offset: CGFloat, blockHeight: CGFloat, topInset: CGFloat, + heightIsEstimate: Bool) -> CGFloat { + guard heightIsEstimate, offset - topInset >= blockHeight else { return offset } + return max(0, blockHeight - 1) + } + + /// What a `.reported` row's height change does to the scroll offset. + enum ReportAction: Equatable { + /// Nothing: the change is below what the reader is anchored on. + case keep + /// Move the content top by the change: it is above the anchor, so the + /// anchor stays where it is on screen. + case shift + /// The anchored block itself: place the anchor again (`anchorPlacement`) + /// against the block's new height. + case placeAnchor + } + + /// The rule for a height report of `row`. A block changes height at its + /// BOTTOM (rows are top-aligned), so the change moves everything below it. /// - /// A block changes height at its BOTTOM (rows are top-aligned), so the - /// change moves everything below that bottom. A block whose top is above - /// the content top — the one straddling it included — is compensated by - /// the same amount, so what the reader sees below it stays put. A block - /// whose top is in view needs nothing. + /// With an anchor (`anchorRow`: the list's last anchor while the content + /// top is still where it put it, or the target of a jump in flight) the + /// anchored block is what stays put: rows above it shift the content top + /// with them — rows in view above a jump target included, so a search hit + /// stays centred while the images above it load — and the anchored block + /// itself is placed again. Its offset was measured at its real height + /// while the block sat at an estimate; compensating would move the reader + /// by the estimate's error, a whole image. /// - /// Except the block the reader is anchored on: `anchoredOffset` is the - /// list's last scroll anchor when it is in this very block, and if the - /// content top is still where that anchor put it, the offset was measured - /// at the block's previous real height while the block sat at an estimate - /// (a restore after a zoom or a re-measure) — compensating would move the - /// reader by the estimate's error, a whole image. `contentTop` must be - /// read BEFORE the table applies the change. - static func contentTopAfterReport(contentTop: CGFloat, blockTop: CGFloat, delta: CGFloat, - anchoredOffset: CGFloat?) -> CGFloat? { - if let anchoredOffset, abs(contentTop - (blockTop + anchoredOffset)) < 0.5 { return nil } // see `isAt` - return blockTop < contentTop ? contentTop + delta : nil + /// Without one (the reader scrolled), a block whose top is above the + /// content top — the one straddling it included — shifts the content top, + /// and a block whose top is in view needs nothing. `contentTop` must be + /// read BEFORE the table applies the change (at the end of the document + /// AppKit clamps the clip origin when the table shrinks). + static func reportAction(row: Int, anchorRow: Int?, blockTop: CGFloat, contentTop: CGFloat) -> ReportAction { + guard let anchorRow else { return blockTop < contentTop ? .shift : .keep } + if row < anchorRow { return .shift } + return row == anchorRow ? .placeAnchor : .keep } /// Whether a point at `y` in a row belongs to the row BEFORE it: the upper diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index e9845e0..178e72d 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -66,7 +66,14 @@ struct MermaidBlockView: View { _diagramHeight = State(initialValue: heightCache.height(for: blockId) ?? Metrics.defaultHeight) let cached = MermaidSnapshotStore.image(source: "\(fontScale)|\(contentWidth)|\(source)", isDark: theme.isDark) _snapshot = State(initialValue: cached) - if let cached { _diagramHeight = State(initialValue: cached.size.height) } + if let cached { + _diagramHeight = State(initialValue: cached.size.height) + // The web view path seeds the cache through `onHeight`; this path + // never runs it. Without the seed every re-measure after a re-parse + // (zoom, theme) — which empties the cache — measures the diagram at + // the 200 pt default again and it reports back on screen. + heightCache.set(cached.size.height, for: blockId) + } } var body: some View { diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index a4868ef..e3c51e4 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -366,20 +366,41 @@ struct VirtualBlockList: NSViewRepresentable { /// re-measure even when its natural height happens to be unchanged. private(set) var generation: Int = 0 private var rowForBlockId: [String: Int] = [:] - /// The anchor this list last put the content top on (a restore, the - /// reading-mode toggle, a `.top` jump), by block id. While the content - /// top is still exactly there it IS the reader's place, and two things - /// read it instead of the geometry: + /// The reader's place as this list last set it — a restore, the + /// reading-mode toggle, a jump — by block id. While the content top is + /// still where it was put (`placed`; or a jump to it is in flight) this + /// IS the reader's place, and it is read instead of the geometry: /// - `captureAnchor`: a style toggle or a restore can move the content /// top across a row boundary (24 pt of inset, an offset into the gap - /// above the anchored block), and re-anchoring on whatever row is - /// under it now would tie the next layout to a DIFFERENT block — the - /// round trip then drifts by any difference between the two layouts - /// in between (the space between those two blocks, a placeholder - /// height). Keeping the block keeps every round trip exact. - /// - `reportHeight`: the anchored block's own report is not - /// compensated (`RowGeometry.contentTopAfterReport`). - private var lastAnchor: (blockId: String, offsetFromBlockTop: CGFloat)? + /// above the anchored block, an offset past a placeholder), and + /// re-anchoring on whatever row is under it now would tie the next + /// layout to a DIFFERENT block — the round trip then drifts by any + /// difference between the two layouts in between. Keeping the block + /// and the offset keeps every round trip exact. + /// - `reportHeight`: the anchored block stays put on screen + /// (`RowGeometry.reportAction`). + private var anchor: ReaderAnchor? + /// A programmatic animated scroll is on its way to `anchor`: reports + /// re-aim it instead of compensating a content top that is mid-flight. + private var isJumping = false + /// Bumped per animated jump, so an older animation's completion does + /// not end a newer one. + private var jumpGeneration = 0 + /// `.reported` blocks whose view has told this table its height: their + /// height is no longer an estimate (`RowGeometry.anchorPlacement`). + private var reportedBlocks: Set = [] + + private struct ReaderAnchor { + let blockId: String + /// Where the reader is: the content top's offset below the block's + /// top, as captured from a real layout or asked for by a jump. + var offset: CGFloat + /// Where the content top actually is relative to that block top: + /// `offset`, unless the block's estimated height cannot hold it + /// (`RowGeometry.anchorPlacement`) or AppKit clamped the scroll at + /// an end of the document. + var placed: CGFloat + } // MARK: Inputs refreshed per update @@ -491,7 +512,9 @@ struct VirtualBlockList: NSViewRepresentable { let anchor = captureAnchor() let previousCount = blocks.count - lastAnchor = nil + self.anchor = nil + isJumping = false + reportedBlocks = [] blocks = newBlocks table = newTable @@ -593,11 +616,8 @@ struct VirtualBlockList: NSViewRepresentable { } let top = contentTopY() - if preferringLastAnchor, let last = lastAnchor, let row = rowForBlockId[last.blockId], - row < tableView.numberOfRows, row < blocks.count, - RowGeometry.isAt(anchorOffset: last.offsetFromBlockTop, contentTop: top, - rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)) { - return CapturedAnchor(row: row, offsetFromBlockTop: last.offsetFromBlockTop, + if preferringLastAnchor, let anchor, let row = anchoredRow(contentTop: top) { + return CapturedAnchor(row: row, offsetFromBlockTop: anchor.offset, sourceLine: blocks[row].sourceLine, signature: blockSignature(blocks[row])) } @@ -670,21 +690,53 @@ struct VirtualBlockList: NSViewRepresentable { #if DEBUG listLog.debug("restoreAnchor: by \(matchedBy, privacy: .public), row \(anchor.row) → \(row), offset=\(anchor.offsetFromBlockTop, format: .fixed(precision: 1))") #endif - setContentTop(contentTop(anchorOffset: anchor.offsetFromBlockTop, row: row), animated: false) - noteAnchor(row: row, offsetFromBlockTop: anchor.offsetFromBlockTop) + place(row: row, offset: anchor.offsetFromBlockTop, animated: false) } - /// The content top that puts the reader `anchorOffset` below row - /// `row`'s block top in the CURRENT layout (`RowGeometry`). - private func contentTop(anchorOffset: CGFloat, row: Int) -> CGFloat { - guard let tableView else { return 0 } - return RowGeometry.contentTop(anchorOffset: anchorOffset, rowRect: tableView.rect(ofRow: row), - spacingAbove: table.spacing(row)) + /// Puts the reader `offset` below row `row`'s block top in the CURRENT + /// layout — or inside the block, while its estimated height cannot hold + /// that offset (`RowGeometry.anchorPlacement`) — and makes that the + /// list's `anchor`. Animated = a jump: `isJumping` until it lands. + private func place(row: Int, offset: CGFloat, animated: Bool) { + guard let tableView, let scrollView, row >= 0, row < blocks.count, + row < tableView.numberOfRows else { return } + let rect = tableView.rect(ofRow: row) + let spacing = table.spacing(row) + let blockTop = RowGeometry.topTarget(rowRect: rect, spacingAbove: spacing) + let placement = RowGeometry.anchorPlacement( + offset: offset, blockHeight: RowGeometry.contentRect(rowRect: rect, spacingAbove: spacing).height, + topInset: scrollView.contentInsets.top, + heightIsEstimate: row < table.kinds.count && table.kinds[row] == .reported + && !reportedBlocks.contains(blocks[row].id)) + var completion: (() -> Void)? + if animated { + jumpGeneration += 1 + let jump = jumpGeneration + isJumping = true + completion = { [weak self] in + guard let self, self.jumpGeneration == jump else { return } + self.isJumping = false + } + } else { + isJumping = false + } + let reached = setContentTop(blockTop + placement, animated: animated, completion: completion) + anchor = ReaderAnchor(blockId: blocks[row].id, offset: offset, placed: reached - blockTop) + #if DEBUG + listLog.debug("place: row \(row) offset=\(offset, format: .fixed(precision: 1)) placed=\(reached - blockTop, format: .fixed(precision: 1))\(animated ? " (jump)" : "", privacy: .public)") + #endif } - /// Records the anchor the content top was just put on (`lastAnchor`). - private func noteAnchor(row: Int, offsetFromBlockTop: CGFloat) { - lastAnchor = row >= 0 && row < blocks.count ? (blocks[row].id, offsetFromBlockTop) : nil + /// The row of `anchor` while it still holds: a jump to it is in + /// flight, or the content top is where it was put. Nil once the reader + /// has scrolled away (or the anchor's block is gone). + private func anchoredRow(contentTop: CGFloat) -> Int? { + guard let anchor, let tableView, let row = rowForBlockId[anchor.blockId], + row < tableView.numberOfRows, row < blocks.count else { return nil } + if isJumping { return row } + return RowGeometry.isAt(anchorOffset: anchor.placed, contentTop: contentTop, + rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)) + ? row : nil } /// The row whose content fingerprint equals `signature`, closest to @@ -722,6 +774,7 @@ struct VirtualBlockList: NSViewRepresentable { guard let row = rowForBlockId[blockId], row < table.heights.count else { return } // Defensive: an exact row cannot know better than the measurer. guard row < table.kinds.count, table.kinds[row] == .reported else { return } + reportedBlocks.insert(blockId) guard abs(height - table.heights[row]) >= 0.5 else { return } // The coordinator's own table is patched SYNCHRONOUSLY so that a @@ -766,24 +819,37 @@ struct VirtualBlockList: NSViewRepresentable { // shrinking row shortens the table and AppKit clamps the clip // origin by itself right there — reading the content top after // that applied the shrink twice (the last line ended up on the - // window's bottom edge). Which reports move the scroll offset: - // `RowGeometry.contentTopAfterReport`. + // window's bottom edge). Which reports move the scroll offset, + // and how: `RowGeometry.reportAction`. let top = self.contentTopY() - let anchored = self.lastAnchor.flatMap { $0.blockId == blockId ? $0.offsetFromBlockTop : nil } - let newTop = RowGeometry.contentTopAfterReport(contentTop: top, blockTop: self.blockTopY(row), - delta: delta, anchoredOffset: anchored) + let anchorRow = self.anchoredRow(contentTop: top) + let action = RowGeometry.reportAction(row: row, anchorRow: anchorRow, + blockTop: self.blockTopY(row), contentTop: top) NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0 tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) NSAnimationContext.endGrouping() - if let newTop { - self.setContentTop(newTop, animated: false) + switch action { + case .keep: + break + case .shift where self.isJumping, .placeAnchor: + // Re-aim a jump in flight at its target's new place, or put + // the reader back into the block that just reported. + if let anchorRow, let anchor = self.anchor { + self.place(row: anchorRow, offset: anchor.offset, animated: self.isJumping) + } + case .shift: + let reached = self.setContentTop(top + delta, animated: false) + if let anchorRow, self.anchor != nil { + // Moved with the block, unless AppKit clamped it. + self.anchor?.placed = reached - self.blockTopY(anchorRow) + } } #if DEBUG - listLog.debug("reportHeight: row \(row) (\(blockId, privacy: .public)) \(applied, format: .fixed(precision: 1)) → \(target, format: .fixed(precision: 1)) at top \(top, format: .fixed(precision: 1))\(newTop != nil ? " (compensated)" : anchored != nil ? " (anchor inside)" : "")") + listLog.debug("reportHeight: row \(row) (\(blockId, privacy: .public)) \(applied, format: .fixed(precision: 1)) → \(target, format: .fixed(precision: 1)) at top \(top, format: .fixed(precision: 1)), anchor row \(anchorRow ?? -1): \(String(describing: action), privacy: .public)\(self.isJumping ? " (jump)" : "", privacy: .public)") #endif // The parent holds the authoritative table (it feeds the next @@ -798,13 +864,15 @@ struct VirtualBlockList: NSViewRepresentable { guard let tableView, let scrollView, let row = rowForBlockId[request.blockId], row < tableView.numberOfRows else { return } + // The jump target is the reader's anchor from here on: a block above + // it that reports its real height while the scroll is in flight, or + // after it landed, moves the content top with it (`reportHeight`). let target: CGFloat switch request.anchor { case .top: // The block's top, below the space above it: the content inset // is then the gap above it, as above the document's first block. target = blockTopY(row) - noteAnchor(row: row, offsetFromBlockTop: 0) case .center: // The BLOCK's middle (not its row's, which includes the space // above it) at the centre of the CONTENT area (between the @@ -815,12 +883,11 @@ struct VirtualBlockList: NSViewRepresentable { target = RowGeometry.centerTarget(rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row), visibleHeight: contentHeight) - lastAnchor = nil } #if DEBUG listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(request.anchor == .top ? "top" : "center", privacy: .public) target=\(target, format: .fixed(precision: 1))") #endif - setContentTop(target, animated: request.animated) + place(row: row, offset: target - blockTopY(row), animated: request.animated) } // MARK: - Scroll offset arithmetic @@ -838,8 +905,16 @@ struct VirtualBlockList: NSViewRepresentable { + scrollView.contentInsets.top } - private func setContentTop(_ documentY: CGFloat, animated: Bool) { - guard let scrollView, let tableView else { return } + /// Scrolls so the content top is at `documentY`, as far as AppKit's + /// clamp allows; returns the content top it goes to. `completion` runs + /// when it is there (at once when nothing animates). + @discardableResult + private func setContentTop(_ documentY: CGFloat, animated: Bool, + completion: (() -> Void)? = nil) -> CGFloat { + guard let scrollView, let tableView else { + completion?() + return documentY + } let clipView = scrollView.contentView let rawY = documentY + tableView.frame.origin.y - scrollView.contentInsets.top // AppKit's own clamp: honours the content insets, the document @@ -851,8 +926,12 @@ struct VirtualBlockList: NSViewRepresentable { #if DEBUG listLog.debug("setContentTop: documentY=\(documentY, format: .fixed(precision: 1)) tableOrigin=\(tableView.frame.origin.y, format: .fixed(precision: 1)) tableH=\(tableView.frame.height, format: .fixed(precision: 1)) clipOrigin=\(clipView.bounds.origin.y, format: .fixed(precision: 1)) clipH=\(clipView.bounds.height, format: .fixed(precision: 1)) insetTop=\(scrollView.contentInsets.top, format: .fixed(precision: 1)) rawY=\(rawY, format: .fixed(precision: 1)) constrained=\(constrained.y, format: .fixed(precision: 1)) animated=\(animated)") #endif + let reached = constrained.y - tableView.frame.origin.y + scrollView.contentInsets.top guard abs(constrained.y - clipView.bounds.origin.y) > 0.01 - || abs(constrained.x - clipView.bounds.origin.x) > 0.01 else { return } + || abs(constrained.x - clipView.bounds.origin.x) > 0.01 else { + completion?() + return reached + } if animated { NSAnimationContext.runAnimationGroup({ context in @@ -860,14 +939,16 @@ struct VirtualBlockList: NSViewRepresentable { context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) clipView.animator().setBoundsOrigin(constrained) }, completionHandler: { [weak scrollView] in - guard let scrollView else { return } - scrollView.reflectScrolledClipView(scrollView.contentView) + if let scrollView { scrollView.reflectScrolledClipView(scrollView.contentView) } + completion?() }) scrollView.reflectScrolledClipView(clipView) } else { clipView.scroll(to: constrained) scrollView.reflectScrolledClipView(clipView) + completion?() } + return reached } // MARK: - contentWidth reporting (D4) @@ -955,9 +1036,7 @@ struct VirtualBlockList: NSViewRepresentable { // (a 24 pt jolt on every toggle); keeping the raw clip origin // means restoring the document offset minus that delta. let delta = layoutStyle.edgeInset - previousInset - let offset = anchor.offsetFromBlockTop - delta - setContentTop(contentTop(anchorOffset: offset, row: anchor.row), animated: false) - noteAnchor(row: anchor.row, offsetFromBlockTop: offset) + place(row: anchor.row, offset: anchor.offsetFromBlockTop - delta, animated: false) } } #if DEBUG diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index c4392a8..3520442 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -336,12 +336,46 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertFalse(RowGeometry.isInUpperHalf(y: 140, rowRect: row, spacingAbove: 20)) } - // MARK: - Scroll anchor across layouts (Reading Mode, width, zoom) + // MARK: - Scroll anchor across layouts (Reading Mode, width, zoom, jumps) + + /// `VirtualBlockList.Coordinator`'s scrolling over a model column, step by + /// step as the coordinator runs it: capture → new table → place the + /// anchor (`install`), the reading-mode toggle, jumps, and the height + /// reports of the `.reported` rows that are on screen. The decisions are + /// the coordinator's own (`RowGeometry.isAt`, `anchorPlacement`, + /// `reportAction`); `current == false` replays the rules of the branch + /// before this fix, as a control: re-anchor on the row under the content + /// top every time, place the offset as is, compensate any block whose top + /// is above the content top. + private struct ListModel { + struct Row { + var spacing: CGFloat + /// What the list lays the block out at (the height table). + var height: CGFloat + /// What the block's view measures. + var real: CGFloat + /// `.reported`: its view reports `real` once it is on screen. + var reported: Bool + + static func exact(_ spacing: CGFloat, _ height: CGFloat) -> Row { + Row(spacing: spacing, height: height, real: height, reported: false) + } + static func reported(_ spacing: CGFloat, _ estimate: CGFloat, real: CGFloat) -> Row { + Row(spacing: spacing, height: estimate, real: real, reported: true) + } + } + + var rows: [Row] + var contentTop: CGFloat + var current = true + var inset: CGFloat = 24 + var clipHeight: CGFloat = 828 + var anchor: (row: Int, offset: CGFloat, placed: CGFloat)? + /// A jump in flight: the content top is still on its way to here. + var jumpTarget: CGFloat? + /// Rows whose view reported since the last table (`reportedBlocks`). + var confirmed: Set = [] - /// A column of rows as `NSTableView` lays it out with no intercell - /// spacing: each row is its space above plus its block. - private struct Column { - let rows: [(spacing: CGFloat, height: CGFloat)] var rects: [NSRect] { var y: CGFloat = 0 return rows.map { row in @@ -352,104 +386,257 @@ final class VerticalRhythmTests: XCTestCase { func blockTop(_ row: Int) -> CGFloat { RowGeometry.topTarget(rowRect: rects[row], spacingAbove: rows[row].spacing) } - } + /// Where block `row`'s top is on screen, from the top of the clip view. + func screenY(_ row: Int) -> CGFloat { blockTop(row) - (contentTop - inset) } + func rowUnderContentTop() -> Int { + rects.firstIndex { contentTop >= $0.minY && contentTop < $0.maxY } ?? rows.count - 1 + } + /// The rows AppKit has views for: those in the clip view, whose top + /// inset shows content too. + var rowsOnScreen: [Int] { + let top = contentTop - inset + return rects.indices.filter { rects[$0].maxY > top && rects[$0].minY < top + clipHeight } + } - /// `VirtualBlockList.Coordinator`'s anchor bookkeeping over a `Column`: - /// `captureAnchor` (the last anchor while the content top is still on it, - /// else the row under the content top), `restore`, the reading-mode toggle - /// and the install of a table measured at another width. - private struct Scroller { - var column: Column - var contentTop: CGFloat - /// Off = re-anchor on the row under the content top every time (the - /// first version of the branch). - let sticky: Bool - var last: (row: Int, offset: CGFloat)? - - mutating func capture() -> (row: Int, offset: CGFloat) { - let rects = column.rects - if sticky, let last, RowGeometry.isAt(anchorOffset: last.offset, contentTop: contentTop, - rowRect: rects[last.row], - spacingAbove: column.rows[last.row].spacing) { - return last - } - let row = rects.firstIndex { contentTop >= $0.minY && contentTop < $0.maxY } ?? rects.count - 1 + func anchoredRow() -> Int? { + guard current, let anchor else { return nil } + if jumpTarget != nil { return anchor.row } + return RowGeometry.isAt(anchorOffset: anchor.placed, contentTop: contentTop, + rowRect: rects[anchor.row], spacingAbove: rows[anchor.row].spacing) + ? anchor.row : nil + } + func capture() -> (row: Int, offset: CGFloat) { + if let row = anchoredRow(), let anchor { return (row, anchor.offset) } + let row = rowUnderContentTop() return (row, RowGeometry.anchorOffset(contentTop: contentTop, rowRect: rects[row], - spacingAbove: column.rows[row].spacing)) + spacingAbove: rows[row].spacing)) } - mutating func restore(_ anchor: (row: Int, offset: CGFloat)) { - contentTop = RowGeometry.contentTop(anchorOffset: anchor.offset, rowRect: column.rects[anchor.row], - spacingAbove: column.rows[anchor.row].spacing) - last = anchor + mutating func place(_ row: Int, _ offset: CGFloat) { + let placed = current + ? RowGeometry.anchorPlacement(offset: offset, blockHeight: rows[row].height, topInset: inset, + heightIsEstimate: rows[row].reported && !confirmed.contains(row)) + : offset + if jumpTarget != nil { + jumpTarget = blockTop(row) + placed + } else { + contentTop = blockTop(row) + placed + } + anchor = (row, offset, placed) + } + mutating func report(_ row: Int) { + let top = contentTop + let delta = rows[row].real - rows[row].height + let blockTop = blockTop(row) + let anchorRow = anchoredRow() + rows[row].height = rows[row].real + guard current else { + if blockTop < top, jumpTarget == nil { contentTop = top + delta } + return + } + switch RowGeometry.reportAction(row: row, anchorRow: anchorRow, blockTop: blockTop, contentTop: top) { + case .keep: + break + case .shift where jumpTarget != nil, .placeAnchor: + if let anchor { place(anchor.row, anchor.offset) } + case .shift: + contentTop = top + delta + } + } + /// Every view on screen reports; a report can bring new rows on screen. + mutating func settle() { + for _ in 0..<50 { + confirmed.formUnion(rowsOnScreen.filter { rows[$0].reported }) + guard let row = rowsOnScreen.first(where: { + rows[$0].reported && abs(rows[$0].real - rows[$0].height) >= 0.5 + }) else { return } + report(row) + } + XCTFail("the reports never settled") } - /// `applyLayoutStyle`: the anchor minus the inset delta. - mutating func toggleStyle(insetDelta: CGFloat) { - let anchor = capture() - restore((anchor.row, anchor.offset - insetDelta)) + /// A new table (zoom, width): capture, install, place, reports. + mutating func install(_ new: [Row], settling: Bool = true) { + let captured = capture() + rows = new + anchor = nil + jumpTarget = nil + confirmed = [] + place(captured.row, captured.offset) + if settling { settle() } } - /// `install` of the table measured at the new width. - mutating func install(_ new: Column) { - let anchor = capture() - column = new - restore(anchor) + /// `applyLayoutStyle` mid-document: the anchor minus the inset delta. + mutating func toggleStyle(inset newInset: CGFloat) { + let captured = capture() + let delta = newInset - inset + inset = newInset + place(captured.row, captured.offset - delta) + } + /// A `.center` jump that lands after the views on screen reported. + mutating func centerJump(to row: Int) { + let target = RowGeometry.centerTarget(rowRect: rects[row], spacingAbove: rows[row].spacing, + visibleHeight: clipHeight - 2 * inset) + jumpTarget = target + anchor = (row, target - blockTop(row), target - blockTop(row)) + settle() // still at the start of the glide + contentTop = jumpTarget ?? target + jumpTarget = nil + settle() // landed + } + /// The block's middle on screen minus the content area's middle. + func offCentre(_ row: Int) -> CGFloat { + let middle = blockTop(row) + rows[row].height / 2 + return (middle - contentTop) - (clipHeight - 2 * inset) / 2 } } - /// Reading Mode on and off, step by step as the coordinator runs it: the - /// style toggle restores the anchor minus the inset delta (a 24 pt move of - /// the content top, across a row boundary here), then the table measured - /// at the new width is installed and the anchor restored again. The - /// narrow layout has taller rows above the reader AND less space between - /// the two blocks the content top moves between (a block's edges can - /// change with the wrap); the content top starts inside the gap. - func testReadingModeRoundTripReturnsToTheSameBlockOffset() { - let wide = Column(rows: [(0, 39), (19, 17), (25, 17), (25, 17), (21, 66), (21, 17), (50, 33), (12, 186), (5, 79)]) - let narrow = Column(rows: [(0, 39), (19, 34), (25, 34), (25, 34), (21, 66), (13, 17), (50, 33), (12, 186), (5, 79)]) - let standardInset: CGFloat = 24, readingInset: CGFloat = 48 - var drifted = false + /// fixture.md around the image, as at 100 % zoom (`scale` 1), wrapped at + /// `wrap` lines per short paragraph (2 = Reading Mode's 720 pt): the + /// image is a placeholder in every new table (`imageEstimate`) and + /// `imageReal` once loaded; the diagram starts at the 200 pt default. + private func fixtureRows(scale s: CGFloat = 1, wrap: CGFloat = 1, imageReal: CGFloat = 655, + imageEstimate: CGFloat = 116, diagram: CGFloat = 94) -> [ListModel.Row] { + let paragraph = { (spacing: CGFloat) in ListModel.Row.exact(spacing * s, 17 * s * wrap) } + var rows: [ListModel.Row] = [.reported(0, 39 * s, real: 39 * s)] // # Rhythm fixture + rows += (0..<10).map { _ in paragraph(25) } // the long list + rows += [ + .reported(50 * s, 33 * s, real: 33 * s), // ## Image and caption + .reported(16 * s, imageEstimate, real: imageReal), // the image + .exact(17 * s, 17 * s), // its caption + .reported(50 * s, 33 * s, real: 33 * s), // ## Math + paragraph(19), + .reported(13 * s, 64 * s, real: 64 * s), // a formula + paragraph(13), + .exact(21 * s, 66 * s), // the latex fallback + paragraph(21), + .reported(50 * s, 33 * s, real: 33 * s), // ## Diagram + .reported(12 * s, 224, real: diagram + 24), // the diagram + .exact(5 * s, 79 * s), // a table + .reported(50 * s, 39 * s, real: 39 * s), // # Second part + ] + rows += (0..<12).map { _ in paragraph(25) } + return rows + } + private let image = 12, secondPart = 23 + + /// A model list on fixture.md with every view reported, the content top + /// `offset` below the image's top. + private func listInsideTheImage(at offset: CGFloat, current: Bool) -> ListModel { + var rows = fixtureRows() + for index in rows.indices { rows[index].height = rows[index].real } + var list = ListModel(rows: rows, contentTop: 0, current: current) + list.contentTop = list.blockTop(image) + offset + list.anchor = nil + return list + } + + /// ⌘+ ⌘+ ⌘− ⌘− with the viewport top inside the image, only its bottom + /// edge visible. Every zoom re-parses: the image comes back as a 116 pt + /// placeholder. The reader stays in the image at every step and comes + /// back to the very same point. + func testZoomRoundTripKeepsAReaderInsideAnImage() { + for current in [true, false] { + var list = listInsideTheImage(at: 600, current: current) + let start = list.contentTop + var lostTheImage = false + for scale: CGFloat in [1.1, 1.25, 1.1, 1] { + list.install(fixtureRows(scale: scale)) + if list.rowUnderContentTop() != image { lostTheImage = true } + } + if current { + XCTAssertFalse(lostTheImage) + XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + } else { + // The regression: placed 600 pt into a 116 pt placeholder, the + // image is off screen, never reports, and the reader is left + // in the blocks after it. + XCTAssertTrue(lostTheImage) + XCTAssertGreaterThan(abs(list.contentTop - start), 10) + } + } + } - for start in stride(from: CGFloat(150), through: 330, by: 7.5) { - for sticky in [true, false] { - var list = Scroller(column: wide, contentTop: start, sticky: sticky) - list.toggleStyle(insetDelta: readingInset - standardInset) - // The block the reader was anchored on, and where they saw it. - let block = list.last?.row ?? 0 - let before = wide.blockTop(block) - start + standardInset + /// Reading Mode on and off with the viewport top inside the image (the + /// debug log: `row 21 + 579.5` on the way in, `row 29 + 0.0` on the way + /// out). At 720 pt the image is smaller and the short paragraphs wrap. + func testReadingModeRoundTripKeepsAReaderInsideAnImage() { + for current in [true, false] { + var list = listInsideTheImage(at: 603.5, current: current) + let start = list.contentTop + list.toggleStyle(inset: 48) + list.install(fixtureRows(wrap: 2, imageReal: 576)) + if current { + // Still on the image: its bottom edge in the top inset. + XCTAssertEqual(list.anchor?.row, image) + XCTAssertEqual(list.contentTop - list.blockTop(image), 579.5, accuracy: 0.001) + } + list.toggleStyle(inset: 24) + list.install(fixtureRows()) + if current { + XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + } else { + XCTAssertGreaterThan(abs(list.contentTop - start), 10) + } + } + } + /// A second re-measure before the image had a chance to report (a width + /// change right after a zoom): the anchor is still the image and its + /// offset, not the row the placeholder left under the content top. + func testAnchorInsideAPlaceholderSurvivesAnotherReinstall() { + var list = listInsideTheImage(at: 550, current: true) + let start = list.contentTop + list.install(fixtureRows(), settling: false) + list.install(fixtureRows(), settling: false) + list.settle() + XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + } + + /// Reading Mode on and off with the content top in the space above a + /// block: the toggle's 24 pt move crosses into the row above, and the + /// narrow layout has less space between those two blocks. Sticky anchor + /// = exact; re-anchoring on the row under the content top drifts. + func testReadingModeRoundTripWithTheReaderInAGap() { + let wide: [ListModel.Row] = [.exact(0, 39), .exact(19, 17), .exact(25, 17), .exact(25, 17), .exact(21, 66), + .exact(21, 17), .exact(50, 33), .exact(12, 186), .exact(5, 79)] + var narrow = wide + for row in 1...3 { narrow[row].height = 34; narrow[row].real = 34 } + narrow[5].spacing = 13 + var drifted = false + for start in stride(from: CGFloat(150), through: 330, by: 7.5) { + for current in [true, false] { + var list = ListModel(rows: wide, contentTop: start, current: current) + list.toggleStyle(inset: 48) + let block = list.capture().row + let seen = list.screenY(block) list.install(narrow) - if sticky { - XCTAssertEqual(narrow.blockTop(block) - list.contentTop + readingInset, before + 48, - accuracy: 0.001, "entering at \(start)") + if current { + XCTAssertEqual(list.screenY(block), seen, accuracy: 0.001, "entering at \(start)") } - - list.toggleStyle(insetDelta: standardInset - readingInset) + list.toggleStyle(inset: 24) list.install(wide) - if sticky { + if current { XCTAssertEqual(list.contentTop, start, accuracy: 0.001, "round trip from \(start)") } else if abs(list.contentTop - start) > 0.5 { drifted = true } } } - // Why the anchor is sticky: re-anchoring on the row under the content - // top lands on block 4 after the toggle and comes back by block 5 — so - // the round trip moves by the change in the space between them. XCTAssertTrue(drifted) } - /// A zoom (or any re-measure) while the reader is inside an image: the - /// image comes back at its placeholder height and the restore puts the - /// content top past it; a second re-measure before the image reported - /// must not anchor on whatever row is now under the content top. - func testAnchorInsideAPlaceholderSurvivesAnotherReinstall() { - let loaded = Column(rows: [(0, 39), (16, 640), (17, 17), (50, 33), (19, 17), (25, 17), (25, 17)]) - let placeholder = Column(rows: [(0, 39), (16, 116), (17, 17), (50, 33), (19, 17), (25, 17), (25, 17)]) - let start = loaded.blockTop(1) + 550 - var list = Scroller(column: loaded, contentTop: start, sticky: true) - list.install(placeholder) // re-parse: the image is a placeholder again - list.install(placeholder) // width change before it reported - list.install(loaded) // the image reported (no compensation: see the report tests) - XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + /// ⌘F "Second part" from the top of the document: the image above the + /// heading loads while the scroll is in flight, the diagram above it + /// reports after it landed. The heading ends in the middle (before: at + /// the bottom of the window, in 1.14.4 as well). + func testSearchJumpStaysCentredWhileTheBlocksAboveReport() { + for current in [true, false] { + var list = ListModel(rows: fixtureRows(), contentTop: 0, current: current) + list.centerJump(to: secondPart) + if current { + XCTAssertEqual(list.offCentre(secondPart), 0, accuracy: 0.001) + } else { + XCTAssertGreaterThan(list.offCentre(secondPart), 300) + } + } } func testAnchorIsRelativeToTheBlockNotTheRowEdge() { @@ -462,27 +649,31 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(RowGeometry.contentTop(anchorOffset: offset, rowRect: regapped, spacingAbove: 45), 2025) } + func testAnAnchorPastAnEstimateIsPlacedInsideTheBlock() { + // 600 pt into an image whose placeholder is 116 pt: on its last point. + XCTAssertEqual(RowGeometry.anchorPlacement(offset: 600, blockHeight: 116, topInset: 24, heightIsEstimate: true), 115) + // Still partly visible in the top inset: as is. + XCTAssertEqual(RowGeometry.anchorPlacement(offset: 130, blockHeight: 116, topInset: 24, heightIsEstimate: true), 130) + // A measured block keeps the offset (the toggle's 24 pt can pass its end). + XCTAssertEqual(RowGeometry.anchorPlacement(offset: 600, blockHeight: 116, topInset: 24, heightIsEstimate: false), 600) + // In the gap above the block. + XCTAssertEqual(RowGeometry.anchorPlacement(offset: -20, blockHeight: 116, topInset: 24, heightIsEstimate: true), -20) + } + // MARK: - Height reports - func testReportAboveTheViewportIsCompensated() { - // A block wholly above, and one straddling the content top. - XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 100, delta: 24, anchoredOffset: nil), 524) - XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 480, delta: -37.5, anchoredOffset: nil), 462.5) - // A block in view grows below the reader: nothing to do. - XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 500, delta: 24, anchoredOffset: nil)) - XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 500, blockTop: 620, delta: 24, anchoredOffset: nil)) - } - - func testReportOfTheAnchoredBlockKeepsTheReaderInIt() { - // After a zoom the reader was 550 pt into an image that came back at a - // 116 pt placeholder: the restore put the content top 550 below its - // block top. The image then reports its real height — the reader must - // stay 550 pt into it, not move on by the placeholder's error. - XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 1550, blockTop: 1000, delta: 524, anchoredOffset: 550)) - // A diagram that came back TALLER than it is (the 200 pt default). - XCTAssertNil(RowGeometry.contentTopAfterReport(contentTop: 1150, blockTop: 1000, delta: -37.5, anchoredOffset: 150)) - // The reader scrolled since the restore: an ordinary straddling block. - XCTAssertEqual(RowGeometry.contentTopAfterReport(contentTop: 1300, blockTop: 1000, delta: 524, anchoredOffset: 550), 1824) + func testReportsMoveTheContentTopAroundTheAnchor() { + // No anchor (the reader scrolled): blocks above the content top, the + // straddling one included, shift it; blocks in view don't. + XCTAssertEqual(RowGeometry.reportAction(row: 3, anchorRow: nil, blockTop: 100, contentTop: 500), .shift) + XCTAssertEqual(RowGeometry.reportAction(row: 3, anchorRow: nil, blockTop: 480, contentTop: 500), .shift) + XCTAssertEqual(RowGeometry.reportAction(row: 3, anchorRow: nil, blockTop: 500, contentTop: 500), .keep) + XCTAssertEqual(RowGeometry.reportAction(row: 3, anchorRow: nil, blockTop: 620, contentTop: 500), .keep) + // An anchor: above it shifts — in view above a jump target too —, the + // anchored block is placed again, below it nothing. + XCTAssertEqual(RowGeometry.reportAction(row: 3, anchorRow: 7, blockTop: 620, contentTop: 500), .shift) + XCTAssertEqual(RowGeometry.reportAction(row: 7, anchorRow: 7, blockTop: 480, contentTop: 500), .placeAnchor) + XCTAssertEqual(RowGeometry.reportAction(row: 8, anchorRow: 7, blockTop: 480, contentTop: 500), .keep) } func testTableRowHeightIsSpacingPlusBlock() { From 8d8da439ee73ebb6ab7246ef3c01ed628c10ca4e Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:15:30 +0100 Subject: [PATCH 010/105] Print/PDF: a heading never ends a page Orbit.md on Letter ended page 1 with "## Configuration" and started its table on page 2 (1.14.4 did the same). PrintRhythm.paginate now takes whole blocks and returns the printed slices. A run of headings (an H2 and the H3 under it) that would end a page moves to the next page with the block after it, when the run and that block fit on one page; otherwise moving would only orphan it there, and it stays. A block taller than a page is not moved: its first slice fills the rest of the heading's page (the run moves first when not even two lines fit below it), the rest is a page per slice as before. A run that already starts its page never moves, so nothing loops. The PDF export builds its pages from those slices; the screen path is untouched. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownExport.swift | 51 +++---- QuickMD/QuickMD/VerticalRhythm.swift | 134 +++++++++++++++--- .../QuickMDTests/VerticalRhythmTests.swift | 69 +++++++-- 3 files changed, 188 insertions(+), 66 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index aa66e47..ac72086 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -751,23 +751,24 @@ class PDFExportManager { let spacing = BlockLayout.Spacing.column(rhythm, fontScale: 1) // Paginate: a block taller than a page is sliced; only its first slice - // has the block's space above it. - var segments: [BlockSegment] = [] - var segmentSpacing: [CGFloat] = [] - for (index, measured) in measuredBlocks.enumerated() { - let slices = sliceBlockIfOversized(block: measured.block, size: measured.size, maxHeight: contentHeight) - for (slice, segment) in slices.enumerated() { - segments.append(segment) - segmentSpacing.append(slice == 0 ? spacing[index] : 0) - } - } - let placements = PrintRhythm.paginate( - zip(segments, segmentSpacing).map { (height: $0.sliceHeight, spacingAbove: $1) }, + // has the block's space above it, and a heading never ends a page + // without the start of the block after it (`PrintRhythm.paginate`). + let slices = PrintRhythm.paginate( + measuredBlocks.enumerated().map { index, measured in + let isHeading: Bool + if case .heading = measured.block.content { isHeading = true } else { isHeading = false } + return PrintRhythm.Block(height: measured.size.height, spacingAbove: spacing[index], + keepsWithNext: isHeading) + }, pageHeight: contentHeight) var pages: [[PlacedSegment]] = [[]] - for (segment, placement) in zip(segments, placements) { - while pages.count <= placement.page { pages.append([]) } - pages[placement.page].append(PlacedSegment(segment: segment, yOffset: placement.y)) + for slice in slices { + while pages.count <= slice.page { pages.append([]) } + let measured = measuredBlocks[slice.block] + pages[slice.page].append(PlacedSegment( + segment: BlockSegment(block: measured.block, size: measured.size, + sliceOffset: slice.offset, sliceHeight: slice.height), + yOffset: slice.y)) } let pdfData = NSMutableData() @@ -822,26 +823,6 @@ class PDFExportManager { return pdfData as Data } - private static func sliceBlockIfOversized(block: MarkdownBlock, size: CGSize, maxHeight: CGFloat) -> [BlockSegment] { - guard size.height > maxHeight, size.height > 0 else { - return [BlockSegment(block: block, size: size, sliceOffset: 0, sliceHeight: size.height)] - } - - var segments: [BlockSegment] = [] - var offset: CGFloat = 0 - while offset < size.height { - let sliceHeight = min(maxHeight, size.height - offset) - segments.append(BlockSegment( - block: block, - size: size, - sliceOffset: offset, - sliceHeight: sliceHeight - )) - offset += sliceHeight - } - return segments - } - private static func showError(_ message: String) { let alert = NSAlert() alert.messageText = "Export Error" diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 192e65e..309b355 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -347,36 +347,126 @@ enum PrintRhythm { return ns } - /// Where a segment lands: its page and its y from the top of the page's - /// content area. - struct Placement: Equatable { + /// A block to print: its height, the space above it, and whether it must + /// not end a page (a heading keeps with the block after it). + struct Block: Equatable { + let height: CGFloat + let spacingAbove: CGFloat + let keepsWithNext: Bool + } + + /// One printed piece of a block — the whole block, or one slice of a + /// block taller than the room it gets: its part of the block (`offset`, + /// `height`), its page and its y from the top of the page's content area. + struct Slice: Equatable { + let block: Int + let offset: CGFloat + let height: CGFloat let page: Int let y: CGFloat } - /// Lays `segments` (a block, or one slice of a block taller than a page) - /// out top to bottom on pages `pageHeight` tall. + /// The least of a sliced block that has to fit below a heading kept with + /// it: two lines of body text. + static let minimumFirstSlice: CGFloat = 40 + + /// Lays `blocks` out top to bottom on pages `pageHeight` tall. /// - /// A segment gets its `spacingAbove` unless it is first on its page — the - /// page's margin already separates it from the page above. A segment that - /// does not fit below the last one starts a new page. - static func paginate(_ segments: [(height: CGFloat, spacingAbove: CGFloat)], - pageHeight: CGFloat) -> [Placement] { - var placements: [Placement] = [] - placements.reserveCapacity(segments.count) + /// - A block gets its `spacingAbove` unless it is first on its page — the + /// page's margin already separates it from the page above. + /// - A block that does not fit below the last one starts a new page; a + /// block taller than a page starts one and is sliced, a page per slice. + /// - Keep with next: a run of headings (`keepsWithNext`, an H2 and the H3 + /// under it) never ends a page. When the block after the run starts a + /// new page, the run moves to that page with it — if the run and the + /// block fit on one page; otherwise nothing would be gained and the run + /// stays. A block taller than a page is not moved: its first slice + /// fills the rest of the run's page (moving the run first when not even + /// `minimumFirstSlice` fits there), so a heading is kept with that first + /// slice and nothing loops. + static func paginate(_ blocks: [Block], pageHeight: CGFloat) -> [Slice] { + var slices: [Slice] = [] + slices.reserveCapacity(blocks.count) var page = 0 - var bottom: CGFloat = 0 + var bottom: CGFloat = 0 // of the last slice on `page` var pageIsEmpty = true - for segment in segments { - var y = pageIsEmpty ? 0 : bottom + segment.spacingAbove - if !pageIsEmpty, y + segment.height > pageHeight { - page += 1 - y = 0 - } - placements.append(Placement(page: page, y: y)) - bottom = y + segment.height + + func put(_ block: Int, offset: CGFloat = 0, height: CGFloat, at y: CGFloat) { + slices.append(Slice(block: block, offset: offset, height: height, page: page, y: y)) + bottom = y + height pageIsEmpty = false } - return placements + func newPage() { + page += 1 + bottom = 0 + pageIsEmpty = true + } + /// The headings that end the current page right before `block`, as + /// indices into `slices` (empty: none). + func headingRun(before block: Int) -> Range { + var first = slices.count + var expected = block - 1 + while first > 0 { + let last = slices[first - 1] + guard last.page == page, last.block == expected, last.offset == 0, + blocks[last.block].keepsWithNext else { break } + first -= 1 + expected -= 1 + } + return first..) -> Bool { !run.isEmpty && slices[run.lowerBound].y > 0 } + /// Moves `run` to the top of a new page. + func moveToNewPage(_ run: Range) { + let moved = Array(slices[run]) + slices.removeSubrange(run) + newPage() + for slice in moved { + put(slice.block, height: slice.height, + at: pageIsEmpty ? 0 : bottom + blocks[slice.block].spacingAbove) + } + } + func runHeight(_ run: Range) -> CGFloat { + guard let first = run.first else { return 0 } + return slices[run.last ?? first].y + slices[run.last ?? first].height - slices[first].y + } + + for (index, block) in blocks.enumerated() { + let y = pageIsEmpty ? 0 : bottom + block.spacingAbove + let run = headingRun(before: index) + if block.height <= pageHeight { + if pageIsEmpty || y + block.height <= pageHeight { + put(index, height: block.height, at: y) + } else if canMove(run), runHeight(run) + block.spacingAbove + block.height <= pageHeight { + moveToNewPage(run) + put(index, height: block.height, at: bottom + block.spacingAbove) + } else { + newPage() + put(index, height: block.height, at: 0) + } + continue + } + + // Taller than a page: sliced, a page per slice — except the first + // slice after a heading run, which fills the rest of the run's page. + var offset: CGFloat = 0 + if !run.isEmpty, pageHeight - y < minimumFirstSlice, canMove(run) { + moveToNewPage(run) + } + let top = pageIsEmpty ? 0 : bottom + block.spacingAbove + if !run.isEmpty, pageHeight - top >= minimumFirstSlice { + let first = pageHeight - top + put(index, height: first, at: top) + offset = first + } + while offset < block.height { + if !pageIsEmpty { newPage() } + let height = min(pageHeight, block.height - offset) + put(index, offset: offset, height: height, at: 0) + offset += height + } + } + return slices } } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 3520442..1840a20 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -734,16 +734,67 @@ final class VerticalRhythmTests: XCTestCase { } } + private func printBlocks(_ blocks: [(CGFloat, CGFloat, Bool)]) -> [PrintRhythm.Block] { + blocks.map { PrintRhythm.Block(height: $0.0, spacingAbove: $0.1, keepsWithNext: $0.2) } + } + private func placed(_ slices: [PrintRhythm.Slice]) -> [String] { + slices.map { "\($0.block)@\($0.page):\(Int($0.y))" + ($0.offset > 0 || $0.height < 0 ? "+\(Int($0.offset))" : "") } + } + func testPaginationDropsTheSpaceAtAPageTop() { - let placements = PrintRhythm.paginate([(100, 0), (50, 30), (600, 30), (40, 30), (700, 30), (10, 0)], - pageHeight: 700) - XCTAssertEqual(placements, [ - .init(page: 0, y: 0), - .init(page: 0, y: 130), - .init(page: 1, y: 0), // 180 + 30 + 600 > 700: new page, no space above - .init(page: 1, y: 630), // 600 + 30 + 40 = 670 fits - .init(page: 2, y: 0), // a full page (a slice of a taller block) - .init(page: 3, y: 0), // the next slice: nothing fits below a full page + let slices = PrintRhythm.paginate(printBlocks([(100, 0, false), (50, 30, false), (600, 30, false), + (40, 30, false), (710, 30, false)]), + pageHeight: 700) + XCTAssertEqual(placed(slices), [ + "0@0:0", + "1@0:130", + "2@1:0", // 180 + 30 + 600 > 700: new page, no space above + "3@1:630", // 600 + 30 + 40 = 670 fits + "4@2:0", // taller than a page: a page per slice + "4@3:0+700", ]) + XCTAssertEqual(slices.map(\.height), [100, 50, 600, 40, 700, 10]) + } + + /// Orbit.md on Letter: `## Configuration` ended page 1 and its table + /// started page 2. + func testAHeadingMovesWithTheBlockThatStartsTheNextPage() { + let slices = PrintRhythm.paginate(printBlocks([(600, 0, false), (30, 50, true), (79, 20, false)]), + pageHeight: 700) + XCTAssertEqual(placed(slices), ["0@0:0", "1@1:0", "2@1:50"]) + } + + func testAHeadingChainMovesTogether() { + let slices = PrintRhythm.paginate(printBlocks([(560, 0, false), (30, 50, true), (25, 15, true), + (100, 20, false), (17, 25, false)]), + pageHeight: 700) + XCTAssertEqual(placed(slices), ["0@0:0", "1@1:0", "2@1:45", "3@1:90", "4@1:215"]) + } + + func testAHeadingStaysWhenItCannotShareAPageWithTheNextBlock() { + // 30 + 20 + 690 > 700: moving the heading would only orphan it on + // the next page. + let slices = PrintRhythm.paginate(printBlocks([(600, 0, false), (30, 50, true), (690, 20, false)]), + pageHeight: 700) + XCTAssertEqual(placed(slices), ["0@0:0", "1@0:650", "2@1:0"]) + // A heading that already starts its page is never moved (no loop). + let top = PrintRhythm.paginate(printBlocks([(30, 0, true), (690, 20, false), (17, 25, false)]), + pageHeight: 700) + XCTAssertEqual(placed(top), ["0@0:0", "1@1:0", "2@2:0"]) + } + + func testAHeadingKeepsWithTheFirstSliceOfABlockTallerThanAPage() { + // Room below the heading: the first slice fills the rest of its page. + let roomy = PrintRhythm.paginate(printBlocks([(300, 0, false), (30, 50, true), (1500, 20, false)]), + pageHeight: 700) + XCTAssertEqual(placed(roomy), ["0@0:0", "1@0:350", "2@0:400", "2@1:0+300", "2@2:0+1000"]) + XCTAssertEqual(roomy.map(\.height), [300, 30, 300, 700, 500]) + // No room: the heading moves, the first slice fills the rest of the + // new page, and every slice is placed exactly once. + let full = PrintRhythm.paginate(printBlocks([(600, 0, false), (30, 50, true), (1500, 20, false)]), + pageHeight: 700) + XCTAssertEqual(placed(full), ["0@0:0", "1@1:0", "2@1:50", "2@2:0+650", "2@3:0+1350"]) + XCTAssertEqual(full.filter { $0.block == 2 }.map(\.height).reduce(0, +), 1500) } + } From 03579d1f40f294af2fdbffc8d9378234d47de641 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:17:02 +0100 Subject: [PATCH 011/105] Rhythm: front matter the parser could not read is still front matter YAML that `FrontMatter` rejects (a nested map) falls back to a `yaml` code block at row 0, which got the box role: ~66 pt above the title instead of the 24.5 pt front matter gets, on screen and in print. The parser now marks the block it made from the front-matter fence (`MarkdownBlock.isFrontMatter`, either form) and the screen and print rhythm read that flag instead of guessing from the row: a user's own ```yaml fence on the first line stays an ordinary code block. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 19 +++++----- QuickMD/QuickMD/MarkdownBlock.swift | 22 ++++++++---- QuickMD/QuickMD/MarkdownBlockParser.swift | 5 +-- QuickMD/QuickMD/MarkdownExport.swift | 3 +- QuickMD/QuickMD/VerticalRhythm.swift | 14 +++----- .../QuickMDTests/VerticalRhythmTests.swift | 36 ++++++++++++++++--- 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index f4a4432..e9aae18 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -1024,7 +1024,7 @@ enum BlockHeightMeasurer { + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) kinds.append(.reported) } - rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } ?? boxRhythm(block, row: row)) + rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } ?? boxRhythm(block)) } #if DEBUG @@ -1058,26 +1058,23 @@ enum BlockHeightMeasurer { /// The rhythm input of a row whose edges its view fixes: the vertical /// padding between the row's edge and the visible box (or the picture). - /// `row` decides front matter: a properties table counts as front matter - /// only as the document's first block. - private static func boxRhythm(_ block: MarkdownBlock, row: Int) -> BlockLayout.Spacing.Row { + /// Front matter (either form) is the parser's call: `isFrontMatter`. + private static func boxRhythm(_ block: MarkdownBlock) -> BlockLayout.Spacing.Row { typealias Document = BlockLayout.Document let pad: CGFloat - var role = BlockLayout.Spacing.Role.box switch block.content { case .blockquote: pad = BlockLayout.Quote.verticalPadding case .alert: pad = Document.alertOuterVerticalPadding case .codeBlock: pad = Document.codeOuterVerticalPadding - case .table(_, _, _, let style): + case .table: pad = Document.tableOuterVerticalPadding - if style == .properties, row == 0 { role = .frontMatter } case .mathBlock: pad = Document.mathOuterVerticalPadding + BlockLayout.Math.verticalPadding case .mermaidDiagram: pad = Document.mermaidOuterVerticalPadding + BlockLayout.Mermaid.verticalPadding case .image, .svgImage: pad = Document.imageOuterVerticalPadding case .text, .heading: pad = 0 // measured, never here } - return BlockLayout.Spacing.Row(role: role, edges: .box(top: pad, bottom: pad), - joinsPrevious: block.joinsPrevious) + return BlockLayout.Spacing.Row(role: block.isFrontMatter ? .frontMatter : .box, + edges: .box(top: pad, bottom: pad), joinsPrevious: block.joinsPrevious) } /// One `.codeBlock` row: the code laid out at `codeWidth` plus `chrome` @@ -1155,7 +1152,7 @@ enum BlockHeightMeasurer { // guard only keeps a mismatched call from trapping. var rhythm = measured.rhythm.count == blocks.count ? measured.rhythm - : blocks.enumerated().map { boxRhythm($0.element, row: $0.offset) } + : blocks.map { boxRhythm($0) } let textWidth = BlockLayout.clampedWidth(contentWidth) for index in pending { @@ -1199,7 +1196,7 @@ enum BlockHeightMeasurer { codeWidth: BlockLayout.clampedWidth(contentWidth - 2 * BlockLayout.Code.horizontalPadding), chrome: BlockLayout.Code.verticalChrome(hasLanguage: !language.isEmpty, fontScale: fontScale)) kinds[index] = .exact - rhythm[index] = boxRhythm(block, row: index) + rhythm[index] = boxRhythm(block) default: continue diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift index 2e11277..fae325a 100644 --- a/QuickMD/QuickMD/MarkdownBlock.swift +++ b/QuickMD/QuickMD/MarkdownBlock.swift @@ -121,12 +121,20 @@ struct MarkdownBlock: Identifiable, Sendable { /// across the cut joins the two rows with a line break, not a blank line /// (`DocumentCopyBuilder`). let joinsPrevious: Bool - - init(id: String, content: BlockContent, sourceLine: Int, joinsPrevious: Bool = false) { + /// The document's YAML front matter — as the properties table, or as the + /// `yaml` code block it falls back to when `FrontMatter` cannot read it. + /// Set by the parser alone (only it knows where the fence was): a user's + /// own ```` ```yaml ```` fence on the first line is an ordinary code block. + /// The rhythm gives it its own role (`BlockLayout.Spacing.Role.frontMatter`). + let isFrontMatter: Bool + + init(id: String, content: BlockContent, sourceLine: Int, joinsPrevious: Bool = false, + isFrontMatter: Bool = false) { self.id = id self.content = content self.sourceLine = sourceLine self.joinsPrevious = joinsPrevious + self.isFrontMatter = isFrontMatter } enum BlockContent: Sendable { @@ -170,13 +178,15 @@ struct MarkdownBlock: Identifiable, Sendable { joinsPrevious: joinsPrevious) } static func table(index: Int, headers: [String], rows: [[String]], alignments: [TextAlignment], - style: TableStyle, sourceLine: Int) -> MarkdownBlock { + style: TableStyle, sourceLine: Int, isFrontMatter: Bool = false) -> MarkdownBlock { MarkdownBlock(id: "table-\(index)", content: .table(headers: headers, rows: rows, alignments: alignments, style: style), - sourceLine: sourceLine) + sourceLine: sourceLine, isFrontMatter: isFrontMatter) } - static func codeBlock(index: Int, code: String, language: String, sourceLine: Int) -> MarkdownBlock { - MarkdownBlock(id: "code-\(index)", content: .codeBlock(code: code, language: language), sourceLine: sourceLine) + static func codeBlock(index: Int, code: String, language: String, sourceLine: Int, + isFrontMatter: Bool = false) -> MarkdownBlock { + MarkdownBlock(id: "code-\(index)", content: .codeBlock(code: code, language: language), + sourceLine: sourceLine, isFrontMatter: isFrontMatter) } static func image(index: Int, url: String, alt: String, width: ImageWidth?, sourceLine: Int) -> MarkdownBlock { MarkdownBlock(id: "image-\(index)", content: .image(url: url, alt: alt, width: width), sourceLine: sourceLine) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index d3d33e6..93c7af8 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -64,9 +64,10 @@ struct MarkdownBlockParser: Sendable { blocks.append(.table(index: blockIndex, headers: [], rows: properties.map { [$0.key, $0.value] }, alignments: [.leading, .leading], style: .properties, - sourceLine: 0)) + sourceLine: 0, isFrontMatter: true)) } else { - blocks.append(.codeBlock(index: blockIndex, code: yaml, language: "yaml", sourceLine: 0)) + blocks.append(.codeBlock(index: blockIndex, code: yaml, language: "yaml", sourceLine: 0, + isFrontMatter: true)) } blockIndex += 1 } diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index ac72086..1f073ca 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -745,8 +745,7 @@ class PDFExportManager { // of the PRINTED views, at 1.0 zoom — print never follows the window's. let printRenderer = MarkdownRenderer(colorScheme: .light) let rhythm = measuredBlocks.enumerated().map { index, measured in - PrintRhythm.row(for: measured.block, isFirst: index == 0, - renderer: printRenderer, width: contentWidth) + PrintRhythm.row(for: measured.block, renderer: printRenderer, width: contentWidth) } let spacing = BlockLayout.Spacing.column(rhythm, fontScale: 1) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 309b355..1a7fc6b 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -310,10 +310,8 @@ enum PrintRhythm { /// styles, so their ink is measured without them. Every box (code, quote, /// alert, table, image, diagram) prints its background or its picture from /// its frame's edge; a display formula has its vertical padding. - /// - /// - Parameter isFirst: the block is the document's first (front matter). - static func row(for block: MarkdownBlock, isFirst: Bool, - renderer: MarkdownRenderer, width: CGFloat) -> BlockLayout.Spacing.Row { + /// Front matter (either form) is the parser's call: `isFrontMatter`. + static func row(for block: MarkdownBlock, renderer: MarkdownRenderer, width: CGFloat) -> BlockLayout.Spacing.Row { typealias Spacing = BlockLayout.Spacing let role: Spacing.Role let edges: Spacing.Edges @@ -325,17 +323,15 @@ enum PrintRhythm { role = .heading(level: level) edges = .text(BlockHeightMeasurer.textInk(text: printed(renderer.renderHeader(title, level: level)), width: width)) - case .table(_, _, _, let style): - role = style == .properties && isFirst ? .frontMatter : .box - edges = .box(top: 0, bottom: 0) case .mathBlock: role = .box edges = .box(top: BlockLayout.Math.verticalPadding, bottom: BlockLayout.Math.verticalPadding) - case .codeBlock, .blockquote, .alert, .image, .svgImage, .mermaidDiagram: + case .table, .codeBlock, .blockquote, .alert, .image, .svgImage, .mermaidDiagram: role = .box edges = .box(top: 0, bottom: 0) } - return Spacing.Row(role: role, edges: edges, joinsPrevious: block.joinsPrevious) + return Spacing.Row(role: block.isFrontMatter ? .frontMatter : role, edges: edges, + joinsPrevious: block.joinsPrevious) } /// The string as SwiftUI `Text` lays it out: no paragraph styles. diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 1840a20..b88f960 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -158,10 +158,36 @@ final class VerticalRhythmTests: XCTestCase { } func testFrontMatterToTitleIsOneMarginEdgeToEdge() { - let measured = measure(parse("---\ntitle: Notes\ntags: [a, b]\n---\n# Notes\n\nText.")) - XCTAssertEqual(measured.rhythm.first?.role, .frontMatter) - XCTAssertEqual(measured.rhythm.count, 3) - XCTAssertEqual(realizedGap(measured.rhythm, measured.table.spacingAbove, 1), 1.75 * 14, accuracy: tolerance) + // The properties table, and the yaml code block the parser falls back + // to when `FrontMatter` cannot read the YAML (a nested map here) — + // on screen and in print. + let renderer = MarkdownRenderer(colorScheme: .light) + for markdown in ["---\ntitle: Notes\ntags: [a, b]\n---\n# Notes\n\nText.", + "---\nauthor:\n name: X\n---\n# Notes\n\nText."] { + let blocks = parse(markdown) + XCTAssertTrue(blocks[0].isFrontMatter, markdown) + let measured = measure(blocks) + XCTAssertEqual(measured.rhythm.first?.role, .frontMatter, markdown) + XCTAssertEqual(measured.rhythm.count, 3) + XCTAssertEqual(realizedGap(measured.rhythm, measured.table.spacingAbove, 1), 1.75 * 14, + accuracy: tolerance, markdown) + let printRows = blocks.map { PrintRhythm.row(for: $0, renderer: renderer, width: 532) } + XCTAssertEqual(printRows.first?.role, .frontMatter, markdown) + XCTAssertEqual(realizedGap(printRows, Spacing.column(printRows, fontScale: 1), 1), 1.75 * 14, + accuracy: tolerance, markdown) + } + if case .codeBlock(_, let language) = parse("---\nauthor:\n name: X\n---\n# Notes")[0].content { + XCTAssertEqual(language, "yaml") + } else { + XCTFail("the fallback is a yaml code block") + } + } + + func testAYamlFenceOnTheFirstLineIsAnOrdinaryCodeBlock() { + let blocks = parse("```yaml\ntitle: Notes\n```\n# Notes") + XCTAssertFalse(blocks[0].isFrontMatter) + XCTAssertEqual(measure(blocks).rhythm.first?.role, .box) + XCTAssertFalse(parse("# Notes\n\n---\ntitle: x\n---")[1...].contains { $0.isFrontMatter }) } // MARK: - Between other blocks @@ -719,7 +745,7 @@ final class VerticalRhythmTests: XCTestCase { let blocks = MarkdownBlockParser(theme: printTheme, fontScale: 1).parse(markdown) let renderer = MarkdownRenderer(colorScheme: .light) let printRows = blocks.enumerated().map { - PrintRhythm.row(for: $0.element, isFirst: $0.offset == 0, renderer: renderer, width: 532) + PrintRhythm.row(for: $0.element, renderer: renderer, width: 532) } let printSpacing = Spacing.column(printRows, fontScale: 1) let screen = BlockHeightMeasurer.measure(blocks: blocks, theme: printTheme, fontScale: 1, From cb25b5fcdba0a450588b350f79b282a3c2a4c089 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:18:33 +0100 Subject: [PATCH 012/105] Parser: cut a row only where CommonMark lets a list interrupt A line like `1984. Something` inside a paragraph became its own row with a 24.5 pt gap above it. In CommonMark only a bullet item, or an ordered item numbered 1 (and not an empty one), interrupts a paragraph; any other item line is paragraph continuation. The parser now starts a row only there. The line is still drawn as a list item, as in 1.14.4. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 11 ++++++++--- QuickMD/QuickMD/MarkdownRenderer.swift | 16 +++++++++++++++- QuickMD/QuickMDTests/VerticalRhythmTests.swift | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 93c7af8..fd1ebb8 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -905,14 +905,19 @@ struct MarkdownBlockParser: Sendable { inList = false continue } - let isItem = MarkdownRenderer.isListItem(line) - if isItem, !inList { flushRow() } + // A list starts at an item that opens the row, or that may + // interrupt the paragraph before it (CommonMark: a bullet, or an + // ordered item numbered 1). Any other item line is paragraph text + // the renderer draws as an item, as it always did. + let startsList = !inList && MarkdownRenderer.isListItem(line) + && (row.isEmpty || MarkdownRenderer.canInterruptParagraph(line)) + if startsList { flushRow() } if row.count >= Self.textChunkLineLimit { flushRow() rowJoinsPrevious = true } append(position) - if isItem { inList = true } + if startsList { inList = true } } flushRow() } diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 6e38fa2..6b34535 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -305,9 +305,23 @@ struct MarkdownRenderer: Sendable { } /// A bullet, ordered or task list item — `renderLine` renders it as one. - /// The parser starts a new row at an item that follows paragraph text. static func isListItem(_ line: String) -> Bool { classify(line) == .listItem } + /// Whether this item line may interrupt a paragraph (CommonMark): a + /// bullet or task item, or an ordered item numbered 1 — and not an empty + /// one. Any other item line after paragraph text (`1984. was a year`) + /// continues the paragraph: it is still drawn as an item, but it is no + /// block boundary, so the parser does not start a new row there. + static func canInterruptParagraph(_ line: String) -> Bool { + guard isListItem(line) else { return false } + let trimmed = line.trimmingCharacters(in: .whitespaces) + if let marker = trimmed.range(of: #"^\d+\."#, options: .regularExpression) { + guard Int(trimmed[marker].dropLast()) == 1 else { return false } + return !trimmed[marker.upperBound...].trimmingCharacters(in: .whitespaces).isEmpty + } + return !trimmed.dropFirst(2).trimmingCharacters(in: .whitespaces).isEmpty + } + /// A thematic break (`---`, `***`, `___`) — `renderLine` draws it as a rule. static func isThematicBreak(_ line: String) -> Bool { line.trimmingCharacters(in: .whitespaces) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index b88f960..305384d 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -119,6 +119,23 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertTrue(blocks.allSatisfy { !$0.joinsPrevious }) } + /// CommonMark: only a bullet, or an ordered item numbered 1, interrupts a + /// paragraph. `1984. …` after paragraph text continues the paragraph — + /// drawn as an item as before, but no new row and no gap. + func testOnlyAnItemThatMayInterruptAParagraphStartsARow() { + for line in ["- item", "* item", "+ item", "- [ ] task", "1. first"] { + XCTAssertEqual(parse("Paragraph text.\n" + line).count, 2, line) + } + for line in ["1984. A year", "2. second", "-", "1."] { + XCTAssertEqual(parse("Paragraph text.\n" + line).count, 1, line) + } + // Still drawn on its own line, as an item. + let year = plain(parse("Paragraph text.\n1984. A year\n1985. Another")[0]) + XCTAssertTrue(year.contains("\n1984.") && year.contains("\n1985."), year) + // A list that starts a row is still a list, whatever its number. + XCTAssertEqual(parse("Text.\n\n3. third\n4. fourth").count, 2) + } + // MARK: - Headings func testEveryBlockUnderAHeadingGetsTheSameOpticalGap() { From 80c3954c34d4ad53f7e7957d18bf03e04af1c2d2 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:25:00 +0100 Subject: [PATCH 013/105] Rhythm: a quote is read by its text, a diagram by its ink Measured on fixture.md at 100 %: `## Quotes` to the quote's first line read 35 pt, `## Loose ordered list` to its first line 30.5 pt. The quote was measured to the top of its bar, which spans the text's line boxes, 3.5 pt above the cap top; the eye reads the text. - A quote's edges are now a paragraph's, set the quote's padding inside the row: optical on its text (cap top, last baseline) around a heading, its line boxes between blocks. Its text sits where a paragraph's would. - A diagram has no frame either: its first ink is Mermaid's 8 px diagram padding inside the picture (measured 8-9 pt), at the diagram's scale. That inset is now part of its edges, on screen and in a printed diagram picture. - A display formula's picture is its ink (measured headlessly with MathImage: within 1.5 pt at 1.0 and 1.5), alerts, code, tables and images keep their box edges, and so does a printed quote, which has a background. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 49 ++++++++++++++----- QuickMD/QuickMD/MarkdownExport.swift | 11 ++++- QuickMD/QuickMD/VerticalRhythm.swift | 23 ++++++++- .../QuickMDTests/VerticalRhythmTests.swift | 49 +++++++++++++++++++ 4 files changed, 116 insertions(+), 16 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index e9aae18..3de65be 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -432,6 +432,12 @@ enum BlockLayout { enum Mermaid { static let verticalPadding: CGFloat = 8 + /// Space between the diagram's box and its first ink, at 1.0: the + /// `diagramPadding` Mermaid puts around a flowchart, class or state + /// graph inside its SVG (8 px), drawn at the diagram's scale — the + /// zoom, for a diagram narrower than the column. The diagram has no + /// frame, so this is where the eye sees it start (measured: 8–9 pt). + static let inkInset: CGFloat = 8 /// Height a diagram starts at when nothing has measured it yet. static let defaultHeight: CGFloat = 200 static let cornerRadius: CGFloat = 6 @@ -912,8 +918,10 @@ enum BlockHeightMeasurer { for (row, block) in blocks.enumerated() { // Text and headings have their edges measured below; every other - // kind's are fixed by its view (`boxRhythm`). + // kind's are fixed by its view (`boxRhythm`) — a quote's from its + // text's ink. var textEdges: BlockLayout.Spacing.Edges? + var quoteInk: BlockLayout.Spacing.TextInk? switch block.content { case .text(let attributed): @@ -947,8 +955,9 @@ enum BlockHeightMeasurer { theme: theme, fontScale: fontScale, math: engine) converted[block.id] = ns let width = BlockLayout.Quote.bodyWidth(contentWidth: contentWidth, level: level) - heights.append(exactHeight(text: ns, width: width) - + 2 * BlockLayout.Quote.verticalPadding) + let ink = textInk(text: ns, width: width) + quoteInk = ink + heights.append(ink.height + 2 * BlockLayout.Quote.verticalPadding) kinds.append(.exact) case .alert(_, let content): @@ -1024,7 +1033,8 @@ enum BlockHeightMeasurer { + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) kinds.append(.reported) } - rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } ?? boxRhythm(block)) + rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } + ?? boxRhythm(block, fontScale: fontScale, quoteInk: quoteInk)) } #if DEBUG @@ -1057,24 +1067,37 @@ enum BlockHeightMeasurer { } /// The rhythm input of a row whose edges its view fixes: the vertical - /// padding between the row's edge and the visible box (or the picture). - /// Front matter (either form) is the parser's call: `isFrontMatter`. - private static func boxRhythm(_ block: MarkdownBlock) -> BlockLayout.Spacing.Row { + /// padding between the row's edge and the visible box (or the picture) — + /// where the eye sees the block start. Two blocks have no visible box: a + /// quote is read by its text (`quoteInk`, its edges are a paragraph's, + /// `Edges.text(_:padding:)`), a diagram by its first ink + /// (`Mermaid.inkInset`). A display formula's picture IS its ink (measured: + /// within 1.5 pt). Front matter (either form) is the parser's call. + private static func boxRhythm(_ block: MarkdownBlock, fontScale: CGFloat, + quoteInk: BlockLayout.Spacing.TextInk? = nil) -> BlockLayout.Spacing.Row { typealias Document = BlockLayout.Document + let role: BlockLayout.Spacing.Role = block.isFrontMatter ? .frontMatter : .box let pad: CGFloat switch block.content { - case .blockquote: pad = BlockLayout.Quote.verticalPadding + case .blockquote: + pad = BlockLayout.Quote.verticalPadding + if let quoteInk { + return BlockLayout.Spacing.Row(role: role, edges: .text(quoteInk, padding: pad), + joinsPrevious: block.joinsPrevious) + } case .alert: pad = Document.alertOuterVerticalPadding case .codeBlock: pad = Document.codeOuterVerticalPadding case .table: pad = Document.tableOuterVerticalPadding case .mathBlock: pad = Document.mathOuterVerticalPadding + BlockLayout.Math.verticalPadding - case .mermaidDiagram: pad = Document.mermaidOuterVerticalPadding + BlockLayout.Mermaid.verticalPadding + case .mermaidDiagram: + pad = Document.mermaidOuterVerticalPadding + BlockLayout.Mermaid.verticalPadding + + BlockLayout.Mermaid.inkInset * fontScale case .image, .svgImage: pad = Document.imageOuterVerticalPadding case .text, .heading: pad = 0 // measured, never here } - return BlockLayout.Spacing.Row(role: block.isFrontMatter ? .frontMatter : .box, - edges: .box(top: pad, bottom: pad), joinsPrevious: block.joinsPrevious) + return BlockLayout.Spacing.Row(role: role, edges: .box(top: pad, bottom: pad), + joinsPrevious: block.joinsPrevious) } /// One `.codeBlock` row: the code laid out at `codeWidth` plus `chrome` @@ -1152,7 +1175,7 @@ enum BlockHeightMeasurer { // guard only keeps a mismatched call from trapping. var rhythm = measured.rhythm.count == blocks.count ? measured.rhythm - : blocks.map { boxRhythm($0) } + : blocks.map { boxRhythm($0, fontScale: fontScale) } let textWidth = BlockLayout.clampedWidth(contentWidth) for index in pending { @@ -1196,7 +1219,7 @@ enum BlockHeightMeasurer { codeWidth: BlockLayout.clampedWidth(contentWidth - 2 * BlockLayout.Code.horizontalPadding), chrome: BlockLayout.Code.verticalChrome(hasLanguage: !language.isEmpty, fontScale: fontScale)) kinds[index] = .exact - rhythm[index] = boxRhythm(block) + rhythm[index] = boxRhythm(block, fontScale: fontScale) default: continue diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index 1f073ca..e22615b 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -744,8 +744,15 @@ class PDFExportManager { // The screen's vertical rhythm (`BlockLayout.Spacing`), from the edges // of the PRINTED views, at 1.0 zoom — print never follows the window's. let printRenderer = MarkdownRenderer(colorScheme: .light) - let rhythm = measuredBlocks.enumerated().map { index, measured in - PrintRhythm.row(for: measured.block, renderer: printRenderer, width: contentWidth) + let rhythm = measuredBlocks.map { measured in + let isPicture: Bool + if case .mermaidDiagram(let source) = measured.block.content { + isPicture = mermaidImages[source] != nil + } else { + isPicture = false + } + return PrintRhythm.row(for: measured.block, renderer: printRenderer, width: contentWidth, + diagramIsPicture: isPicture) } let spacing = BlockLayout.Spacing.column(rhythm, fontScale: 1) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 1a7fc6b..56ea8f7 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -97,6 +97,18 @@ extension BlockLayout { Edges(inkAbove: top, inkBelow: bottom, boxAbove: top, boxBelow: bottom) } + /// Text set `padding` points inside the row on both sides, with no + /// visible box around it: a quote, whose bars span the text's line + /// boxes — the eye reads the TEXT. Optical edges on its ink, box + /// edges on its line boxes, exactly as a paragraph's, so a quote's + /// text sits where a paragraph's would, under a heading and + /// between blocks. + static func text(_ ink: TextInk, padding: CGFloat) -> Edges { + Edges(inkAbove: padding + ink.firstBaseline - ink.firstCapHeight, + inkBelow: padding + ink.height - ink.lastBaseline, + boxAbove: padding, boxBelow: padding) + } + /// Text laid out as `ink` describes, `top` points below the row's /// top edge, in a row `rowHeight` tall (default: the text's own /// height plus `top`). Its line boxes are the row's edges. @@ -311,7 +323,13 @@ enum PrintRhythm { /// alert, table, image, diagram) prints its background or its picture from /// its frame's edge; a display formula has its vertical padding. /// Front matter (either form) is the parser's call: `isFrontMatter`. - static func row(for block: MarkdownBlock, renderer: MarkdownRenderer, width: CGFloat) -> BlockLayout.Spacing.Row { + /// + /// A printed quote has a background, so unlike the screen's it keeps its + /// box edge. A diagram printed as a picture (`diagramIsPicture`; without + /// one it prints its source as a code block) starts where its ink does, + /// `Mermaid.inkInset` inside the picture, as on screen. + static func row(for block: MarkdownBlock, renderer: MarkdownRenderer, width: CGFloat, + diagramIsPicture: Bool = false) -> BlockLayout.Spacing.Row { typealias Spacing = BlockLayout.Spacing let role: Spacing.Role let edges: Spacing.Edges @@ -326,6 +344,9 @@ enum PrintRhythm { case .mathBlock: role = .box edges = .box(top: BlockLayout.Math.verticalPadding, bottom: BlockLayout.Math.verticalPadding) + case .mermaidDiagram where diagramIsPicture: + role = .box + edges = .box(top: BlockLayout.Mermaid.inkInset, bottom: BlockLayout.Mermaid.inkInset) case .table, .codeBlock, .blockquote, .alert, .image, .svgImage, .mermaidDiagram: role = .box edges = .box(top: 0, bottom: 0) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 305384d..837964d 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -207,6 +207,55 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertFalse(parse("# Notes\n\n---\ntitle: x\n---")[1...].contains { $0.isFrontMatter }) } + /// Where the eye sees a block start under a heading: the heading's last + /// baseline to the next block's first ink — measured independently of + /// the rhythm's own edges. + private func eyeGapUnderHeading(_ follower: String, fontScale: CGFloat, + firstInk: (MarkdownBlock, MeasuredBlocks) -> CGFloat) -> CGFloat { + let blocks = parse("## Heading\n\n" + follower, fontScale: fontScale) + let measured = measure(blocks, fontScale: fontScale) + return measured.rhythm[0].edges.inkBelow + measured.table.spacingAbove[1] + firstInk(blocks[1], measured) + } + + /// The owner's measurement: `## Quotes` → the quote's first line read + /// 35 pt, `## Loose ordered list` → its first line 30.5 pt. The quote's + /// bar starts above its text; the eye reads the text. + func testAQuoteUnderAHeadingSitsWhereAParagraphWould() { + for scale: CGFloat in [1, 1.5] { + let capTop = { (block: MarkdownBlock, measured: MeasuredBlocks, padding: CGFloat, width: CGFloat) in + let ink = BlockHeightMeasurer.textInk(text: measured.converted[block.id]!, width: width) + return padding + ink.firstBaseline - ink.firstCapHeight + } + let paragraph = eyeGapUnderHeading("Some paragraph text.", fontScale: scale) { + capTop($0, $1, 0, BlockLayout.clampedWidth(640)) + } + let quote = eyeGapUnderHeading("> Some quoted text.", fontScale: scale) { + capTop($0, $1, BlockLayout.Quote.verticalPadding, + BlockLayout.Quote.bodyWidth(contentWidth: 640, level: 1)) + } + XCTAssertEqual(quote, paragraph, accuracy: 1, "at \(scale)") + // …and above a heading, from the quote's last baseline. + assertEveryGapOnTarget("> A quote.\n\n## Next", fontScale: scale) + } + } + + /// A diagram has no frame: the eye sees it start at its first ink, + /// Mermaid's padding inside the picture. + func testADiagramUnderAHeadingStartsWhereAParagraphWould() { + for scale: CGFloat in [1, 1.5] { + let paragraph = eyeGapUnderHeading("Some paragraph text.", fontScale: scale) { block, measured in + let ink = BlockHeightMeasurer.textInk(text: measured.converted[block.id]!, + width: BlockLayout.clampedWidth(640)) + return ink.firstBaseline - ink.firstCapHeight + } + let diagram = eyeGapUnderHeading("```mermaid\ngraph TD; A-->B\n```", fontScale: scale) { _, _ in + BlockLayout.Document.mermaidOuterVerticalPadding + BlockLayout.Mermaid.verticalPadding + + BlockLayout.Mermaid.inkInset * scale + } + XCTAssertEqual(diagram, paragraph, accuracy: 1, "at \(scale)") + } + } + // MARK: - Between other blocks func testNonHeadingPairsAreOneMarginApart() { From 4f6735185e3a9dfd3f33846f71181684daf6cc77 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:35:11 +0100 Subject: [PATCH 014/105] Measurer: an autorelease pool per text layout A prose document now has a row per paragraph, and one measure pass lays out every text row on its own TextKit stack; the autoreleased parts of those stacks stayed alive until the pass ended. 5000 short paragraphs: peak footprint 37.7 MB (1.14.4: 20.5 MB), now 24.7 MB; 2000 headings with a paragraph each: 26.7 MB in 1.14.4, now 16.5 MB. No time cost. The copy builder also reuses its two separator strings instead of making one per row. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 15 +++++++++++++-- QuickMD/QuickMD/DocumentSelection.swift | 5 +++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 3de65be..19c20fc 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -798,14 +798,25 @@ enum BlockHeightMeasurer { /// container height, `ensureLayout`, `ceil(usedRect.height)`. Anything else /// here breaks parity. static func exactHeight(text ns: NSAttributedString, width: CGFloat) -> CGFloat { - let (layoutManager, container, storage) = laidOut(ns, width: width) - return withExtendedLifetime(storage) { ceil(layoutManager.usedRect(for: container).height) } + autoreleasepool { + let (layoutManager, container, storage) = laidOut(ns, width: width) + return withExtendedLifetime(storage) { ceil(layoutManager.usedRect(for: container).height) } + } } /// `exactHeight`, plus where the text's ink starts and ends — the edges /// `BlockLayout.Spacing` measures optical gaps from. ONE layout pass: the /// baselines are read from the layout `exactHeight` computes anyway. + /// + /// Both run in their own autorelease pool: TextKit autoreleases parts of + /// every stack it lays out, and one measure pass lays out every text row — + /// thousands in a prose document, whose peak memory grew with the row + /// count (5000 paragraphs: 37.7 MB → 26.1 MB, no time cost). static func textInk(text ns: NSAttributedString, width: CGFloat) -> BlockLayout.Spacing.TextInk { + autoreleasepool { textInkInPool(text: ns, width: width) } + } + + private static func textInkInPool(text ns: NSAttributedString, width: CGFloat) -> BlockLayout.Spacing.TextInk { let (layoutManager, container, storage) = laidOut(ns, width: width) return withExtendedLifetime(storage) { let height = ceil(layoutManager.usedRect(for: container).height) diff --git a/QuickMD/QuickMD/DocumentSelection.swift b/QuickMD/QuickMD/DocumentSelection.swift index 904dbaa..e80da23 100644 --- a/QuickMD/QuickMD/DocumentSelection.swift +++ b/QuickMD/QuickMD/DocumentSelection.swift @@ -347,12 +347,13 @@ enum DocumentCopyBuilder { static func build(parts: [Part]) -> DocumentCopyOutput { let result = NSMutableAttributedString() + let rowBreak = NSAttributedString(string: rowSeparator) + let lineBreak = NSAttributedString(string: continuationSeparator) for part in parts { let text = attributed(for: part.piece) guard text.length > 0 else { continue } if result.length > 0 { - result.append(NSAttributedString(string: part.continuesPrevious ? continuationSeparator - : rowSeparator)) + result.append(part.continuesPrevious ? lineBreak : rowBreak) } result.append(text) } From f5bbb2da98640cee9c7ae029227765f8525604d9 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 19:54:03 +0100 Subject: [PATCH 015/105] fix(list): hold the anchor through a layout switch; seed pictures from reported heights Reading Mode off lost the reader by 37.5 pt: between the toggle and the install of the table measured for the new width, the clip origin moved without the list moving it, and the install re-captured the anchor by geometry on the block above. The toggle now pins the sticky anchor (isSwitchingLayout) until the install, the width report finding nothing to re-measure, or two seconds; reports during the switch re-place it. DEBUG builds log every clip move setContentTop did not make, with the stack that made it. A width re-measure sent every image back to its placeholder and every diagram to the default height (as 1.14.4 did). Heights reported by placed images, SVGs and Mermaid diagrams are now remembered by content, width, zoom and theme (ReportedHeights) and seed the next measure where the key matches, so Reading Mode on/off and a zoom round trip start from the real heights. BlockHeightCache moves next to the measurer. Tests replay the reported sequence (819 table with real heights, 720 re-measure, 819 re-measure, anchor in a gap, a -37.5 clip move during the switch) within 1 pt, with the old rules as a control. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 99 +++++++++++++++-- QuickMD/QuickMD/MarkdownView.swift | 18 ++- QuickMD/QuickMD/Views/TextBlockView.swift | 27 +---- QuickMD/QuickMD/Views/VirtualBlockList.swift | 103 +++++++++++++++++- .../QuickMDTests/VerticalRhythmTests.swift | 78 ++++++++++++- 5 files changed, 278 insertions(+), 47 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 19c20fc..073e88e 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -685,6 +685,77 @@ extension NSTextView { } } +// MARK: - Block Height Cache + +/// Remembers the last measured height of each block, keyed by block id. +/// LazyVStack destroys and recreates views during scroll; without this a +/// recreated view starts at a placeholder height and "jumps" when its real +/// height arrives, making upward scrolling stutter. Since 1.8.0 text and code +/// blocks size synchronously (`SelfSizingTextView.size(fitting:)`) and no longer +/// need it — Mermaid diagrams (height arrives from the WebView) still do. +/// +/// Main-thread only BY CONVENTION (all reads/writes happen from SwiftUI view +/// inits / height callbacks dispatched to main). Deliberately NOT @MainActor: +/// view initializers are nonisolated on older SDKs where the SwiftUI View +/// protocol annotates only `body`, and an isolated init call there is a hard +/// compile error (caught by CI on the GitHub runner toolchain). +final class BlockHeightCache { + private var heights: [String: CGFloat] = [:] + /// Row heights reported by placed pictures and diagrams, by + /// `ReportedHeights.key` — content keys, so a re-parse keeps them. + private var reported: [String: CGFloat] = [:] + + func height(for id: String) -> CGFloat? { heights[id] } + func set(_ height: CGFloat, for id: String) { heights[id] = height } + /// Forgets the block-id heights (a re-parse: ids are positional). The + /// reported heights stay — they are keyed by content. + func removeAll() { heights.removeAll() } + + func setReported(_ height: CGFloat, forKey key: String) { + // A live resize reports at every width it passes through: keep the + // cache small rather than complete. + if reported.count >= Self.reportedLimit, reported[key] == nil { reported.removeAll() } + reported[key] = height + } + static let reportedLimit = 512 + + /// A value copy for `BlockHeightMeasurer.measure(heightSeeds:)` — the + /// measurer runs off-main and must not read this reference type from there. + var snapshot: [String: CGFloat] { heights } + /// The same for `measure(reportedHeights:)`. + var reportedSnapshot: [String: CGFloat] { reported } +} + +// MARK: - Reported heights + +/// The heights the placed views of pictures and diagrams reported, by what +/// they show: a block id is a position, which a re-parse (zoom, an edit +/// elsewhere) reassigns, and a width change re-measures every row — the +/// image went back to its 116 pt placeholder and the diagram to the 200 pt +/// default each time, swinging the table by hundreds of points and moving +/// the reader with it. Keyed by the block's content and the width, zoom and +/// theme it was laid out at, a reported height is valid wherever the key +/// matches: Reading Mode on and off, a zoom and back. +enum ReportedHeights { + /// The key of `block`'s row at `contentWidth`, `fontScale` and `theme`; + /// nil for the kinds the measurer computes exactly (or estimates from + /// text, a heading). + static func key(for block: MarkdownBlock, contentWidth: CGFloat, fontScale: CGFloat, + theme: MarkdownTheme) -> String? { + let place = "\(Int(contentWidth.rounded()))|\(fontScale)|\(theme.name)|\(theme.isDark)" + switch block.content { + case .image(let url, let alt, let width): + return "image|\(place)|\(url)|\(alt)|\(String(describing: width))" + case .svgImage(let source): + return "svg|\(place)|\(source)" + case .mermaidDiagram(let source): + return "mermaid|\(place)|\(source)" + default: + return nil + } + } +} + // MARK: - Height table /// How a row's height was obtained, and therefore whether the placed view is @@ -866,6 +937,10 @@ enum BlockHeightMeasurer { /// the list column width minus `2 * Document.contentHorizontalPadding`. /// - heightSeeds: last-known heights (`BlockHeightCache`) used to seed /// Mermaid rows so a re-created diagram doesn't start at the default. + /// - reportedHeights: row heights the placed views of pictures and + /// diagrams reported, by `ReportedHeights.key` — what the block shows, + /// at which width, zoom and theme. Such a row starts at the height it + /// had there instead of the placeholder. /// - math: the math engine, or `nil` when the call is NOT on the main /// thread — see the thread rule in the file header. With `nil`, rows /// that need the engine come back at a placeholder height, listed in @@ -885,6 +960,7 @@ enum BlockHeightMeasurer { fontScale: CGFloat, contentWidth: CGFloat, heightSeeds: [String: CGFloat] = [:], + reportedHeights: [String: CGFloat] = [:], math: MathRendering?) -> MeasuredBlocks { #if DEBUG let signpostID = heightSignpost.makeSignpostID() @@ -1026,22 +1102,29 @@ enum BlockHeightMeasurer { // is a two-view tuple (image + italic caption `Text`), so the // placed view reports a taller row than this. It is `.reported` // precisely because the real size — decoded bitmap, caption or - // no caption — only exists once the view is placed. - heights.append(BlockLayout.ImageBlock.placeholderHeight - + 2 * BlockLayout.Document.imageOuterVerticalPadding) + // no caption — only exists once the view is placed. Unless it + // was placed before at this width and zoom (`reportedHeights`). + heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, + fontScale: fontScale, theme: theme) ?? ""] + ?? BlockLayout.ImageBlock.placeholderHeight + + 2 * BlockLayout.Document.imageOuterVerticalPadding) kinds.append(.reported) case .svgImage: // Same shape as `.image`: the decoded size only exists once the // view is placed (decode is off-main), so the row is reported. - heights.append(BlockLayout.ImageBlock.placeholderHeight - + 2 * BlockLayout.Document.imageOuterVerticalPadding) + heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, + fontScale: fontScale, theme: theme) ?? ""] + ?? BlockLayout.ImageBlock.placeholderHeight + + 2 * BlockLayout.Document.imageOuterVerticalPadding) kinds.append(.reported) case .mermaidDiagram: - heights.append((heightSeeds[block.id] ?? BlockLayout.Mermaid.defaultHeight) - + 2 * BlockLayout.Mermaid.verticalPadding - + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) + heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, + fontScale: fontScale, theme: theme) ?? ""] + ?? (heightSeeds[block.id] ?? BlockLayout.Mermaid.defaultHeight) + + 2 * BlockLayout.Mermaid.verticalPadding + + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) kinds.append(.reported) } rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index 12bfeea..732a384 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -651,6 +651,8 @@ struct MarkdownView: View { // first layout — then the heights task below picks it up as soon as // the coordinator reports one. let width = contentWidth + // Content-keyed (`ReportedHeights`): valid across this re-parse. + let reported = heightCache.reportedSnapshot let out: ParsedAndMeasured = await Task.detached(priority: .userInitiated) { let blocks = MarkdownBlockParser(theme: currentTheme, fontScale: scale).parse(text) // Pre-compute per-text-block metadata on the background thread so the @@ -674,11 +676,12 @@ struct MarkdownView: View { // the rows it defers are finished on main just below. // No `heightSeeds`: `heightCache` is cleared for this parse, and // block ids are positional, so the previous document's diagram - // heights would seed the wrong diagrams. + // heights would seed the wrong diagrams. `reportedHeights` are + // keyed by content, width and zoom instead. let measured = width > 0 ? BlockHeightMeasurer.measure(blocks: blocks, theme: currentTheme, fontScale: scale, contentWidth: width, - math: nil) + reportedHeights: reported, math: nil) : MeasuredBlocks.empty return ParsedAndMeasured(parsed: ParsedDocument(blocks: blocks, textMeta: meta), measured: measured) @@ -750,6 +753,9 @@ struct MarkdownView: View { // Mermaid rows: seed from the heights the WebViews actually reported, // so a re-measure doesn't send every diagram back to the 200 pt default. let seeds = heightCache.snapshot + // Pictures and diagrams placed at this width before (Reading Mode + // off again, a window back to its size): their reported heights. + let reported = heightCache.reportedSnapshot // Inline-math paragraphs: hand the previous pass's strings back so the // main-actor step re-wraps them instead of re-rendering every `$…$` // segment through SwiftMath. Safe only because this task never runs @@ -759,7 +765,8 @@ struct MarkdownView: View { let reusableStrings = measured.converted let raw: MeasuredBlocks = await Task.detached(priority: .userInitiated) { BlockHeightMeasurer.measure(blocks: blocks, theme: currentTheme, fontScale: scale, - contentWidth: width, heightSeeds: seeds, math: nil) + contentWidth: width, heightSeeds: seeds, + reportedHeights: reported, math: nil) }.value // A parse that landed while we were measuring owns the state now — // its own transaction published a matching table. @@ -1401,6 +1408,11 @@ struct MarkdownView: View { // ignores such reports too — this is the same rule on the parent's side, // so a stale report can never overwrite an exact height). guard row < table.kinds.count, table.kinds[row] == .reported else { return } + // Remembered by content, width and zoom for the next measure there. + if let key = ReportedHeights.key(for: cachedBlocks[row], contentWidth: table.contentWidth, + fontScale: CGFloat(renderedFontScale), theme: theme) { + heightCache.setReported(height, forKey: key) + } guard row < table.heights.count, abs(table.heights[row] - height) >= 0.5 else { return } // The block's height only: its space above stays as measured. measured = measured.replacingHeight(height, at: row) diff --git a/QuickMD/QuickMD/Views/TextBlockView.swift b/QuickMD/QuickMD/Views/TextBlockView.swift index 1f0b11c..75d424c 100644 --- a/QuickMD/QuickMD/Views/TextBlockView.swift +++ b/QuickMD/QuickMD/Views/TextBlockView.swift @@ -1,31 +1,8 @@ import SwiftUI import AppKit -// MARK: - Block Height Cache - -/// Remembers the last measured height of each block, keyed by block id. -/// LazyVStack destroys and recreates views during scroll; without this a -/// recreated view starts at a placeholder height and "jumps" when its real -/// height arrives, making upward scrolling stutter. Since 1.8.0 text and code -/// blocks size synchronously (`SelfSizingTextView.size(fitting:)`) and no longer -/// need it — Mermaid diagrams (height arrives from the WebView) still do. -/// -/// Main-thread only BY CONVENTION (all reads/writes happen from SwiftUI view -/// inits / height callbacks dispatched to main). Deliberately NOT @MainActor: -/// view initializers are nonisolated on older SDKs where the SwiftUI View -/// protocol annotates only `body`, and an isolated init call there is a hard -/// compile error (caught by CI on the GitHub runner toolchain). -final class BlockHeightCache { - private var heights: [String: CGFloat] = [:] - - func height(for id: String) -> CGFloat? { heights[id] } - func set(_ height: CGFloat, for id: String) { heights[id] = height } - func removeAll() { heights.removeAll() } - - /// A value copy for `BlockHeightMeasurer.measure(heightSeeds:)` — the - /// measurer runs off-main and must not read this reference type from there. - var snapshot: [String: CGFloat] { heights } -} +// `BlockHeightCache` lives in BlockHeightMeasurer.swift, next to the +// measurer that reads it (and the tests, which compile that file). // MARK: - Shared NSTextView Search Highlight diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index e3c51e4..2bc1f5b 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -389,6 +389,24 @@ struct VirtualBlockList: NSViewRepresentable { /// `.reported` blocks whose view has told this table its height: their /// height is no longer an estimate (`RowGeometry.anchorPlacement`). private var reportedBlocks: Set = [] + /// The layout style changed and the table measured for the new width + /// is not installed yet. Meanwhile the cells re-lay out at the new + /// width inside the old rows, and the clip origin can move without + /// this list moving it (seen: -37.5 pt between leaving Reading Mode + /// and the new table, which re-anchored the reader on the block + /// above). The anchor holds through the switch whatever the clip does; + /// the install puts the reader back on it. + private var isSwitchingLayout = false + private var layoutSwitchGeneration = 0 + #if DEBUG + /// Where the last `setContentTop` left the content top, to tell a clip + /// move this list made from one something else made (logged with the + /// stack that made it — `clipBoundsChanged`). + private var expectedContentTop: CGFloat? + private var isSettingContentTop = false + private var isLiveScrolling = false + private var debugObservers: [NSObjectProtocol] = [] + #endif private struct ReaderAnchor { let blockId: String @@ -443,6 +461,9 @@ struct VirtualBlockList: NSViewRepresentable { deinit { widthWork?.cancel() if let frameObserver { NotificationCenter.default.removeObserver(frameObserver) } + #if DEBUG + debugObservers.forEach { NotificationCenter.default.removeObserver($0) } + #endif } func attach(scrollView: BlockScrollView, tableView: BlockTableView) { @@ -461,7 +482,45 @@ struct VirtualBlockList: NSViewRepresentable { ) { [weak self] _ in self?.clipFrameChanged(afterLiveResize: false) } + #if DEBUG + observeClipMoves(scrollView) + #endif + } + + #if DEBUG + /// DEBUG: logs every move of the clip origin that `setContentTop` did + /// not make — outside a jump and a live (wheel, trackpad) scroll — with + /// the stack that made it. + private func observeClipMoves(_ scrollView: NSScrollView) { + let center = NotificationCenter.default + scrollView.contentView.postsBoundsChangedNotifications = true + debugObservers.append(center.addObserver(forName: NSView.boundsDidChangeNotification, + object: scrollView.contentView, queue: nil) { [weak self] _ in + self?.clipBoundsChanged() + }) + debugObservers.append(center.addObserver(forName: NSScrollView.willStartLiveScrollNotification, + object: scrollView, queue: nil) { [weak self] _ in + self?.isLiveScrolling = true + }) + debugObservers.append(center.addObserver(forName: NSScrollView.didEndLiveScrollNotification, + object: scrollView, queue: nil) { [weak self] _ in + self?.isLiveScrolling = false + self?.expectedContentTop = self?.contentTopY() + }) + } + + private func clipBoundsChanged() { + guard !isSettingContentTop, !isJumping, !isLiveScrolling, + let expected = expectedContentTop else { return } + let top = contentTopY() + guard abs(top - expected) > 0.5 else { return } + expectedContentTop = top + listLog.debug("clipMoved: content top \(expected, format: .fixed(precision: 1)) → \(top, format: .fixed(precision: 1)) NOT by setContentTop\(self.isSwitchingLayout ? " (layout switch)" : "", privacy: .public)") + for (index, frame) in Thread.callStackSymbols.prefix(40).enumerated() { + listLog.debug("clipMoved #\(index): \(frame, privacy: .public)") + } } + #endif /// One turn later: the request arrives in the same SwiftUI update that /// removes the overlay, whose views may still hold focus until then. @@ -514,6 +573,7 @@ struct VirtualBlockList: NSViewRepresentable { let previousCount = blocks.count self.anchor = nil isJumping = false + isSwitchingLayout = false reportedBlocks = [] blocks = newBlocks @@ -733,7 +793,7 @@ struct VirtualBlockList: NSViewRepresentable { private func anchoredRow(contentTop: CGFloat) -> Int? { guard let anchor, let tableView, let row = rowForBlockId[anchor.blockId], row < tableView.numberOfRows, row < blocks.count else { return nil } - if isJumping { return row } + if isJumping || isSwitchingLayout { return row } return RowGeometry.isAt(anchorOffset: anchor.placed, contentTop: contentTop, rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)) ? row : nil @@ -834,9 +894,11 @@ struct VirtualBlockList: NSViewRepresentable { switch action { case .keep: break - case .shift where self.isJumping, .placeAnchor: - // Re-aim a jump in flight at its target's new place, or put - // the reader back into the block that just reported. + case .shift where self.isJumping || self.isSwitchingLayout, .placeAnchor: + // Re-aim a jump in flight at its target's new place, put the + // reader back on the anchor during a layout switch (the clip + // may have moved under it), or into the block that just + // reported. if let anchorRow, let anchor = self.anchor { self.place(row: anchorRow, offset: anchor.offset, animated: self.isJumping) } @@ -938,16 +1000,26 @@ struct VirtualBlockList: NSViewRepresentable { context.duration = 0.25 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) clipView.animator().setBoundsOrigin(constrained) - }, completionHandler: { [weak scrollView] in + }, completionHandler: { [weak scrollView, weak self] in if let scrollView { scrollView.reflectScrolledClipView(scrollView.contentView) } + #if DEBUG + self?.expectedContentTop = self?.contentTopY() + #endif completion?() }) scrollView.reflectScrolledClipView(clipView) } else { + #if DEBUG + isSettingContentTop = true + defer { isSettingContentTop = false } + #endif clipView.scroll(to: constrained) scrollView.reflectScrolledClipView(clipView) completion?() } + #if DEBUG + expectedContentTop = reached + #endif return reached } @@ -1038,6 +1110,7 @@ struct VirtualBlockList: NSViewRepresentable { let delta = layoutStyle.edgeInset - previousInset place(row: anchor.row, offset: anchor.offsetFromBlockTop - delta, animated: false) } + beginLayoutSwitch() } #if DEBUG listLog.debug("layoutStyle: cap=\(style.maxContentWidth.map { "\(Int($0))" } ?? "none", privacy: .public) padding=\(style.verticalPadding, format: .fixed(precision: 0))") @@ -1050,6 +1123,20 @@ struct VirtualBlockList: NSViewRepresentable { scheduleWidthReport() } + /// `isSwitchingLayout` until the next install — or until the width + /// report finds nothing to re-measure, or two seconds, whichever comes + /// first, so a switch that never brings a new table cannot pin the + /// anchor for good. + private func beginLayoutSwitch() { + isSwitchingLayout = true + layoutSwitchGeneration += 1 + let switchGeneration = layoutSwitchGeneration + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + guard let self, self.layoutSwitchGeneration == switchGeneration else { return } + self.isSwitchingLayout = false + } + } + private func applyContentInsets() { guard let scrollView else { return } let inset = layoutStyle.edgeInset @@ -1118,7 +1205,11 @@ struct VirtualBlockList: NSViewRepresentable { return } let width = self.currentContentWidth() - guard width > 0, abs(width - self.reportedContentWidth) >= 1 else { return } + guard width > 0, abs(width - self.reportedContentWidth) >= 1 else { + // Same width: no new table is coming to end a layout switch. + self.isSwitchingLayout = false + return + } self.publish(width: width) } widthWork = work diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 837964d..2f77f04 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -467,6 +467,9 @@ final class VerticalRhythmTests: XCTestCase { var jumpTarget: CGFloat? /// Rows whose view reported since the last table (`reportedBlocks`). var confirmed: Set = [] + /// A layout switch is under way (`isSwitchingLayout`): from the + /// style toggle to the install of the table for the new width. + var switching = false var rects: [NSRect] { var y: CGFloat = 0 @@ -492,7 +495,7 @@ final class VerticalRhythmTests: XCTestCase { func anchoredRow() -> Int? { guard current, let anchor else { return nil } - if jumpTarget != nil { return anchor.row } + if jumpTarget != nil || switching { return anchor.row } return RowGeometry.isAt(anchorOffset: anchor.placed, contentTop: contentTop, rowRect: rects[anchor.row], spacingAbove: rows[anchor.row].spacing) ? anchor.row : nil @@ -528,7 +531,7 @@ final class VerticalRhythmTests: XCTestCase { switch RowGeometry.reportAction(row: row, anchorRow: anchorRow, blockTop: blockTop, contentTop: top) { case .keep: break - case .shift where jumpTarget != nil, .placeAnchor: + case .shift where jumpTarget != nil || switching, .placeAnchor: if let anchor { place(anchor.row, anchor.offset) } case .shift: contentTop = top + delta @@ -551,6 +554,7 @@ final class VerticalRhythmTests: XCTestCase { rows = new anchor = nil jumpTarget = nil + switching = false confirmed = [] place(captured.row, captured.offset) if settling { settle() } @@ -561,7 +565,10 @@ final class VerticalRhythmTests: XCTestCase { let delta = newInset - inset inset = newInset place(captured.row, captured.offset - delta) + switching = current } + /// Something other than the list moves the clip origin. + mutating func clipMoved(by dy: CGFloat) { contentTop += dy } /// A `.center` jump that lands after the views on screen reported. mutating func centerJump(to row: Int) { let target = RowGeometry.centerTarget(rowRect: rects[row], spacingAbove: rows[row].spacing, @@ -585,7 +592,8 @@ final class VerticalRhythmTests: XCTestCase { /// image is a placeholder in every new table (`imageEstimate`) and /// `imageReal` once loaded; the diagram starts at the 200 pt default. private func fixtureRows(scale s: CGFloat = 1, wrap: CGFloat = 1, imageReal: CGFloat = 655, - imageEstimate: CGFloat = 116, diagram: CGFloat = 94) -> [ListModel.Row] { + imageEstimate: CGFloat = 116, diagram: CGFloat = 94, + diagramEstimate: CGFloat = 224) -> [ListModel.Row] { let paragraph = { (spacing: CGFloat) in ListModel.Row.exact(spacing * s, 17 * s * wrap) } var rows: [ListModel.Row] = [.reported(0, 39 * s, real: 39 * s)] // # Rhythm fixture rows += (0..<10).map { _ in paragraph(25) } // the long list @@ -600,14 +608,45 @@ final class VerticalRhythmTests: XCTestCase { .exact(21 * s, 66 * s), // the latex fallback paragraph(21), .reported(50 * s, 33 * s, real: 33 * s), // ## Diagram - .reported(12 * s, 224, real: diagram + 24), // the diagram + .reported(12 * s, diagramEstimate, real: diagram + 24), // the diagram .exact(5 * s, 79 * s), // a table .reported(50 * s, 39 * s, real: 39 * s), // # Second part ] rows += (0..<12).map { _ in paragraph(25) } return rows } - private let image = 12, secondPart = 23 + private let image = 12, textAfterFormulas = 19, secondPart = 23 + + /// The debug log of the drift (`full.log`): at 819 pt every height real, + /// the content top 11.5 pt above "Text after the formulas"; Reading Mode + /// re-measures at 720 (the image back at its placeholder, the diagram + /// seeded); between leaving Reading Mode and the 819 table the clip moves + /// by -37.5 pt without the list moving it; the 819 re-measure has the + /// image at its placeholder again. The reader comes back to the point. + func testReadingModeRoundTripSurvivesAClipMoveDuringTheSwitch() { + for current in [true, false] { + var rows = fixtureRows() + for index in rows.indices { rows[index].height = rows[index].real } + var list = ListModel(rows: rows, contentTop: 0, current: current) + list.contentTop = list.blockTop(textAfterFormulas) - 11.5 + list.confirmed = Set(rows.indices) + let seen = list.screenY(textAfterFormulas) + + list.toggleStyle(inset: 48) + list.install(fixtureRows(wrap: 2, diagramEstimate: 118)) + list.toggleStyle(inset: 24) + list.clipMoved(by: -37.5) + list.install(fixtureRows(diagramEstimate: 118)) + + if current { + XCTAssertEqual(list.screenY(textAfterFormulas), seen, accuracy: 1) + XCTAssertEqual(list.contentTop - list.blockTop(textAfterFormulas), -11.5, accuracy: 0.001) + } else { + // Re-anchored by geometry on the latex block: 37.5 pt off. + XCTAssertEqual(list.screenY(textAfterFormulas) - seen, 37.5, accuracy: 1) + } + } + } /// A model list on fixture.md with every view reported, the content top /// `offset` below the image's top. @@ -731,6 +770,35 @@ final class VerticalRhythmTests: XCTestCase { } } + /// A picture or a diagram measured again where it was placed before + /// (Reading Mode off, a zoom back) starts at the height it reported + /// there, not at the placeholder; elsewhere it still starts at the + /// placeholder. Keyed by content, so a re-parse keeps it. + func testAReportedPictureHeightSeedsTheNextMeasureAtThatWidth() { + let blocks = parse("Text.\n\n![QuickMD icon](icon.png)\n\n```mermaid\ngraph TD; A-->B\n```") + let keys = [1, 2].map { ReportedHeights.key(for: blocks[$0], contentWidth: 819, fontScale: 1, theme: theme)! } + let seeds = [keys[0]: 655, keys[1]: 94] as [String: CGFloat] + let seeded = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, contentWidth: 819, + reportedHeights: seeds, math: math) + XCTAssertEqual(seeded.table.heights[1], 655) + XCTAssertEqual(seeded.table.heights[2], 94) + let elsewhere = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, contentWidth: 720, + reportedHeights: seeds, math: math) + XCTAssertEqual(elsewhere.table.heights[1], BlockLayout.ImageBlock.placeholderHeight + + 2 * BlockLayout.Document.imageOuterVerticalPadding) + XCTAssertNotEqual(elsewhere.table.heights[2], 94) + // Another picture at the same position is another key. + XCTAssertNotEqual(ReportedHeights.key(for: parse("Text.\n\n![other](other.png)")[1], contentWidth: 819, + fontScale: 1, theme: theme), keys[0]) + // A re-parse forgets the block-id heights, not the reported ones. + let cache = BlockHeightCache() + cache.set(70, for: "mermaid-2") + cache.setReported(655, forKey: keys[0]) + cache.removeAll() + XCTAssertNil(cache.height(for: "mermaid-2")) + XCTAssertEqual(cache.reportedSnapshot[keys[0]], 655) + } + func testAnchorIsRelativeToTheBlockNotTheRowEdge() { let rect = NSRect(x: 0, y: 1000, width: 500, height: 80) // In the gap above the block: negative, and it stays the same distance From 7ddc1ce689993c9e975104651bbed4ee6974b289 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:06:52 +0100 Subject: [PATCH 016/105] fix(list): a text row under the top inset no longer scrolls the document on resize The -24.5 / -37.5 pt on leaving Reading Mode: NSTextView keeps its visible part visible while it resizes (_setFrameSize:forceScroll: -> scrollRectToVisible:). A text view lying in the list's top content inset is visible to it but not to scrollRectToVisible:, which measures against the inset-adjusted area, so every width change with a text row under the top inset scrolled the document until that row's text sat at the content top (both GUI logs: the latex row's text, "row 27 + 38"). The document list's text views now derive from DocumentCellTextView, which refuses a scroll requested from inside its own frame change; an explicit request (VoiceOver) still scrolls. The layout-switch pin stays as the second line. A Mermaid diagram at a zoom it was never placed at started at the 200 pt default and swung the table by ~120 pt on every zoom step. Its height is now remembered per unit of zoom (from the web view and the snapshot path) and seeds the measurer and the view's first frame, scaled. Tests: the failing run replayed on the fixture's real 41-row tables (search jump at -351.5, Reading Mode on, 720 table, off, a -24.5 clip move, 819 table) lands on row 32 at -351.5; the 4f67351 rules reproduce the log's "row 27 + 38" at 1452.0 as the control. AppKit's resize scroll is pinned with a plain NSTextView as the control. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 73 +++++++- QuickMD/QuickMD/Views/CodeBlockView.swift | 2 +- QuickMD/QuickMD/Views/MermaidBlockView.swift | 12 +- QuickMD/QuickMD/Views/VirtualBlockList.swift | 10 +- .../QuickMDTests/VerticalRhythmTests.swift | 164 +++++++++++++++++- 5 files changed, 251 insertions(+), 10 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 073e88e..6ecb963 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -685,6 +685,42 @@ extension NSTextView { } } +// MARK: - Document cell text view + +/// The base of every text view the document list hosts (`SelfSizingTextView`, +/// `TableTextView`): a text view that never scrolls the document because it +/// was resized. +/// +/// NSTextView keeps the part of itself that is visible visible while it +/// resizes: `_setFrameSize:forceScroll:` → `scrollRectToVisible:` on the +/// enclosing clip view. A text view lying in the list's top content inset is +/// visible to it but not to `scrollRectToVisible:`, which measures against the +/// inset-adjusted area — so any width change (Reading Mode, a sidebar, a window +/// resize) with a text row under the top inset scrolled the DOCUMENT until that +/// row's text sat at the content top (seen: -24.5 and -37.5 pt on leaving +/// Reading Mode, which moved the reader). These views are cells of the list, +/// and the list owns the scroll position. An explicit request (VoiceOver's +/// scroll to element) still scrolls. +/// +/// Here rather than next to `SelfSizingTextView` so the unit tests, which +/// compile this file, run the very class. +class DocumentCellTextView: NSTextView { + /// Nesting depth of `setFrameSize` — AppKit re-enters it from inside, so a + /// flag would be cleared by the inner call before the outer one scrolls. + private var frameSizeChanges = 0 + + override func setFrameSize(_ newSize: NSSize) { + frameSizeChanges += 1 + defer { frameSizeChanges -= 1 } + super.setFrameSize(newSize) + } + + override func scrollToVisible(_ rect: NSRect) -> Bool { + if frameSizeChanges > 0 { return false } + return super.scrollToVisible(rect) + } +} + // MARK: - Block Height Cache /// Remembers the last measured height of each block, keyed by block id. @@ -711,6 +747,13 @@ final class BlockHeightCache { /// reported heights stay — they are keyed by content. func removeAll() { heights.removeAll() } + /// A diagram's height as its web view (or snapshot) gave it, per unit of + /// zoom (`ReportedHeights.diagramHeight`). + func setDiagramHeight(_ height: CGFloat, source: String, isDark: Bool, fontScale: CGFloat) { + guard fontScale > 0, height > 0 else { return } + setReported(height / fontScale, forKey: ReportedHeights.diagramUnitKey(source: source, isDark: isDark)) + } + func setReported(_ height: CGFloat, forKey key: String) { // A live resize reports at every width it passes through: keep the // cache small rather than complete. @@ -754,6 +797,25 @@ enum ReportedHeights { return nil } } + + /// A Mermaid diagram's height per unit of zoom, by what it shows. One that + /// fits its column is drawn at `fontScale` times its natural size + /// (mermaid-template.html), so a zoom it was never placed at starts from + /// the last one it was placed at, scaled — not from the 200 pt default + /// (which swung the table by ~120 pt on every zoom step). One wider than + /// its column is drawn smaller than that and reports it. + static func diagramUnitKey(source: String, isDark: Bool) -> String { + "mermaid-unit|\(isDark)|\(source)" + } + + /// The diagram height (without the block's padding) `source` is expected + /// to have at `fontScale`; nil if it was never placed. Shared by the + /// measurer and `MermaidBlockView`'s first frame, so the two agree. + static func diagramHeight(source: String, isDark: Bool, fontScale: CGFloat, + in reported: [String: CGFloat]) -> CGFloat? { + // 50: the web view's minimum (`MermaidWebView`). + reported[diagramUnitKey(source: source, isDark: isDark)].map { max(50, $0 * fontScale) } + } } // MARK: - Height table @@ -1119,11 +1181,16 @@ enum BlockHeightMeasurer { + 2 * BlockLayout.Document.imageOuterVerticalPadding) kinds.append(.reported) - case .mermaidDiagram: + case .mermaidDiagram(let source): + // This block's last height at this zoom (a width-only re-measure), + // else the diagram's height at another zoom, scaled. + let diagram = heightSeeds[block.id] + ?? ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, + fontScale: fontScale, in: reportedHeights) + ?? BlockLayout.Mermaid.defaultHeight heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, fontScale: fontScale, theme: theme) ?? ""] - ?? (heightSeeds[block.id] ?? BlockLayout.Mermaid.defaultHeight) - + 2 * BlockLayout.Mermaid.verticalPadding + ?? diagram + 2 * BlockLayout.Mermaid.verticalPadding + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) kinds.append(.reported) } diff --git a/QuickMD/QuickMD/Views/CodeBlockView.swift b/QuickMD/QuickMD/Views/CodeBlockView.swift index 0a9681d..0ed2e68 100644 --- a/QuickMD/QuickMD/Views/CodeBlockView.swift +++ b/QuickMD/QuickMD/Views/CodeBlockView.swift @@ -331,7 +331,7 @@ private struct CodeTextView: NSViewRepresentable { /// measured the text and pushed it into SwiftUI `@State` (`.frame(height:)`) /// on the next runloop turn, so every freshly created block changed size after /// appearing. See constraints.md ("Block heights are synchronous"). -class SelfSizingTextView: NSTextView { +class SelfSizingTextView: DocumentCellTextView { /// Identity reference to the last-applied attributed string. Lets the wrapper /// short-circuit `updateNSView` work when SwiftUI hands us the same instance. var cachedAttributed: NSAttributedString? diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index 178e72d..a92426d 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -63,7 +63,13 @@ struct MermaidBlockView: View { self.fontScale = fontScale self.contentWidth = contentWidth self.onEnlarge = onEnlarge - _diagramHeight = State(initialValue: heightCache.height(for: blockId) ?? Metrics.defaultHeight) + // The measurer's seed, so the first frame matches the table's row and + // the cell does not report the 200 pt default first. + _diagramHeight = State(initialValue: heightCache.height(for: blockId) + ?? ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, + fontScale: fontScale, + in: heightCache.reportedSnapshot) + ?? Metrics.defaultHeight) let cached = MermaidSnapshotStore.image(source: "\(fontScale)|\(contentWidth)|\(source)", isDark: theme.isDark) _snapshot = State(initialValue: cached) if let cached { @@ -73,6 +79,8 @@ struct MermaidBlockView: View { // (zoom, theme) — which empties the cache — measures the diagram at // the 200 pt default again and it reports back on screen. heightCache.set(cached.size.height, for: blockId) + heightCache.setDiagramHeight(cached.size.height, source: source, isDark: theme.isDark, + fontScale: fontScale) } } @@ -95,6 +103,8 @@ struct MermaidBlockView: View { diagramHeight: $diagramHeight, onHeight: { height in heightCache.set(height, for: blockId) + heightCache.setDiagramHeight(height, source: source, isDark: theme.isDark, + fontScale: fontScale) }, onSnapshot: { image in MermaidSnapshotStore.set(image, source: snapshotKey, isDark: theme.isDark) diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 2bc1f5b..92f4aea 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -392,10 +392,12 @@ struct VirtualBlockList: NSViewRepresentable { /// The layout style changed and the table measured for the new width /// is not installed yet. Meanwhile the cells re-lay out at the new /// width inside the old rows, and the clip origin can move without - /// this list moving it (seen: -37.5 pt between leaving Reading Mode - /// and the new table, which re-anchored the reader on the block - /// above). The anchor holds through the switch whatever the clip does; - /// the install puts the reader back on it. + /// this list moving it (seen: -24.5 and -37.5 pt between leaving + /// Reading Mode and the new table — a text view under the top inset + /// scrolling itself visible as it resized, now prevented by + /// `DocumentCellTextView` — which re-anchored the reader on the block + /// above). The anchor holds through the switch whatever moves the + /// clip; the install puts the reader back on it. private var isSwitchingLayout = false private var layoutSwitchGeneration = 0 #if DEBUG diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 2f77f04..077e75e 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -470,6 +470,9 @@ final class VerticalRhythmTests: XCTestCase { /// A layout switch is under way (`isSwitchingLayout`): from the /// style toggle to the install of the table for the new width. var switching = false + /// The style toggle pins the anchor until the install (false: the + /// rules of 4f67351, sticky anchor without the pin). + var pinsLayoutSwitch = true var rects: [NSRect] { var y: CGFloat = 0 @@ -565,7 +568,7 @@ final class VerticalRhythmTests: XCTestCase { let delta = newInset - inset inset = newInset place(captured.row, captured.offset - delta) - switching = current + switching = current && pinsLayoutSwitch } /// Something other than the list moves the clip origin. mutating func clipMoved(by dy: CGFloat) { contentTop += dy } @@ -648,6 +651,165 @@ final class VerticalRhythmTests: XCTestCase { } } + /// fixture.md row by row as the app measures it (41 rows): the measurer's + /// table at `width` (819 = the window, 720 = Reading Mode), with the two + /// formulas as SwiftMath and the 1.14.3 fallback leave them (65 pt; a + /// ```latex block, 21 pt above and below) and the diagram at + /// `diagramEstimate` (224 = the default, 94 = seeded). It reproduces the + /// debug log of the round trip to the point: table 3452 / 3322 / 3492 pt, + /// "Second part" at 1958 / 1828 / 1998. + private func searchFixtureRows(width: CGFloat, diagramEstimate: CGFloat) -> [ListModel.Row] { + let wrapped: CGFloat = width < 800 ? 34 : 17 // the ordered items + var rows: [ListModel.Row] = [ + .reported(0, 39, real: 39), // 0 # Rhythm fixture + .exact(19, 17), .exact(21, 66), .exact(21, 17), // 1-3 intro, bash, "Features:" + .exact(25, 34), // 4 the tight list + .reported(50, 33, real: 33), // 5 ## Quotes + .exact(17, 21), .exact(0, 21), .exact(0, 21), // 6-8 + .reported(48, 33, real: 33), // 9 ## Loose ordered list + .exact(19, wrapped), // 10 + ] + rows += (0..<9).map { _ in .exact(25, wrapped) } // 11-19 + rows += [ + .reported(50, 33, real: 33), // 20 ## Image and caption + .reported(16, 116, real: 655), // 21 the image (never on screen here) + .exact(17, 17), // 22 its caption + .reported(50, 33, real: 33), // 23 ## Math + .exact(19, 17), .exact(13, 65), .exact(13, 17), // 24-26 + .exact(21, 66), // 27 the formula SwiftMath refuses + .exact(21, 17), // 28 "Text after the formulas." + .reported(50, 33, real: 33), // 29 ## Diagram + .reported(4, diagramEstimate, real: 94), // 30 the diagram + .exact(0, 79), // 31 the table + .reported(50, 39, real: 39), // 32 # Second part + .reported(15, 29, real: 29), .reported(16, 25, real: 25), + .exact(19, 17), // 35 + .reported(50, 33, real: 33), // 36 ## Long list + .exact(19, 510), .exact(0, 510), .exact(0, 170), // 37-39 + .exact(25, 17), // 40 + ] + return rows + } + + /// A diagram placed at one zoom starts at another zoom at its height + /// there, scaled (fixture.md's diagram: 70 pt at 100 %, reported 77 and + /// 87.5 at 110 and 125 %), not at the 200 pt default; never placed, at the + /// default; and this block's own last height at this zoom still wins. + func testADiagramPlacedAtOneZoomStartsAnotherZoomScaled() throws { + let blocks = parse("```mermaid\nflowchart LR\n A[Start] --> B[End]\n```") + guard case .mermaidDiagram(let source) = blocks[0].content else { return XCTFail("not a diagram") } + let chrome = 2 * BlockLayout.Mermaid.verticalPadding + 2 * BlockLayout.Document.mermaidOuterVerticalPadding + let cache = BlockHeightCache() + cache.setDiagramHeight(70, source: source, isDark: theme.isDark, fontScale: 1) + cache.removeAll() // the re-parse of a zoom step + for (scale, diagram) in [(1.1, 77.0), (1.25, 87.5), (0.9, 63.0)] as [(CGFloat, CGFloat)] { + let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: scale, + contentWidth: 819, reportedHeights: cache.reportedSnapshot, + math: math) + XCTAssertEqual(measured.table.heights[0], diagram + chrome, accuracy: 0.001, "at \(scale)") + XCTAssertEqual(ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, fontScale: scale, + in: cache.reportedSnapshot) ?? 0, + diagram, accuracy: 0.001) + } + let unplaced = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1.25, + contentWidth: 819, math: math) + XCTAssertEqual(unplaced.table.heights[0], BlockLayout.Mermaid.defaultHeight + chrome) + let ownSeed = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1.25, contentWidth: 720, + heightSeeds: [blocks[0].id: 80], + reportedHeights: cache.reportedSnapshot, math: math) + XCTAssertEqual(ownSeed.table.heights[0], 80 + chrome) + } + + /// The failing GUI run (f1dbg.log), step by step: ⌘F "Second part" and a + /// zoom round trip left the anchor on the H1 at -351.5 with the diagram + /// still at its default; it reports 94 and the content top shifts to + /// 1476.5. Reading Mode on (1452.5), the 720 table (1622.5), Reading Mode + /// off (1646.5) — then, before the 819 table, the latex row's text view + /// re-lays out under the top inset and AppKit scrolls the document by + /// -24.5 pt (now prevented: `DocumentCellTextView`). The pin must hold + /// whatever moves the clip. The reader must come + /// back to row 32 at -351.5, the very point it left. + func testReadingModeRoundTripFromASearchJumpLandsWhereItLeft() { + let secondPart = 32 + for pins in [true, false] { + var list = ListModel(rows: searchFixtureRows(width: 819, diagramEstimate: 224), contentTop: 0) + list.pinsLayoutSwitch = pins + list.contentTop = list.blockTop(secondPart) - 351.5 + list.anchor = (secondPart, -351.5, -351.5) + XCTAssertEqual(list.contentTop, 1606.5) + list.settle() + XCTAssertEqual(list.contentTop, 1476.5) + let seen = list.screenY(secondPart) + + list.toggleStyle(inset: 48) + XCTAssertEqual(list.contentTop, 1452.5) + list.install(searchFixtureRows(width: 720, diagramEstimate: 94)) + XCTAssertEqual(list.contentTop, 1622.5) + list.toggleStyle(inset: 24) + XCTAssertEqual(list.contentTop, 1646.5) + list.clipMoved(by: -24.5) + list.install(searchFixtureRows(width: 819, diagramEstimate: 94)) + + if pins { + XCTAssertEqual(list.anchor?.row, secondPart) + XCTAssertEqual(list.anchor?.offset, -351.5) + XCTAssertEqual(list.contentTop, 1476.5) + XCTAssertEqual(list.screenY(secondPart), seen, accuracy: 0.001) + } else { + // The log, to the point: re-captured by geometry as "row 27 + + // 38" in the 720 table and restored at 1452.0, 24.5 pt off. + XCTAssertEqual(list.anchor?.row, 27) + XCTAssertEqual(list.anchor?.offset, 38) + XCTAssertEqual(list.contentTop, 1452.0) + XCTAssertEqual(list.screenY(secondPart) - seen, 24.5, accuracy: 0.001) + } + } + } + + /// The -24.5 / -37.5 pt itself: AppKit's NSTextView keeps its visible part + /// visible while it resizes (`_setFrameSize:forceScroll:` → + /// `scrollRectToVisible:`), and a text view lying in the list's top content + /// inset is visible to it but not to `scrollRectToVisible:` — every width + /// change scrolled the document until that text sat at the content top. + /// `DocumentCellTextView` (the base of `SelfSizingTextView`) never scrolls + /// on a frame change; a plain NSTextView in the same place still does (the + /// control), and an explicit request still scrolls. + func testATextRowUnderTheTopInsetDoesNotScrollTheDocumentWhenItResizes() { + final class Flipped: NSView { override var isFlipped: Bool { true } } + func clipMove(_ textView: NSTextView, _ change: (NSTextView) -> Void) -> CGFloat { + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled], backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + defer { window.close() } + let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) + scrollView.automaticallyAdjustsContentInsets = false + scrollView.contentInsets = NSEdgeInsets(top: 24, left: 0, bottom: 24, right: 0) + let document = Flipped(frame: NSRect(x: 0, y: 0, width: 800, height: 5000)) + scrollView.documentView = document + window.contentView = scrollView + // The latex row's text: its top 0.5 pt below the clip's top edge, + // all of it inside the 24 pt inset (content top 1067.5). + textView.configureForSelfSizing() + textView.frame = NSRect(x: 20, y: 1044, width: 700, height: 17) + textView.textStorage?.setAttributedString(NSAttributedString(string: "V(e_1) \\not\\le V(e_2)")) + document.addSubview(textView) + scrollView.contentView.scroll(to: NSPoint(x: 0, y: 1043.5)) + scrollView.reflectScrolledClipView(scrollView.contentView) + scrollView.layoutSubtreeIfNeeded() + let before = scrollView.contentView.bounds.origin.y + change(textView) + scrollView.layoutSubtreeIfNeeded() + return scrollView.contentView.bounds.origin.y - before + } + let wider = { (view: NSTextView) in view.setFrameSize(NSSize(width: 790, height: 17)) } + let reframed = { (view: NSTextView) in view.frame = NSRect(x: 20, y: 1044, width: 300, height: 34) } + + XCTAssertEqual(clipMove(NSTextView(), wider), -23.5, accuracy: 0.5, "the control: AppKit scrolls") + XCTAssertEqual(clipMove(DocumentCellTextView(), wider), 0) + XCTAssertEqual(clipMove(DocumentCellTextView(), reframed), 0) + XCTAssertNotEqual(clipMove(DocumentCellTextView()) { _ = $0.scrollToVisible($0.bounds) }, 0) + } + /// A model list on fixture.md with every view reported, the content top /// `offset` below the image's top. private func listInsideTheImage(at offset: CGFloat, current: Bool) -> ListModel { From 719d43797ad6253484aa7328a61f1edf5855c5dc Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:24:17 +0100 Subject: [PATCH 017/105] fix(heights): a picture or diagram shown once is measured exactly at any width and zoom Every re-measure (zoom, width, Reading Mode, theme) sent images and SVGs back to the 100 pt loading view and Mermaid diagrams to the 200 pt default; each then reported its real height on screen and swung the table by hundreds of points (as 1.14.4 did). The rows are now computed with the rule the views draw by: - Images: the view records the original size (ImageLoader keeps it) per document, keyed by the link; ImageBlock.pictureSize gives the picture at any width and zoom (D4 cap, own width x zoom, HTML width), and the row adds the caption (TextKit lines x singleLineHeight, which ImageCaption is at least as tall as) with the padding SwiftUI applies to each element of the view's two-view body. The loading view, caption included, is measured as drawn too, and holds the picture's size once it is known. - SVG: the markup is in the block, so the measurer decodes it once (SVGImageDecoder cache, shared with the view, which no longer shows the loading view on a re-created row). - Mermaid: the template posts the diagram's natural size and the page's overhead (mermaidGeometry); BlockLayout.Mermaid.diagramHeight is the template's own resizeDiagram formula, used by the measurer and by the view's first frame. Process-wide by theme and source, like the snapshots. This replaces the reported-height keys of f5bbb2d (which hashed whole data: links) and the per-zoom diagram seed of 7ddc1ce. Tests: the measurer gives exactly what the GUI logs reported (655 / 716 / 809 for the icon at 100 / 110 / 125 %, 655 in Reading Mode, 155 while loading; 94 / 101 / 111.5 for the diagram); ImageCaption hosted in a window is as tall as captionHeight across the zoom ladder, three widths and a wrapping caption; zoom and Reading Mode round trips report 0 times (the 116 / 224 control reports on every step). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 304 ++++++++++++------ QuickMD/QuickMD/MarkdownView.swift | 25 +- .../QuickMD/Resources/mermaid-template.html | 16 + QuickMD/QuickMD/SVGImageDecoder.swift | 50 +++ QuickMD/QuickMD/Views/ImageBlockView.swift | 36 ++- QuickMD/QuickMD/Views/MermaidBlockView.swift | 33 +- QuickMD/QuickMD/Views/SVGBlockView.swift | 37 ++- .../BlockHeightMeasurerTests.swift | 9 + .../QuickMDTests/VerticalRhythmTests.swift | 242 +++++++++++--- 9 files changed, 561 insertions(+), 191 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 6ecb963..59155c6 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -414,6 +414,81 @@ enum BlockLayout { let available = max(1, contentWidth) return min(available, min(maxDisplayWidth, available) * fontScale) } + + /// The size a picture — a bitmap image (`ImageBlockView`) or an SVG + /// (`SVGBlockView`) — is drawn at, from its own size (`NSImage.size`: + /// the ORIGINAL pixel size for a bitmap, see `ImageLoader`, the declared + /// size for an SVG): `displayWidth`, never wider than its own width × + /// zoom (D4, GitHub parity) or an HTML `` in its place, at + /// its aspect ratio. The height is rounded to whole points, so it is + /// pixel-exact at 1× and 2×. The views FRAME the picture with this and + /// the measurer sizes the row with it: once a picture's size is known, + /// its row is exact at every width and zoom. + static func pictureSize(imageSize: CGSize, htmlWidth: ImageWidth?, fontScale: CGFloat, + contentWidth: CGFloat) -> CGSize { + let cap = displayWidth(fontScale: fontScale, contentWidth: contentWidth) + let width: CGFloat + if let htmlWidth { + width = htmlWidth.displayWidth(cap: cap, fontScale: fontScale) + } else if imageSize.width > 0 { + width = min(cap, imageSize.width * fontScale) + } else { + width = cap + } + guard imageSize.width > 0, imageSize.height > 0 else { + return CGSize(width: width, height: placeholderHeight) + } + return CGSize(width: width, height: (width * imageSize.height / imageSize.width).rounded()) + } + + /// The alt-text caption under an image (`ImageCaption`): 12 pt × zoom, + /// system italic. + static let captionFontSize: CGFloat = 12 + + static func captionFont(fontScale: CGFloat) -> NSFont { + let size = captionFontSize * fontScale + let base = NSFont.systemFont(ofSize: size) + return NSFont(descriptor: base.fontDescriptor.withSymbolicTraits(.italic), size: size) ?? base + } + + /// The caption's height at `contentWidth`: its lines, broken by TextKit + /// as SwiftUI breaks them, × `singleLineHeight` — an upper bound of + /// SwiftUI's own line height, which no font metric reproduces. + /// `ImageCaption` is at least this tall, so the row is exactly this + /// whenever the two agree on the lines, and grows (and reports) if + /// they ever don't, rather than clipping. 0 without a caption. + static func captionHeight(alt: String, fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat { + guard !alt.isEmpty else { return 0 } + let font = captionFont(fontScale: fontScale) + // A fresh stack per call: TextKit is not thread-safe and this runs + // off-main in the measurer. + let storage = NSTextStorage(string: alt, attributes: [.font: font]) + let layoutManager = NSLayoutManager() + storage.addLayoutManager(layoutManager) + let container = NSTextContainer(size: NSSize(width: BlockLayout.clampedWidth(contentWidth), + height: .greatestFiniteMagnitude)) + container.lineFragmentPadding = 0 + layoutManager.addTextContainer(container) + layoutManager.ensureLayout(for: container) + var lines = 0 + layoutManager.enumerateLineFragments(forGlyphRange: layoutManager.glyphRange(for: container)) { _, _, _, _, _ in + lines += 1 + } + return CGFloat(max(1, lines)) * BlockLayout.singleLineHeight(for: font) + } + + /// An image row: the view's body is a two-view tuple (picture, then the + /// caption), so the row's vertical padding applies to EACH of them, and + /// the row's stack puts `captionSpacing` between them (measured in the + /// GUI: a 1024 px icon at 819 pt reports 8 + 600 + 8 + 8 + 8 + 15 + 8 = + /// 655). `picture` is `pictureSize(...).height`, or the 100 pt loading + /// view while the size is unknown. + static func rowHeight(picture: CGFloat, alt: String, fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat { + let outer = BlockLayout.Document.imageOuterVerticalPadding + guard !alt.isEmpty else { return picture + 2 * outer } + return picture + 2 * outer + captionSpacing + + captionHeight(alt: alt, fontScale: fontScale, contentWidth: contentWidth) + 2 * outer + } } // MARK: Display math @@ -441,6 +516,34 @@ enum BlockLayout { /// Height a diagram starts at when nothing has measured it yet. static let defaultHeight: CGFloat = 200 static let cornerRadius: CGFloat = 6 + /// The least height the web view reports (`MermaidWebView`). + static let minimumHeight: CGFloat = 50 + + /// What a rendered diagram's height follows from, as the inline web view + /// posts it (`mermaidGeometry` in mermaid-template.html): the SVG's + /// natural size (its viewBox) and what the page adds below it. + struct Geometry: Equatable { + var naturalWidth: CGFloat + var naturalHeight: CGFloat + var overhead: CGFloat + } + + /// The diagram's height in a web view `contentWidth` wide at + /// `fontScale` — the template's `resizeDiagram`: the natural size × + /// zoom, never wider than the view (`window.innerWidth`, whole CSS + /// px), plus the page's overhead, and at least `minimumHeight`. + static func diagramHeight(_ geometry: Geometry, fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat { + let viewport = floor(contentWidth) + guard geometry.naturalWidth > 0, geometry.naturalHeight > 0, viewport > 0 else { return defaultHeight } + let fit = viewport / geometry.naturalWidth + let factor = min(fontScale * min(1, fit), fit) + return max(minimumHeight, geometry.naturalHeight * factor + geometry.overhead) + } + + /// A diagram row: the diagram, the view's padding and the row's. + static func rowHeight(diagram: CGFloat) -> CGFloat { + diagram + 2 * verticalPadding + 2 * BlockLayout.Document.mermaidOuterVerticalPadding + } } // MARK: - Derived text widths @@ -721,6 +824,30 @@ class DocumentCellTextView: NSTextView { } } +// MARK: - Image caption + +/// The italic alt-text line under an image (`ImageBlockView`). Here rather +/// than in the view's file so the unit tests host the very view the measurer +/// sizes (`BlockLayout.ImageBlock.captionHeight`): at least that tall, so the +/// row is exact whenever SwiftUI breaks the caption into the lines TextKit +/// counted. +struct ImageCaption: View { + let alt: String + let color: Color + let fontScale: CGFloat + let contentWidth: CGFloat + + var body: some View { + Text(alt) + .font(.system(size: BlockLayout.ImageBlock.captionFontSize * fontScale)) + .foregroundColor(color) + .italic() + .frame(minHeight: BlockLayout.ImageBlock.captionHeight(alt: alt, fontScale: fontScale, + contentWidth: contentWidth), + alignment: .topLeading) + } +} + // MARK: - Block Height Cache /// Remembers the last measured height of each block, keyed by block id. @@ -737,84 +864,71 @@ class DocumentCellTextView: NSTextView { /// compile error (caught by CI on the GitHub runner toolchain). final class BlockHeightCache { private var heights: [String: CGFloat] = [:] - /// Row heights reported by placed pictures and diagrams, by - /// `ReportedHeights.key` — content keys, so a re-parse keeps them. - private var reported: [String: CGFloat] = [:] + /// The size of every image this document has shown (`NSImage.size`, the + /// original pixel size), by its link as written. An `NSString` key, like + /// `ImageLoader`'s caches: a multi-megabyte `data:` link hashes in + /// constant time. Per document, because a relative link names another file + /// in another folder. A re-parse keeps them: they are keyed by the link. + private var imageSizes: [NSString: CGSize] = [:] func height(for id: String) -> CGFloat? { heights[id] } func set(_ height: CGFloat, for id: String) { heights[id] = height } - /// Forgets the block-id heights (a re-parse: ids are positional). The - /// reported heights stay — they are keyed by content. + /// Forgets the block-id heights (a re-parse: ids are positional). func removeAll() { heights.removeAll() } - /// A diagram's height as its web view (or snapshot) gave it, per unit of - /// zoom (`ReportedHeights.diagramHeight`). - func setDiagramHeight(_ height: CGFloat, source: String, isDark: Bool, fontScale: CGFloat) { - guard fontScale > 0, height > 0 else { return } - setReported(height / fontScale, forKey: ReportedHeights.diagramUnitKey(source: source, isDark: isDark)) - } - - func setReported(_ height: CGFloat, forKey key: String) { - // A live resize reports at every width it passes through: keep the - // cache small rather than complete. - if reported.count >= Self.reportedLimit, reported[key] == nil { reported.removeAll() } - reported[key] = height + func setImageSize(_ size: CGSize, for url: String) { + guard size.width > 0, size.height > 0 else { return } + imageSizes[url as NSString] = size } - static let reportedLimit = 512 + func imageSize(for url: String) -> CGSize? { imageSizes[url as NSString] } /// A value copy for `BlockHeightMeasurer.measure(heightSeeds:)` — the /// measurer runs off-main and must not read this reference type from there. var snapshot: [String: CGFloat] { heights } - /// The same for `measure(reportedHeights:)`. - var reportedSnapshot: [String: CGFloat] { reported } + /// The same for `measure(imageSizes:)`. + var imageSizesSnapshot: [NSString: CGSize] { imageSizes } } -// MARK: - Reported heights - -/// The heights the placed views of pictures and diagrams reported, by what -/// they show: a block id is a position, which a re-parse (zoom, an edit -/// elsewhere) reassigns, and a width change re-measures every row — the -/// image went back to its 116 pt placeholder and the diagram to the 200 pt -/// default each time, swinging the table by hundreds of points and moving -/// the reader with it. Keyed by the block's content and the width, zoom and -/// theme it was laid out at, a reported height is valid wherever the key -/// matches: Reading Mode on and off, a zoom and back. -enum ReportedHeights { - /// The key of `block`'s row at `contentWidth`, `fontScale` and `theme`; - /// nil for the kinds the measurer computes exactly (or estimates from - /// text, a heading). - static func key(for block: MarkdownBlock, contentWidth: CGFloat, fontScale: CGFloat, - theme: MarkdownTheme) -> String? { - let place = "\(Int(contentWidth.rounded()))|\(fontScale)|\(theme.name)|\(theme.isDark)" - switch block.content { - case .image(let url, let alt, let width): - return "image|\(place)|\(url)|\(alt)|\(String(describing: width))" - case .svgImage(let source): - return "svg|\(place)|\(source)" - case .mermaidDiagram(let source): - return "mermaid|\(place)|\(source)" - default: - return nil - } +// MARK: - Mermaid geometry + +/// The geometry (`BlockLayout.Mermaid.Geometry`) of every diagram a web view +/// has rendered, by theme and source. With it a diagram that rendered once +/// never starts at the 200 pt default again: the measurer and the view's +/// first frame compute its height at any width and zoom. Process-wide, like +/// `MermaidSnapshotStore`, because a diagram re-created from a snapshot never +/// runs its web view — in this document or another. Locked: the measurer +/// reads it off-main. +enum MermaidGeometryStore { + private static let lock = NSLock() + private static var geometries: [NSString: BlockLayout.Mermaid.Geometry] = [:] + /// Entries are tiny; the cap only keeps a session-long store bounded. + static let limit = 1024 + + private static func key(source: String, isDark: Bool) -> NSString { "\(isDark)|\(source)" as NSString } + + static func set(_ geometry: BlockLayout.Mermaid.Geometry, source: String, isDark: Bool) { + guard geometry.naturalWidth > 0, geometry.naturalHeight > 0 else { return } + let key = key(source: source, isDark: isDark) + lock.lock() + defer { lock.unlock() } + if geometries.count >= limit, geometries[key] == nil { geometries.removeAll() } + geometries[key] = geometry } - /// A Mermaid diagram's height per unit of zoom, by what it shows. One that - /// fits its column is drawn at `fontScale` times its natural size - /// (mermaid-template.html), so a zoom it was never placed at starts from - /// the last one it was placed at, scaled — not from the 200 pt default - /// (which swung the table by ~120 pt on every zoom step). One wider than - /// its column is drawn smaller than that and reports it. - static func diagramUnitKey(source: String, isDark: Bool) -> String { - "mermaid-unit|\(isDark)|\(source)" + static func geometry(source: String, isDark: Bool) -> BlockLayout.Mermaid.Geometry? { + let key = key(source: source, isDark: isDark) + lock.lock() + defer { lock.unlock() } + return geometries[key] } - /// The diagram height (without the block's padding) `source` is expected - /// to have at `fontScale`; nil if it was never placed. Shared by the - /// measurer and `MermaidBlockView`'s first frame, so the two agree. + /// The diagram's height at `fontScale` and `contentWidth`, if it ever + /// rendered in this theme. static func diagramHeight(source: String, isDark: Bool, fontScale: CGFloat, - in reported: [String: CGFloat]) -> CGFloat? { - // 50: the web view's minimum (`MermaidWebView`). - reported[diagramUnitKey(source: source, isDark: isDark)].map { max(50, $0 * fontScale) } + contentWidth: CGFloat) -> CGFloat? { + geometry(source: source, isDark: isDark).map { + BlockLayout.Mermaid.diagramHeight($0, fontScale: fontScale, contentWidth: contentWidth) + } } } @@ -999,10 +1113,11 @@ enum BlockHeightMeasurer { /// the list column width minus `2 * Document.contentHorizontalPadding`. /// - heightSeeds: last-known heights (`BlockHeightCache`) used to seed /// Mermaid rows so a re-created diagram doesn't start at the default. - /// - reportedHeights: row heights the placed views of pictures and - /// diagrams reported, by `ReportedHeights.key` — what the block shows, - /// at which width, zoom and theme. Such a row starts at the height it - /// had there instead of the placeholder. + /// - imageSizes: the sizes of the images this document has shown + /// (`BlockHeightCache.imageSizesSnapshot`): such an image's row is + /// computed (`BlockLayout.ImageBlock`) instead of starting at the + /// loading view. SVG sizes come from `SVGImageDecoder`, diagram + /// geometry from `MermaidGeometryStore`. /// - math: the math engine, or `nil` when the call is NOT on the main /// thread — see the thread rule in the file header. With `nil`, rows /// that need the engine come back at a placeholder height, listed in @@ -1022,7 +1137,7 @@ enum BlockHeightMeasurer { fontScale: CGFloat, contentWidth: CGFloat, heightSeeds: [String: CGFloat] = [:], - reportedHeights: [String: CGFloat] = [:], + imageSizes: [NSString: CGSize] = [:], math: MathRendering?) -> MeasuredBlocks { #if DEBUG let signpostID = heightSignpost.makeSignpostID() @@ -1159,39 +1274,42 @@ enum BlockHeightMeasurer { } kinds.append(.reported) - case .image: - // Only the image itself: when `alt` is non-empty the view's body - // is a two-view tuple (image + italic caption `Text`), so the - // placed view reports a taller row than this. It is `.reported` - // precisely because the real size — decoded bitmap, caption or - // no caption — only exists once the view is placed. Unless it - // was placed before at this width and zoom (`reportedHeights`). - heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, - fontScale: fontScale, theme: theme) ?? ""] - ?? BlockLayout.ImageBlock.placeholderHeight - + 2 * BlockLayout.Document.imageOuterVerticalPadding) + case .image(let url, let alt, let htmlWidth): + // An image this document has shown has a known size, and the + // row is what the view draws for it (`ImageBlock.rowHeight`) at + // any width and zoom. Until then: the loading view, 100 pt + // plus the caption — also exact, until the image arrives. Still + // `.reported`: a link whose file changed, or failed, reports. + let picture = imageSizes[url as NSString].map { + BlockLayout.ImageBlock.pictureSize(imageSize: $0, htmlWidth: htmlWidth, fontScale: fontScale, + contentWidth: contentWidth).height + } ?? BlockLayout.ImageBlock.placeholderHeight + heights.append(BlockLayout.ImageBlock.rowHeight(picture: picture, alt: alt, fontScale: fontScale, + contentWidth: contentWidth)) kinds.append(.reported) - case .svgImage: - // Same shape as `.image`: the decoded size only exists once the - // view is placed (decode is off-main), so the row is reported. - heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, - fontScale: fontScale, theme: theme) ?? ""] - ?? BlockLayout.ImageBlock.placeholderHeight - + 2 * BlockLayout.Document.imageOuterVerticalPadding) + case .svgImage(let source): + // The markup is in the block, so its size is known here: decoded + // once (off-main safe) and cached by `SVGImageDecoder`, where the + // view finds it too. Markup that does not decode shows a notice + // and reports. + let picture = SVGImageDecoder.size(of: source).map { + BlockLayout.ImageBlock.pictureSize(imageSize: $0, htmlWidth: nil, fontScale: fontScale, + contentWidth: contentWidth).height + } ?? BlockLayout.ImageBlock.placeholderHeight + heights.append(picture + 2 * BlockLayout.Document.imageOuterVerticalPadding) kinds.append(.reported) case .mermaidDiagram(let source): - // This block's last height at this zoom (a width-only re-measure), - // else the diagram's height at another zoom, scaled. - let diagram = heightSeeds[block.id] - ?? ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, - fontScale: fontScale, in: reportedHeights) + // A diagram that rendered once (in either document, from either + // its web view or the snapshot that came of it) has its height + // computed for this width and zoom; else this block's last + // height (a width-only re-measure), else the default. + let diagram = MermaidGeometryStore.diagramHeight(source: source, isDark: theme.isDark, + fontScale: fontScale, contentWidth: contentWidth) + ?? heightSeeds[block.id] ?? BlockLayout.Mermaid.defaultHeight - heights.append(reportedHeights[ReportedHeights.key(for: block, contentWidth: contentWidth, - fontScale: fontScale, theme: theme) ?? ""] - ?? diagram + 2 * BlockLayout.Mermaid.verticalPadding - + 2 * BlockLayout.Document.mermaidOuterVerticalPadding) + heights.append(BlockLayout.Mermaid.rowHeight(diagram: diagram)) kinds.append(.reported) } rhythm.append(textEdges.map { rhythmRow(block, edges: $0) } diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index 732a384..d4e3f18 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -651,8 +651,8 @@ struct MarkdownView: View { // first layout — then the heights task below picks it up as soon as // the coordinator reports one. let width = contentWidth - // Content-keyed (`ReportedHeights`): valid across this re-parse. - let reported = heightCache.reportedSnapshot + // Keyed by the link, so valid across this re-parse. + let imageSizes = heightCache.imageSizesSnapshot let out: ParsedAndMeasured = await Task.detached(priority: .userInitiated) { let blocks = MarkdownBlockParser(theme: currentTheme, fontScale: scale).parse(text) // Pre-compute per-text-block metadata on the background thread so the @@ -676,12 +676,13 @@ struct MarkdownView: View { // the rows it defers are finished on main just below. // No `heightSeeds`: `heightCache` is cleared for this parse, and // block ids are positional, so the previous document's diagram - // heights would seed the wrong diagrams. `reportedHeights` are - // keyed by content, width and zoom instead. + // heights would seed the wrong diagrams. Pictures and diagrams + // that were shown before are computed (`imageSizes`, the SVG + // decoder, `MermaidGeometryStore`). let measured = width > 0 ? BlockHeightMeasurer.measure(blocks: blocks, theme: currentTheme, fontScale: scale, contentWidth: width, - reportedHeights: reported, math: nil) + imageSizes: imageSizes, math: nil) : MeasuredBlocks.empty return ParsedAndMeasured(parsed: ParsedDocument(blocks: blocks, textMeta: meta), measured: measured) @@ -753,9 +754,8 @@ struct MarkdownView: View { // Mermaid rows: seed from the heights the WebViews actually reported, // so a re-measure doesn't send every diagram back to the 200 pt default. let seeds = heightCache.snapshot - // Pictures and diagrams placed at this width before (Reading Mode - // off again, a window back to its size): their reported heights. - let reported = heightCache.reportedSnapshot + // Images shown before: their rows are computed at the new width. + let imageSizes = heightCache.imageSizesSnapshot // Inline-math paragraphs: hand the previous pass's strings back so the // main-actor step re-wraps them instead of re-rendering every `$…$` // segment through SwiftMath. Safe only because this task never runs @@ -766,7 +766,7 @@ struct MarkdownView: View { let raw: MeasuredBlocks = await Task.detached(priority: .userInitiated) { BlockHeightMeasurer.measure(blocks: blocks, theme: currentTheme, fontScale: scale, contentWidth: width, heightSeeds: seeds, - reportedHeights: reported, math: nil) + imageSizes: imageSizes, math: nil) }.value // A parse that landed while we were measuring owns the state now — // its own transaction published a matching table. @@ -1157,7 +1157,7 @@ struct MarkdownView: View { case .image(let url, let alt, let width): ImageBlockView(url: url, alt: alt, width: width, theme: theme, documentURL: documentURL, - fontScale: scale, contentWidth: contentWidth, + fontScale: scale, contentWidth: contentWidth, heightCache: heightCache, onEnlarge: presentGraphicPreview) .padding(.vertical, Metrics.imageOuterVerticalPadding) @@ -1408,11 +1408,6 @@ struct MarkdownView: View { // ignores such reports too — this is the same rule on the parent's side, // so a stale report can never overwrite an exact height). guard row < table.kinds.count, table.kinds[row] == .reported else { return } - // Remembered by content, width and zoom for the next measure there. - if let key = ReportedHeights.key(for: cachedBlocks[row], contentWidth: table.contentWidth, - fontScale: CGFloat(renderedFontScale), theme: theme) { - heightCache.setReported(height, forKey: key) - } guard row < table.heights.count, abs(table.heights[row] - height) >= 0.5 else { return } // The block's height only: its space above stays as measured. measured = measured.replacingHeight(height, at: row) diff --git a/QuickMD/QuickMD/Resources/mermaid-template.html b/QuickMD/QuickMD/Resources/mermaid-template.html index bc18e24..7094888 100644 --- a/QuickMD/QuickMD/Resources/mermaid-template.html +++ b/QuickMD/QuickMD/Resources/mermaid-template.html @@ -36,6 +36,21 @@ resizeDiagram = null; container.innerHTML = ''; + // What the height follows from at ANY width and zoom: the natural size and + // what the page adds around the SVG. The host computes the same formula as + // resizeDiagram (BlockLayout.Mermaid.diagramHeight), so a diagram that has + // rendered once never starts at a default height again. Same guard as + // reportHeight: hosts without the handler ignore it. + function reportGeometry(naturalWidth, naturalHeight, svgHeight) { + try { + window.webkit.messageHandlers.mermaidGeometry.postMessage({ + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + overhead: container.getBoundingClientRect().height - svgHeight + }); + } catch (e) { /* no handler in this host -- fine */ } + } + // Report height back to Swift. The zoom viewer hosts this same template // WITHOUT a mermaidHeight handler -- the report must never break rendering // (an unguarded postMessage throws, and the catch() below would replace a @@ -69,6 +84,7 @@ svg.style.maxWidth = 'none'; svg.style.width = (naturalWidth * factor) + 'px'; svg.style.height = (naturalHeight * factor) + 'px'; + if (!fitWindow) reportGeometry(naturalWidth, naturalHeight, naturalHeight * factor); } reportHeight(); }; diff --git a/QuickMD/QuickMD/SVGImageDecoder.swift b/QuickMD/QuickMD/SVGImageDecoder.swift index 76471c3..a79082a 100644 --- a/QuickMD/QuickMD/SVGImageDecoder.swift +++ b/QuickMD/QuickMD/SVGImageDecoder.swift @@ -27,6 +27,56 @@ enum SVGImageDecoder { return image } + // MARK: - Fenced-block cache + + /// Decoded ```svg blocks, by their markup (an `NSString` key: hashing does + /// not read the whole markup). NSCache is thread-safe and may evict; + /// the sizes below never are, so a row keeps its height either way. + private static let images = NSCache() + private static let lock = NSLock() + /// Decoded size per markup; `.zero` = the markup does not decode. + private static var sizes: [NSString: CGSize] = [:] + /// Entries are tiny; the cap only keeps a session-long store bounded. + static let sizeLimit = 2048 + + /// The decoded image if it is cached — what `SVGBlockView` shows at once + /// when its row is re-created, instead of the loading view. + nonisolated static func cachedImage(_ source: String) -> NSImage? { + images.object(forKey: source as NSString) + } + + /// The decoded size, if the markup was ever decoded and decodes. + nonisolated static func cachedSize(_ source: String) -> CGSize? { + lock.lock() + defer { lock.unlock() } + return sizes[source as NSString].flatMap { $0 == .zero ? nil : $0 } + } + + /// `decode`, through the cache: the view's decode and the measurer's size + /// share one decode per markup. Off-main safe. + nonisolated static func decodeCached(_ source: String) -> NSImage? { + let key = source as NSString + if let hit = images.object(forKey: key) { return hit } + lock.lock() + let known = sizes[key] + lock.unlock() + if known == .zero { return nil } + let image = decode(source) + if let image { images.setObject(image, forKey: key) } + lock.lock() + if sizes.count >= sizeLimit, sizes[key] == nil { sizes.removeAll() } + sizes[key] = image?.size ?? .zero + lock.unlock() + return image + } + + /// The size `source` decodes to (decoding and caching it if need be), nil + /// for markup that does not decode — what the measurer sizes the row by. + nonisolated static func size(of source: String) -> CGSize? { + if let known = cachedSize(source) { return known } + return decodeCached(source)?.size + } + /// The same decode for SVG that arrives as bytes — a `data:image/svg+xml` /// image or a remote `.svg` (shields.io badges are SVG) — once ImageIO has /// declined it (`ImageLoader.decodeImage`). Only bytes that START like SVG diff --git a/QuickMD/QuickMD/Views/ImageBlockView.swift b/QuickMD/QuickMD/Views/ImageBlockView.swift index 77d83e8..6b9066d 100644 --- a/QuickMD/QuickMD/Views/ImageBlockView.swift +++ b/QuickMD/QuickMD/Views/ImageBlockView.swift @@ -21,6 +21,9 @@ struct ImageBlockView: View { let documentURL: URL? let fontScale: CGFloat let contentWidth: CGFloat + /// Where the image's size goes once it is known, so every later measure + /// computes this row instead of starting it at the loading view. + let heightCache: BlockHeightCache? var onEnlarge: (GraphicPreview) -> Void = { _ in } /// Display width cap and the pre-load placeholder height — see @@ -38,7 +41,7 @@ struct ImageBlockView: View { @State private var accessDeniedURL: URL? init(url: String, alt: String, width: ImageWidth? = nil, theme: MarkdownTheme, documentURL: URL?, - fontScale: CGFloat, contentWidth: CGFloat, + fontScale: CGFloat, contentWidth: CGFloat, heightCache: BlockHeightCache? = nil, onEnlarge: @escaping (GraphicPreview) -> Void = { _ in }) { self.url = url self.alt = alt @@ -47,6 +50,7 @@ struct ImageBlockView: View { self.documentURL = documentURL self.fontScale = fontScale self.contentWidth = contentWidth + self.heightCache = heightCache self.onEnlarge = onEnlarge let source = ImageSource.resolve(url, documentURL: documentURL) self.source = source @@ -58,6 +62,7 @@ struct ImageBlockView: View { ImageLoader.cachedImage(for: url, source: $0, maxPixel: ImageLoader.displayMaxPixel) } _image = State(initialValue: cached) + if let cached { heightCache?.setImageSize(cached.size, for: url) } // Same reasoning for failures known without I/O (a non-image or // oversized `data:` URL, a recorded data:/remote failure): the row // starts at the error view instead of the 100 pt spinner, so it reports @@ -75,6 +80,13 @@ struct ImageBlockView: View { accessDeniedView(for: accessDeniedURL) } else if source == nil || failure != nil { imageErrorView + } else if let known = heightCache?.imageSize(for: url) { + // Loading an image whose size this document already knows + // (the bitmap was evicted from `ImageLoader`'s cache): hold + // the picture's place, which is the row the measurer computed. + let size = pictureSize(for: known) + ProgressView() + .frame(width: size.width, height: size.height) } else { ProgressView() .frame(height: Metrics.placeholderHeight) @@ -85,10 +97,8 @@ struct ImageBlockView: View { } if !alt.isEmpty { - Text(alt) - .font(.system(size: 12 * fontScale)) - .foregroundColor(theme.secondaryTextColor) - .italic() + ImageCaption(alt: alt, color: theme.secondaryTextColor, fontScale: fontScale, + contentWidth: contentWidth) } } @@ -96,9 +106,10 @@ struct ImageBlockView: View { Button { onEnlarge(preview(for: image)) } label: { + let size = pictureSize(for: image.size) Image(nsImage: image).resizable() .aspectRatio(contentMode: .fit) - .frame(maxWidth: displayWidth(for: image)) + .frame(width: size.width, height: size.height) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) @@ -110,12 +121,11 @@ struct ImageBlockView: View { /// never wider than the image's own width × zoom — the rule `SVGBlockView` /// already used. A 90 px icon stays icon-sized instead of being blown up /// (blurry) to 600 pt; large images are unchanged (the cap wins). An HTML - /// `` takes the place of the image's own width (T-C). - private func displayWidth(for image: NSImage) -> CGFloat { - let cap = Metrics.displayWidth(fontScale: fontScale, contentWidth: contentWidth) - if let width { return width.displayWidth(cap: cap, fontScale: fontScale) } - guard image.size.width > 0 else { return cap } - return min(cap, image.size.width * fontScale) + /// `` takes the place of the image's own width (T-C). The one + /// rule the measurer sizes the row with (`ImageBlock.pictureSize`). + private func pictureSize(for imageSize: CGSize) -> CGSize { + Metrics.pictureSize(imageSize: imageSize, htmlWidth: width, fontScale: fontScale, + contentWidth: contentWidth) } /// The window-filling preview re-decodes at 4096 px. Local files keep the @@ -185,6 +195,7 @@ struct ImageBlockView: View { // of a live view changes, and the old bitmap must not linger. let seeded = ImageLoader.cachedImage(for: url, source: source, maxPixel: ImageLoader.displayMaxPixel) if image !== seeded { image = seeded } + if let seeded { heightCache?.setImageSize(seeded.size, for: url) } let first = await ImageLoader.load(raw: url, source: source, maxPixel: ImageLoader.displayMaxPixel) if case .failure(.unreadable(let fileURL)) = first { @@ -208,6 +219,7 @@ struct ImageBlockView: View { private func apply(_ result: Result) { switch result { case .success(let loaded): + heightCache?.setImageSize(loaded.size, for: url) if loaded !== image { image = loaded } case .failure(let loadFailure): // Terminal: shows the error placeholder. (Before, a retry that still diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index a92426d..63d59e0 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -63,12 +63,14 @@ struct MermaidBlockView: View { self.fontScale = fontScale self.contentWidth = contentWidth self.onEnlarge = onEnlarge - // The measurer's seed, so the first frame matches the table's row and - // the cell does not report the 200 pt default first. - _diagramHeight = State(initialValue: heightCache.height(for: blockId) - ?? ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, - fontScale: fontScale, - in: heightCache.reportedSnapshot) + // The measurer's rule, in the measurer's order, so the first frame is + // the table's row and the cell does not report the 200 pt default: + // computed from the geometry of an earlier render, else this block's + // last height, else the default. + _diagramHeight = State(initialValue: MermaidGeometryStore.diagramHeight( + source: source, isDark: theme.isDark, fontScale: fontScale, + contentWidth: contentWidth) + ?? heightCache.height(for: blockId) ?? Metrics.defaultHeight) let cached = MermaidSnapshotStore.image(source: "\(fontScale)|\(contentWidth)|\(source)", isDark: theme.isDark) _snapshot = State(initialValue: cached) @@ -79,8 +81,6 @@ struct MermaidBlockView: View { // (zoom, theme) — which empties the cache — measures the diagram at // the 200 pt default again and it reports back on screen. heightCache.set(cached.size.height, for: blockId) - heightCache.setDiagramHeight(cached.size.height, source: source, isDark: theme.isDark, - fontScale: fontScale) } } @@ -103,8 +103,6 @@ struct MermaidBlockView: View { diagramHeight: $diagramHeight, onHeight: { height in heightCache.set(height, for: blockId) - heightCache.setDiagramHeight(height, source: source, isDark: theme.isDark, - fontScale: fontScale) }, onSnapshot: { image in MermaidSnapshotStore.set(image, source: snapshotKey, isDark: theme.isDark) @@ -286,6 +284,7 @@ private struct MermaidWebView: NSViewRepresentable { func makeNSView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.userContentController.add(context.coordinator, name: "mermaidHeight") + config.userContentController.add(context.coordinator, name: "mermaidGeometry") let webView = ScrollPassthroughWebView(frame: .zero, configuration: config) webView.setValue(false, forKey: "drawsBackground") @@ -345,6 +344,20 @@ private struct MermaidWebView: NSViewRepresentable { func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == "mermaidGeometry" { + // What the diagram's height follows from at any width and zoom + // (`BlockLayout.Mermaid.diagramHeight`) — posted just before + // the height, after every render. + guard let body = message.body as? [String: Any], + let width = (body["naturalWidth"] as? NSNumber)?.doubleValue, + let height = (body["naturalHeight"] as? NSNumber)?.doubleValue, + let overhead = (body["overhead"] as? NSNumber)?.doubleValue, + let source = lastSource, let isDark = lastIsDark else { return } + MermaidGeometryStore.set(.init(naturalWidth: width, naturalHeight: height, + overhead: max(0, overhead)), + source: source, isDark: isDark) + return + } guard let height = message.body as? Double else { return } let newHeight = max(CGFloat(height), 50) if abs(newHeight - parent.diagramHeight) > 1 { diff --git a/QuickMD/QuickMD/Views/SVGBlockView.swift b/QuickMD/QuickMD/Views/SVGBlockView.swift index 26e9a58..7ae94b0 100644 --- a/QuickMD/QuickMD/Views/SVGBlockView.swift +++ b/QuickMD/QuickMD/Views/SVGBlockView.swift @@ -25,6 +25,18 @@ struct SVGBlockView: View { @State private var image: NSImage? @State private var decodeFailed = false + init(source: String, theme: MarkdownTheme, fontScale: CGFloat, contentWidth: CGFloat, + onEnlarge: @escaping (GraphicPreview) -> Void = { _ in }) { + self.source = source + self.theme = theme + self.fontScale = fontScale + self.contentWidth = contentWidth + self.onEnlarge = onEnlarge + // A re-created row (cell reuse resets `@State`) shows the decoded image + // at once — at the size the measurer gave its row — not the loading view. + _image = State(initialValue: SVGImageDecoder.cachedImage(source)) + } + /// Display width cap and the pre-decode placeholder height — shared with /// bitmap images and `BlockHeightMeasurer` (`.reported` row, like images). typealias Metrics = BlockLayout.ImageBlock @@ -35,10 +47,11 @@ struct SVGBlockView: View { Button { onEnlarge(.image(Image(nsImage: image), title: "SVG", fileURL: nil)) } label: { + let size = pictureSize(for: image.size) Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(maxWidth: displayWidth(for: image)) + .frame(width: size.width, height: size.height) } .buttonStyle(.plain) .help("Click to enlarge image") @@ -48,6 +61,11 @@ struct SVGBlockView: View { .font(.system(size: 12 * fontScale)) .foregroundColor(theme.secondaryTextColor) .padding(.vertical, 12) + } else if let known = SVGImageDecoder.cachedSize(source) { + // Decoded before, evicted since: hold the picture's place. + let size = pictureSize(for: known) + ProgressView() + .frame(width: size.width, height: size.height) } else { ProgressView() .frame(height: Metrics.placeholderHeight) @@ -55,9 +73,12 @@ struct SVGBlockView: View { } .task(id: source) { decodeFailed = false - image = nil + // The current markup's cached image, if any (the id also changes + // when the markup of a live view does). + image = SVGImageDecoder.cachedImage(source) + guard image == nil else { return } let decoded = await Task.detached(priority: .userInitiated) { - SVGImageDecoder.decode(source) + SVGImageDecoder.decodeCached(source) }.value guard !Task.isCancelled else { return } if let decoded { image = decoded } else { decodeFailed = true } @@ -66,10 +87,10 @@ struct SVGBlockView: View { /// The column cap from `ImageBlock.displayWidth` (fitted width × zoom, never /// wider than the column), further capped at the SVG's own declared size × - /// zoom so icons stay icon-sized. - private func displayWidth(for image: NSImage) -> CGFloat { - let cap = Metrics.displayWidth(fontScale: fontScale, contentWidth: contentWidth) - guard image.size.width > 0 else { return cap } - return min(cap, image.size.width * fontScale) + /// zoom so icons stay icon-sized — `ImageBlock.pictureSize`, the rule the + /// measurer sizes the row with. + private func pictureSize(for imageSize: CGSize) -> CGSize { + Metrics.pictureSize(imageSize: imageSize, htmlWidth: nil, fontScale: fontScale, + contentWidth: contentWidth) } } diff --git a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift index 5f2d80a..c7fc917 100644 --- a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift +++ b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift @@ -491,7 +491,16 @@ final class BlockHeightMeasurerTests: XCTestCase { guard let index = blocks.firstIndex(where: { if case .image = $0.content { return true } else { return false } }) else { return XCTFail("fixture did not parse as an image") } + // The loading view as drawn: the 100 pt placeholder AND the caption + // under it, each with the row's padding (`ImageBlock.rowHeight`). XCTAssertEqual(measured.table.heights[index], + BlockLayout.ImageBlock.placeholderHeight + + 4 * BlockLayout.Document.imageOuterVerticalPadding + + BlockLayout.ImageBlock.captionSpacing + + BlockLayout.ImageBlock.captionHeight(alt: "alt", fontScale: 1, contentWidth: 600), + accuracy: tolerance) + let uncaptioned = parse("![](picture.png)") + XCTAssertEqual(measure(uncaptioned, width: 600).table.heights[0], BlockLayout.ImageBlock.placeholderHeight + 2 * BlockLayout.Document.imageOuterVerticalPadding, accuracy: tolerance) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 077e75e..5c7d551 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -473,6 +473,9 @@ final class VerticalRhythmTests: XCTestCase { /// The style toggle pins the anchor until the install (false: the /// rules of 4f67351, sticky anchor without the pin). var pinsLayoutSwitch = true + /// Height reports that changed a row (`reportHeight` past its 0.5 pt + /// threshold): every one is a swing of the table. + var reports = 0 var rects: [NSRect] { var y: CGFloat = 0 @@ -522,6 +525,7 @@ final class VerticalRhythmTests: XCTestCase { anchor = (row, offset, placed) } mutating func report(_ row: Int) { + reports += 1 let top = contentTop let delta = rows[row].real - rows[row].height let blockTop = blockTop(row) @@ -691,33 +695,194 @@ final class VerticalRhythmTests: XCTestCase { return rows } - /// A diagram placed at one zoom starts at another zoom at its height - /// there, scaled (fixture.md's diagram: 70 pt at 100 %, reported 77 and - /// 87.5 at 110 and 125 %), not at the 200 pt default; never placed, at the - /// default; and this block's own last height at this zoom still wins. - func testADiagramPlacedAtOneZoomStartsAnotherZoomScaled() throws { - let blocks = parse("```mermaid\nflowchart LR\n A[Start] --> B[End]\n```") - guard case .mermaidDiagram(let source) = blocks[0].content else { return XCTFail("not a diagram") } - let chrome = 2 * BlockLayout.Mermaid.verticalPadding + 2 * BlockLayout.Document.mermaidOuterVerticalPadding + // MARK: - Pictures and diagrams shown once are measured exactly + + /// The fixture's picture rows as the GUI reported them (debug logs of the + /// zoom and Reading Mode round trips): the 1024 px icon with its "QuickMD + /// icon" caption, and the "Start → End" flowchart — by width and zoom. + private let guiPictureRows: [(width: CGFloat, scale: CGFloat, image: CGFloat, diagram: CGFloat)] = [ + (819, 1.0, 655, 94), (819, 1.1, 716, 101), (819, 1.25, 809, 111.5), (720, 1.0, 655, 94), + ] + private let fixtureDiagram = "flowchart LR\n A[Start] --> B[End]" + + /// Once the icon has been shown (its size known) and the diagram has + /// rendered (its geometry known), the measurer gives exactly the rows the + /// views reported in the GUI, at every width and zoom of the round trips — + /// so no re-measure sends them back to the 116 / 224 pt placeholders and no + /// view reports. Before the icon is known, the row is the loading view, + /// 155 — also what the GUI reported. + func testPicturesShownOnceAreMeasuredAsTheirViewsReportThem() throws { + let blocks = parse("![QuickMD icon](icon.png)\n\n```mermaid\n\(fixtureDiagram)\n```") + guard case .mermaidDiagram(let source) = blocks[1].content else { return XCTFail("not a diagram") } + MermaidGeometryStore.set(.init(naturalWidth: 240, naturalHeight: 70, overhead: 0), + source: source, isDark: theme.isDark) + let sizes: [NSString: CGSize] = ["icon.png": CGSize(width: 1024, height: 1024)] + for row in guiPictureRows { + let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: row.scale, + contentWidth: row.width, imageSizes: sizes, math: math) + XCTAssertEqual(measured.table.heights[0], row.image, "image at \(row.width) × \(row.scale)") + XCTAssertEqual(measured.table.heights[1], row.diagram, accuracy: 0.001, + "diagram at \(row.width) × \(row.scale)") + } + let unknown = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, contentWidth: 819, + math: math) + XCTAssertEqual(unknown.table.heights[0], 155) + // The cache keeps image sizes across a re-parse. let cache = BlockHeightCache() - cache.setDiagramHeight(70, source: source, isDark: theme.isDark, fontScale: 1) - cache.removeAll() // the re-parse of a zoom step - for (scale, diagram) in [(1.1, 77.0), (1.25, 87.5), (0.9, 63.0)] as [(CGFloat, CGFloat)] { + cache.setImageSize(CGSize(width: 1024, height: 1024), for: "icon.png") + cache.set(70, for: blocks[1].id) + cache.removeAll() + XCTAssertNil(cache.height(for: blocks[1].id)) + XCTAssertEqual(cache.imageSizesSnapshot["icon.png"], CGSize(width: 1024, height: 1024)) + } + + /// The picture rule the views frame the picture with: the 600 pt cap × + /// zoom within the column, never past the picture's own width × zoom or + /// an HTML width, at its aspect ratio, whole points. + func testAPictureIsDrawnAtTheSizeTheMeasurerUses() { + typealias Image = BlockLayout.ImageBlock + let icon = CGSize(width: 1024, height: 1024) + XCTAssertEqual(Image.pictureSize(imageSize: icon, htmlWidth: nil, fontScale: 1, contentWidth: 819), + CGSize(width: 600, height: 600)) + XCTAssertEqual(Image.pictureSize(imageSize: icon, htmlWidth: nil, fontScale: 1.25, contentWidth: 819), + CGSize(width: 750, height: 750)) + XCTAssertEqual(Image.pictureSize(imageSize: icon, htmlWidth: nil, fontScale: 1.5, contentWidth: 819), + CGSize(width: 819, height: 819)) + XCTAssertEqual(Image.pictureSize(imageSize: CGSize(width: 90, height: 30), htmlWidth: nil, fontScale: 1.5, + contentWidth: 819), CGSize(width: 135, height: 45)) + XCTAssertEqual(Image.pictureSize(imageSize: CGSize(width: 1000, height: 333), htmlWidth: nil, fontScale: 1, + contentWidth: 819).height, 200) // 199.8 + let html = Image.pictureSize(imageSize: icon, htmlWidth: .points(200), fontScale: 1.1, contentWidth: 819) + XCTAssertEqual(html.width, 220, accuracy: 0.001) + XCTAssertEqual(html.height, 220) + XCTAssertEqual(Image.pictureSize(imageSize: icon, htmlWidth: .fraction(0.5), fontScale: 1, + contentWidth: 819), CGSize(width: 300, height: 300)) + } + + /// The caption as the row reporter sees it — `ImageCaption` hosted in a + /// window, its `GeometryReader` height — is the measurer's + /// `captionHeight`, at the zoom ladder, three widths and a caption that + /// wraps. + func testTheCaptionIsAsTallAsTheMeasurerSays() { + final class Box { var height: CGFloat = -1 } + struct Probe: View { + let caption: ImageCaption + let box: Box + var body: some View { + VStack(alignment: .leading, spacing: 0) { caption } + .frame(maxWidth: .infinity, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .background(GeometryReader { proxy in + Color.clear + .onAppear { box.height = proxy.size.height } + .onChange(of: proxy.size.height) { box.height = $0 } + }) + } + } + let captions = ["QuickMD icon", + String(repeating: "A longer caption that wraps onto several lines in a narrow column. ", + count: 4)] + for scale: CGFloat in [0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 3] { + for width: CGFloat in [819, 720, 300] { + for alt in captions { + let box = Box() + let caption = ImageCaption(alt: alt, color: .secondary, fontScale: scale, contentWidth: width) + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: width, height: 900), + styleMask: [.titled], backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + let host = NSHostingView(rootView: Probe(caption: caption, box: box)) + host.frame = NSRect(x: 0, y: 0, width: width, height: 900) + window.contentView = host + host.layoutSubtreeIfNeeded() + RunLoop.current.run(until: Date().addingTimeInterval(0.02)) + host.layoutSubtreeIfNeeded() + XCTAssertEqual(box.height, + BlockLayout.ImageBlock.captionHeight(alt: alt, fontScale: scale, contentWidth: width), + "\(alt.prefix(12)) at \(width) × \(scale)") + window.close() + } + } + } + } + + /// An SVG's size is known from its markup: the measurer decodes it once + /// (cached for the view, which then never shows the loading view on a + /// re-created row) and sizes the row as the view draws it. Markup that + /// does not decode stays at the loading view and reports. + func testAnSVGIsMeasuredAtItsDecodedSize() { + let markup = "" + let blocks = parse("```svg\n\(markup)\n```") + guard case .svgImage(let source) = blocks[0].content else { return XCTFail("not an SVG block") } + let pad = 2 * BlockLayout.Document.imageOuterVerticalPadding + for (scale, width, picture) in [(1.0, 819.0, 100.0), (1.5, 819.0, 150.0), (1.0, 150.0, 75.0)] as [(CGFloat, CGFloat, CGFloat)] { let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: scale, - contentWidth: 819, reportedHeights: cache.reportedSnapshot, - math: math) - XCTAssertEqual(measured.table.heights[0], diagram + chrome, accuracy: 0.001, "at \(scale)") - XCTAssertEqual(ReportedHeights.diagramHeight(source: source, isDark: theme.isDark, fontScale: scale, - in: cache.reportedSnapshot) ?? 0, - diagram, accuracy: 0.001) + contentWidth: width, math: math) + XCTAssertEqual(measured.table.heights[0], picture + pad, "at \(width) × \(scale)") + } + XCTAssertNotNil(SVGImageDecoder.cachedImage(source)) + XCTAssertEqual(SVGImageDecoder.cachedSize(source), CGSize(width: 200, height: 100)) + let broken = parse("```svg\nRendered", isDark: false)) + } + + /// The zoom round trip and the Reading Mode round trip with the reader in + /// the image, the rows as the measurer now gives them: no height report at + /// all (the table never swings) and the reader back on the very point. The + /// control, with the 116 / 224 pt placeholders every re-measure used to + /// start from, reports on every step. + func testRoundTripsWithPicturesShownOnceNeverSwingTheTable() throws { + let blocks = parse("![QuickMD icon](icon.png)\n\n```mermaid\n\(fixtureDiagram)\n```") + guard case .mermaidDiagram(let source) = blocks[1].content else { return XCTFail("not a diagram") } + MermaidGeometryStore.set(.init(naturalWidth: 240, naturalHeight: 70, overhead: 0), + source: source, isDark: theme.isDark) + let sizes: [NSString: CGSize] = ["icon.png": CGSize(width: 1024, height: 1024)] + func rows(width: CGFloat, scale: CGFloat, measured: Bool) -> [ListModel.Row] { + let gui = guiPictureRows.first { $0.width == width && $0.scale == scale }! + let table = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: scale, + contentWidth: width, imageSizes: sizes, math: math).table + return fixtureRows(scale: scale, wrap: width < 800 ? 2 : 1, imageReal: gui.image, + imageEstimate: measured ? table.heights[0] : 116, + diagram: gui.diagram - 24, diagramEstimate: measured ? table.heights[1] : 224) + } + for measured in [true, false] { + var start = rows(width: 819, scale: 1, measured: true) + for index in start.indices { start[index].height = start[index].real } + var list = ListModel(rows: start, contentTop: 0) + list.contentTop = list.blockTop(image) + 300 + let top = list.contentTop + for scale: CGFloat in [1.1, 1.25, 1.1, 1] { + list.install(rows(width: 819, scale: scale, measured: measured)) + } + list.toggleStyle(inset: 48) + list.install(rows(width: 720, scale: 1, measured: measured)) + list.toggleStyle(inset: 24) + list.install(rows(width: 819, scale: 1, measured: measured)) + if measured { + XCTAssertEqual(list.reports, 0) + XCTAssertEqual(list.contentTop, top, accuracy: 0.001) + } else { + XCTAssertGreaterThanOrEqual(list.reports, 6) + } } - let unplaced = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1.25, - contentWidth: 819, math: math) - XCTAssertEqual(unplaced.table.heights[0], BlockLayout.Mermaid.defaultHeight + chrome) - let ownSeed = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1.25, contentWidth: 720, - heightSeeds: [blocks[0].id: 80], - reportedHeights: cache.reportedSnapshot, math: math) - XCTAssertEqual(ownSeed.table.heights[0], 80 + chrome) } /// The failing GUI run (f1dbg.log), step by step: ⌘F "Second part" and a @@ -932,35 +1097,6 @@ final class VerticalRhythmTests: XCTestCase { } } - /// A picture or a diagram measured again where it was placed before - /// (Reading Mode off, a zoom back) starts at the height it reported - /// there, not at the placeholder; elsewhere it still starts at the - /// placeholder. Keyed by content, so a re-parse keeps it. - func testAReportedPictureHeightSeedsTheNextMeasureAtThatWidth() { - let blocks = parse("Text.\n\n![QuickMD icon](icon.png)\n\n```mermaid\ngraph TD; A-->B\n```") - let keys = [1, 2].map { ReportedHeights.key(for: blocks[$0], contentWidth: 819, fontScale: 1, theme: theme)! } - let seeds = [keys[0]: 655, keys[1]: 94] as [String: CGFloat] - let seeded = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, contentWidth: 819, - reportedHeights: seeds, math: math) - XCTAssertEqual(seeded.table.heights[1], 655) - XCTAssertEqual(seeded.table.heights[2], 94) - let elsewhere = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, contentWidth: 720, - reportedHeights: seeds, math: math) - XCTAssertEqual(elsewhere.table.heights[1], BlockLayout.ImageBlock.placeholderHeight - + 2 * BlockLayout.Document.imageOuterVerticalPadding) - XCTAssertNotEqual(elsewhere.table.heights[2], 94) - // Another picture at the same position is another key. - XCTAssertNotEqual(ReportedHeights.key(for: parse("Text.\n\n![other](other.png)")[1], contentWidth: 819, - fontScale: 1, theme: theme), keys[0]) - // A re-parse forgets the block-id heights, not the reported ones. - let cache = BlockHeightCache() - cache.set(70, for: "mermaid-2") - cache.setReported(655, forKey: keys[0]) - cache.removeAll() - XCTAssertNil(cache.height(for: "mermaid-2")) - XCTAssertEqual(cache.reportedSnapshot[keys[0]], 655) - } - func testAnchorIsRelativeToTheBlockNotTheRowEdge() { let rect = NSRect(x: 0, y: 1000, width: 500, height: 80) // In the gap above the block: negative, and it stays the same distance From a4a8813b8cca866ed858fb9d08e094d80725a4b8 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:26:19 +0100 Subject: [PATCH 018/105] fix(mermaid): a re-created diagram shows its latest snapshot until the web view has painted A diagram row that scrolled back after its view was recycled, without a snapshot at its exact zoom and width (a new width or zoom, or a view recycled before its snapshot was taken), drew an empty box for the 0.5-1 s the web view takes to render. Every snapshot is now also kept as the diagram's latest in its theme; a view without its own shows that one over the web view, scaled to the height the diagram will have, until the page has painted (the snapshot delay after its height message). The keys, the frame-fits rule for taking a snapshot, the JSON-encoded source and ScrollPassthroughWebView are unchanged. MermaidSnapshotStore moves next to MermaidGeometryStore so the tests compile it. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 35 +++++++++ QuickMD/QuickMD/Views/MermaidBlockView.swift | 75 ++++++++++--------- .../QuickMDTests/VerticalRhythmTests.swift | 19 +++++ 3 files changed, 94 insertions(+), 35 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 59155c6..8b6f58d 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -932,6 +932,41 @@ enum MermaidGeometryStore { } } +// MARK: - Mermaid snapshots + +/// Shared store of rendered-diagram snapshots, keyed by (isDark, source). +/// Written by the inline MermaidBlockView after each successful render; read +/// back by MermaidBlockView on re-creation AND by the PDF exporter +/// (MermaidPDFRenderer) as a fast path before spinning up an offscreen +/// WebView. NSCache evicts under memory pressure. Here rather than in the +/// view's file so the unit tests compile it. +enum MermaidSnapshotStore { + private static let cache = NSCache() + + private static func key(source: String, isDark: Bool) -> NSString { + "\(isDark)|\(source)" as NSString + } + + static func image(source: String, isDark: Bool) -> NSImage? { + cache.object(forKey: key(source: source, isDark: isDark)) + } + + static func set(_ image: NSImage, source: String, isDark: Bool) { + cache.setObject(image, forKey: key(source: source, isDark: isDark)) + } + + /// The latest snapshot of `diagram` in this theme at ANY zoom and width: + /// what a view without a snapshot of its own shows while its web view + /// renders, instead of an empty box. + static func latest(diagram: String, isDark: Bool) -> NSImage? { + cache.object(forKey: key(source: "latest|" + diagram, isDark: isDark)) + } + + static func setLatest(_ image: NSImage, diagram: String, isDark: Bool) { + cache.setObject(image, forKey: key(source: "latest|" + diagram, isDark: isDark)) + } +} + // MARK: - Height table /// How a row's height was obtained, and therefore whether the placed view is diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index 63d59e0..c99e9e7 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -14,27 +14,6 @@ import WebKit /// the height is seeded from BlockHeightCache (no scroll jump). /// /// Clicking the diagram opens a window-filling preview with pinch/button zoom. -/// Shared store of rendered-diagram snapshots, keyed by (isDark, source). -/// Written by the inline MermaidBlockView after each successful render; read -/// back by MermaidBlockView on LazyVStack re-creation AND by the PDF exporter -/// (MermaidPDFRenderer) as a fast path before spinning up an offscreen -/// WebView. NSCache evicts under memory pressure. -enum MermaidSnapshotStore { - private static let cache = NSCache() - - private static func key(source: String, isDark: Bool) -> NSString { - "\(isDark)|\(source)" as NSString - } - - static func image(source: String, isDark: Bool) -> NSImage? { - cache.object(forKey: key(source: source, isDark: isDark)) - } - - static func set(_ image: NSImage, source: String, isDark: Bool) { - cache.setObject(image, forKey: key(source: source, isDark: isDark)) - } -} - struct MermaidBlockView: View { let blockId: String let source: String @@ -43,6 +22,11 @@ struct MermaidBlockView: View { @State private var diagramHeight: CGFloat @State private var snapshot: NSImage? + /// No snapshot at this zoom and width: the latest one of this diagram at + /// another, shown over the web view until it has painted — a re-created + /// row never flashes empty for the half second WebKit takes to render. + @State private var standIn: NSImage? + @State private var webViewPainted = false let fontScale: CGFloat let contentWidth: CGFloat let onEnlarge: (GraphicPreview) -> Void @@ -74,6 +58,8 @@ struct MermaidBlockView: View { ?? Metrics.defaultHeight) let cached = MermaidSnapshotStore.image(source: "\(fontScale)|\(contentWidth)|\(source)", isDark: theme.isDark) _snapshot = State(initialValue: cached) + _standIn = State(initialValue: cached == nil + ? MermaidSnapshotStore.latest(diagram: source, isDark: theme.isDark) : nil) if let cached { _diagramHeight = State(initialValue: cached.size.height) // The web view path seeds the cache through `onHeight`; this path @@ -96,21 +82,36 @@ struct MermaidBlockView: View { .frame(maxHeight: diagramHeight) .frame(maxWidth: .infinity) } else { - MermaidWebView( - source: source, - isDark: theme.isDark, - fontScale: fontScale, - diagramHeight: $diagramHeight, - onHeight: { height in - heightCache.set(height, for: blockId) - }, - onSnapshot: { image in - MermaidSnapshotStore.set(image, source: snapshotKey, isDark: theme.isDark) - if fontScale == 1 { - MermaidSnapshotStore.set(image, source: source, isDark: theme.isDark) - } + ZStack { + MermaidWebView( + source: source, + isDark: theme.isDark, + fontScale: fontScale, + diagramHeight: $diagramHeight, + onHeight: { height in + heightCache.set(height, for: blockId) + }, + onSnapshot: { image in + MermaidSnapshotStore.set(image, source: snapshotKey, isDark: theme.isDark) + MermaidSnapshotStore.setLatest(image, diagram: source, isDark: theme.isDark) + if fontScale == 1 { + MermaidSnapshotStore.set(image, source: source, isDark: theme.isDark) + } + }, + onPainted: { webViewPainted = true } + ) + if let standIn, !webViewPainted { + // The diagram at another zoom or width, scaled to + // the height this one will have (exact once it has + // rendered anywhere: `MermaidGeometryStore`). + Image(nsImage: standIn) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: diagramHeight) + .frame(maxWidth: .infinity) + .allowsHitTesting(false) } - ) + } .frame(height: diagramHeight) } } @@ -280,6 +281,9 @@ private struct MermaidWebView: NSViewRepresentable { @Binding var diagramHeight: CGFloat let onHeight: (CGFloat) -> Void let onSnapshot: (NSImage) -> Void + /// The page has drawn a render (the snapshot delay has passed after its + /// height message) — what a stand-in snapshot waits for. + let onPainted: () -> Void func makeNSView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() @@ -372,6 +376,7 @@ private struct MermaidWebView: NSViewRepresentable { guard let webView = message.webView else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self, weak webView] in guard let self, let webView else { return } + self.parent.onPainted() // Snapshot only a web view whose frame already matches the // height it reported. A row that is not laid out yet (created // below the fold, a cell AppKit has not placed) keeps its old diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 5c7d551..0989cad 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -844,6 +844,25 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNil(MermaidGeometryStore.geometry(source: "graph TD; Never-->Rendered", isDark: false)) } + /// A diagram view without a snapshot at its zoom and width finds the + /// latest one of the same diagram in the same theme as its stand-in, so a + /// re-created row never shows the empty box the web view is for the half + /// second it takes to render (seen while recording). Another diagram, or + /// the other theme, has none; the exact key and the PDF exporter's key + /// are untouched. + func testADiagramWithoutASnapshotOfItsOwnFindsTheLatestOneAsAStandIn() { + let diagram = "flowchart LR\n Stand-->In" + let image = NSImage(size: NSSize(width: 240, height: 70)) + MermaidSnapshotStore.set(image, source: "1.0|819.0|\(diagram)", isDark: false) + MermaidSnapshotStore.setLatest(image, diagram: diagram, isDark: false) + XCTAssertNil(MermaidSnapshotStore.image(source: "1.25|819.0|\(diagram)", isDark: false)) + XCTAssertTrue(MermaidSnapshotStore.latest(diagram: diagram, isDark: false) === image) + XCTAssertNil(MermaidSnapshotStore.latest(diagram: diagram, isDark: true)) + XCTAssertNil(MermaidSnapshotStore.latest(diagram: "flowchart LR\n Other-->Diagram", isDark: false)) + XCTAssertNil(MermaidSnapshotStore.image(source: diagram, isDark: false)) + XCTAssertTrue(MermaidSnapshotStore.image(source: "1.0|819.0|\(diagram)", isDark: false) === image) + } + /// The zoom round trip and the Reading Mode round trip with the reader in /// the image, the rows as the measurer now gives them: no height report at /// all (the table never swings) and the reader back on the very point. The From ac4a10326f63e339c3b5569d4beb883679587aea Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:30:29 +0100 Subject: [PATCH 019/105] test: one temporary-defaults helper, no plist left in ~/Library/Preferences UserDefaults(suiteName: "pl.falami.studio.QuickMD..") plus removePersistentDomain empties the domain, but cfprefsd writes the empty plist into ~/Library/Preferences seconds later, so every test run left files behind. TemporaryDefaults uses an absolute-path suite inside a per-instance temporary directory (the approach ReviewPromptTests already had) and deletes the directory; FontTests, RendererTests, DocumentSelectionTests and ReviewPromptTests use it. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 4 ++ .../QuickMDTests/DocumentSelectionTests.swift | 8 +-- QuickMD/QuickMDTests/FontTests.swift | 6 +- QuickMD/QuickMDTests/RendererTests.swift | 6 +- QuickMD/QuickMDTests/ReviewPromptTests.swift | 26 +++---- QuickMD/QuickMDTests/TemporaryDefaults.swift | 70 +++++++++++++++++++ 6 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 QuickMD/QuickMDTests/TemporaryDefaults.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 3e3b590..3c98c6e 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ A1B2C3D401000231 /* ReviewPrompt.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000230 /* ReviewPrompt.swift */; }; A1B2C3D401000232 /* ReviewPromptTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000231 /* ReviewPromptTrigger.swift */; }; A1B2C3D401000233 /* ReviewPromptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000232 /* ReviewPromptTests.swift */; }; + A1B2C3D401000300 /* TemporaryDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000300 /* TemporaryDefaults.swift */; }; A1B2C3D401000201 /* PrintableTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000200 /* PrintableTableTests.swift */; }; A1B2C3D4010001C0 /* SelectionRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; @@ -230,6 +231,7 @@ A1B2C3D400000230 /* ReviewPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPrompt.swift; sourceTree = ""; }; A1B2C3D400000231 /* ReviewPromptTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptTrigger.swift; sourceTree = ""; }; A1B2C3D400000232 /* ReviewPromptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptTests.swift; sourceTree = ""; }; + A1B2C3D400000300 /* TemporaryDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryDefaults.swift; sourceTree = ""; }; A1B2C3D400000200 /* PrintableTableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintableTableTests.swift; sourceTree = ""; }; A1B2C3D4000001C0 /* SelectionRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRows.swift; sourceTree = ""; }; A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; @@ -346,6 +348,7 @@ A1B2C3D4000001B2 /* TableTextTests.swift */, A1B2C3D400000221 /* FrontMatterTests.swift */, A1B2C3D400000232 /* ReviewPromptTests.swift */, + A1B2C3D400000300 /* TemporaryDefaults.swift */, A1B2C3D400000200 /* PrintableTableTests.swift */, A1B2C3D4000001C1 /* TableSelectionTests.swift */, A1B2C3D4000000C1 /* DocumentSelectionTests.swift */, @@ -761,6 +764,7 @@ A1B2C3D4010001B3 /* TableTextTests.swift in Sources */, A1B2C3D401000222 /* FrontMatterTests.swift in Sources */, A1B2C3D401000233 /* ReviewPromptTests.swift in Sources */, + A1B2C3D401000300 /* TemporaryDefaults.swift in Sources */, A1B2C3D401000201 /* PrintableTableTests.swift in Sources */, A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */, ); diff --git a/QuickMD/QuickMDTests/DocumentSelectionTests.swift b/QuickMD/QuickMDTests/DocumentSelectionTests.swift index 6d065c0..465ba21 100644 --- a/QuickMD/QuickMDTests/DocumentSelectionTests.swift +++ b/QuickMD/QuickMDTests/DocumentSelectionTests.swift @@ -534,10 +534,10 @@ final class DocumentSelectionTests: XCTestCase { XCTAssertFalse(SelectionAutoCopy.shouldCopy(after: .click, selection: selection(0, 5), enabled: true)) } - func testAutoCopySettingDefaultsToOff() { - let suite = "pl.falami.studio.QuickMD.tests.\(UUID().uuidString)" - guard let defaults = UserDefaults(suiteName: suite) else { return XCTFail("no defaults suite") } - defer { defaults.removePersistentDomain(forName: suite) } + func testAutoCopySettingDefaultsToOff() throws { + let suite = try TemporaryDefaults("AutoCopy") + defer { suite.remove() } + let defaults = suite.defaults XCTAssertFalse(SelectionAutoCopy.isEnabled(in: defaults)) defaults.set(true, forKey: SelectionAutoCopy.defaultsKey) XCTAssertTrue(SelectionAutoCopy.isEnabled(in: defaults)) diff --git a/QuickMD/QuickMDTests/FontTests.swift b/QuickMD/QuickMDTests/FontTests.swift index 6be3bd8..6421e03 100644 --- a/QuickMD/QuickMDTests/FontTests.swift +++ b/QuickMD/QuickMDTests/FontTests.swift @@ -75,9 +75,9 @@ final class FontTests: XCTestCase { } func testUserDefaultsRoundTrip() throws { - let suite = "pl.falami.studio.QuickMD.FontTests.\(UUID().uuidString)" - let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) - defer { defaults.removePersistentDomain(forName: suite) } + let suite = try TemporaryDefaults("FontTests") + defer { suite.remove() } + let defaults = suite.defaults XCTAssertEqual(DocumentFonts.fromUserDefaults(defaults), .system) defaults.set(Self.body, forKey: DocumentFonts.bodyDefaultsKey) diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index f37ba12..4bb7b0b 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -711,9 +711,9 @@ final class RendererTests: XCTestCase { } func testDefaultZoomStoredReadsItsOwnSuite() throws { - let suite = "QuickMDTests.DefaultZoom.\(UUID().uuidString)" - let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) - defer { defaults.removePersistentDomain(forName: suite) } + let suite = try TemporaryDefaults("DefaultZoom") + defer { suite.remove() } + let defaults = suite.defaults XCTAssertEqual(DefaultZoom.stored(in: defaults), 1.0, "missing key") defaults.set(1.25, forKey: DefaultZoom.defaultsKey) diff --git a/QuickMD/QuickMDTests/ReviewPromptTests.swift b/QuickMD/QuickMDTests/ReviewPromptTests.swift index 7ee5cf6..dd9234a 100644 --- a/QuickMD/QuickMDTests/ReviewPromptTests.swift +++ b/QuickMD/QuickMDTests/ReviewPromptTests.swift @@ -20,29 +20,21 @@ final class ReviewPromptTests: XCTestCase { private let base = Date(timeIntervalSince1970: 1_767_261_600) private let version = "1.15.0" - /// Per-test directory that holds the suite's plist. - private var suiteDirectory: URL! - - /// The suite name is an ABSOLUTE PATH (CFPreferences then stores the - /// domain at `.plist`), inside a per-test temporary directory. - /// A plain suite name leaks `~/Library/Preferences/.plist`: - /// `removePersistentDomain` makes cfprefsd write an EMPTY plist there - /// seconds later (measured: ~20 s after the test process ended), so - /// neither deleting the file in tearDown nor `synchronize()` first wins - /// the race. A late write here lands in the temporary directory. + /// The per-test suite, stored in a temporary directory, never in + /// `~/Library/Preferences` (see `TemporaryDefaults`). + private var suite: TemporaryDefaults! + override func setUpWithError() throws { try super.setUpWithError() - suiteDirectory = FileManager.default.temporaryDirectory - .appendingPathComponent("QuickMD-ReviewPromptTests-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: suiteDirectory, withIntermediateDirectories: true) - suiteName = suiteDirectory.appendingPathComponent("defaults").path - defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) + suite = try TemporaryDefaults("ReviewPromptTests") + suiteName = suite.suiteName + defaults = suite.defaults policy = ReviewPrompt(defaults: defaults, calendar: calendar) } override func tearDownWithError() throws { - defaults.removePersistentDomain(forName: suiteName) - try? FileManager.default.removeItem(at: suiteDirectory) + suite.remove() + suite = nil defaults = nil policy = nil try super.tearDownWithError() diff --git a/QuickMD/QuickMDTests/TemporaryDefaults.swift b/QuickMD/QuickMDTests/TemporaryDefaults.swift new file mode 100644 index 0000000..b4616c3 --- /dev/null +++ b/QuickMD/QuickMDTests/TemporaryDefaults.swift @@ -0,0 +1,70 @@ +import Foundation +import XCTest + +/// A throwaway `UserDefaults` suite for one test, stored OUTSIDE +/// `~/Library/Preferences`. +/// +/// Why not `UserDefaults(suiteName: "pl.falami.studio.QuickMD.tests.")`: +/// that domain lives at `~/Library/Preferences/.plist`, and +/// `removePersistentDomain` only empties it — cfprefsd writes the (empty) +/// plist seconds later (measured ~20 s after the test process ended), so the +/// file is left behind on every run (324 of them piled up by 2026-11-04). +/// Deleting the file in teardown loses that race as well. +/// +/// The suite name here is an ABSOLUTE PATH inside a per-instance temporary +/// directory: CFPreferences then keeps the domain at `.plist`, so even +/// a late cfprefsd write lands in the temporary directory, which `remove()` +/// deletes together with the plist. +final class TemporaryDefaults { + /// The suite name (an absolute path) — what `persistentDomain(forName:)` + /// and `removePersistentDomain(forName:)` take. + let suiteName: String + let defaults: UserDefaults + /// The directory that holds the suite's plist. + let directory: URL + + /// Where CFPreferences stores this suite. + var plistURL: URL { URL(fileURLWithPath: suiteName + ".plist") } + + init(_ label: String = "tests") throws { + directory = FileManager.default.temporaryDirectory + .appendingPathComponent("QuickMD-\(label)-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + suiteName = directory.appendingPathComponent("defaults").path + guard let defaults = UserDefaults(suiteName: suiteName) else { + throw CocoaError(.featureUnsupported) + } + self.defaults = defaults + } + + /// Empties the domain and deletes its plist with the directory. Safe to + /// call more than once. + func remove() { + defaults.removePersistentDomain(forName: suiteName) + try? FileManager.default.removeItem(at: directory) + } +} + +final class TemporaryDefaultsTests: XCTestCase { + /// The plist is written where the helper says — inside its own temporary + /// directory — and nothing of it survives `remove()`. With a plain suite + /// name (the old tests) the flushed plist sits in ~/Library/Preferences + /// instead and this fails. + func testPlistStaysInItsTemporaryDirectory() throws { + let suite = try TemporaryDefaults("Probe") + defer { suite.remove() } + suite.defaults.set(42, forKey: "probe") + XCTAssertTrue(CFPreferencesAppSynchronize(suite.suiteName as CFString)) + + XCTAssertTrue(suite.plistURL.path.hasPrefix(suite.directory.path + "/")) + XCTAssertTrue(FileManager.default.fileExists(atPath: suite.plistURL.path), + "the domain is stored at .plist") + let library = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Preferences") + .appendingPathComponent((suite.suiteName as NSString).lastPathComponent + ".plist") + XCTAssertFalse(FileManager.default.fileExists(atPath: library.path)) + + suite.remove() + XCTAssertFalse(FileManager.default.fileExists(atPath: suite.directory.path)) + } +} From c1907a73022cd79b24cb2a746eb720aefcc954d6 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:31:25 +0100 Subject: [PATCH 020/105] test(tables): pin GFM body-row cell count (pad fewer, ignore excess) Cells beyond the header's column count are dropped by design: the GFM spec says the excess is ignored, and fewer cells are padded with empty ones. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMDTests/ParserTests.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 2b0f9e3..19a30bc 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -335,6 +335,24 @@ final class ParserTests: XCTestCase { XCTAssertTrue(rows.allSatisfy { $0.count == 3 }) } + /// GFM spec (tables extension, example 204): "The remainder of the table's + /// rows may vary in the number of cells. If a number of cells fewer than + /// the number of cells in the header row, empty cells are inserted. If + /// greater, the excess is ignored". Dropping the excess is by design. + func testBodyRowCellCountFollowsGFM() { + let md = """ + | abc | def | + | --- | --- | + | bar | + | bar | baz | boo | + """ + guard case .table(let headers, let rows, _, .grid) = parse(md)[0].content else { + return XCTFail("expected table") + } + XCTAssertEqual(headers, ["abc", "def"]) + XCTAssertEqual(rows, [["bar", ""], ["bar", "baz"]]) + } + func testHeaderlessTableKeepsColumnsFromSeparator() { let md = """ | | | From ce5ba9468b2320f0a71ffab744a2dfaa0bf27553 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:33:16 +0100 Subject: [PATCH 021/105] fix(tables): split cells on unicode scalars, so a pipe before a combining mark still splits parseTableRow iterated Characters: "|" followed by a combining mark (U+0301) is one grapheme, never equal to "|", so the cell did not end there. GFM splits on code points. The row scan, the pipe count for table detection / definition terms and the body-row check now walk unicode scalars; escaped \| and pipes in code spans behave as before. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 34 +++++++++++++++-------- QuickMD/QuickMDTests/ParserTests.swift | 15 ++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index fd1ebb8..2a42e6a 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -297,7 +297,7 @@ struct MarkdownBlockParser: Sendable { } // Table detection - if line.filter({ $0 == "|" }).count >= 2 && !trimmed.isEmpty { + if Self.pipeCount(line) >= 2 && !trimmed.isEmpty { if !isTableSeparator(trimmed) && i + 1 < lines.count && isTableSeparator(lines[i + 1]) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) @@ -310,7 +310,7 @@ struct MarkdownBlockParser: Sendable { var rows: [[String]] = [] i += 2 // Skip header and separator - while i < lines.count && lines[i].contains("|") && !isTableSeparator(lines[i]) { + while i < lines.count && lines[i].unicodeScalars.contains("|") && !isTableSeparator(lines[i]) { rows.append(parseTableRow(lines[i])) i += 1 } @@ -494,6 +494,12 @@ struct MarkdownBlockParser: Sendable { // MARK: - Table Helpers + /// Unescaped and escaped pipes alike (detection counts `\\|` too, as it + /// always has), as unicode scalars — see `parseTableRow`. + private static func pipeCount(_ line: String) -> Int { + line.unicodeScalars.reduce(0) { $1 == "|" ? $0 + 1 : $0 } + } + /// Check if line matches table separator pattern (e.g., |---|---|) private func isTableSeparator(_ line: String) -> Bool { line.trimmingCharacters(in: .whitespaces) @@ -508,26 +514,30 @@ struct MarkdownBlockParser: Sendable { /// never splits, even after `\\`: only that one backslash is dropped, so /// `\\|` leaves `\|` for the inline renderer (`|` in text, `\|` in code). /// Other escapes stay in the text for the inline renderer. + /// + /// The scan walks unicode SCALARS, as GFM works on code points: a `|` + /// followed by a combining mark (U+0301…) is one Swift `Character`, and a + /// Character scan neither split the cell there nor counted the pipe. private func parseTableRow(_ line: String) -> [String] { let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) var cells: [String] = [] - var cell = "" - for ch in trimmed { - if ch != "|" { - cell.append(ch) + var cell = String.UnicodeScalarView() + for scalar in trimmed.unicodeScalars { + if scalar != "|" { + cell.append(scalar) } else if cell.last == "\\" { cell.removeLast() - cell.append("|") + cell.append(scalar) } else { - cells.append(cell) - cell = "" + cells.append(String(cell)) + cell = String.UnicodeScalarView() } } - cells.append(cell) + cells.append(String(cell)) // A leading pipe opens the row rather than ending an empty first cell - if trimmed.hasPrefix("|") { + if trimmed.unicodeScalars.first == "|" { cells.removeFirst() } @@ -634,7 +644,7 @@ struct MarkdownBlockParser: Sendable { // Table rows (the table branch needs a separator to fire, but a pipe row // is still a table row, not a term) and separators - if line.filter({ $0 == "|" }).count >= 2 { return false } + if Self.pipeCount(line) >= 2 { return false } if isTableSeparator(trimmed) { return false } // Rules and setext underlines diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 19a30bc..d2c0a4d 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -353,6 +353,21 @@ final class ParserTests: XCTestCase { XCTAssertEqual(rows, [["bar", ""], ["bar", "baz"]]) } + /// A pipe followed by a combining mark is ONE Swift Character; GFM splits on + /// the code point, so the cell still ends there and the mark opens the next + /// cell. Escaped pipes before a mark stay in the cell, as before. + func testPipeBeforeCombiningMarkStillSplits() { + let md = "| a |\u{0301}b | c |\n| --- | --- | --- |\n| x |\u{0301}y | z |\n| e \\|\u{0301} f | g |" + guard case .table(let headers, let rows, _, .grid) = parse(md)[0].content else { + return XCTFail("expected table") + } + XCTAssertEqual(headers.map { Array($0.unicodeScalars) }, + ["a", "\u{0301}b", "c"].map { Array($0.unicodeScalars) }) + XCTAssertEqual(rows.map { $0.map { Array($0.unicodeScalars) } }, + [["x", "\u{0301}y", "z"], ["e |\u{0301} f", "g", ""]] + .map { $0.map { Array($0.unicodeScalars) } }) + } + func testHeaderlessTableKeepsColumnsFromSeparator() { let md = """ | | | From 11aaaabe56fe013f6465eeaba8a29133ae17e1b1 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:33:19 +0100 Subject: [PATCH 022/105] fix(math): a formula that parses but draws nothing falls back to its source MTMathListBuilder accepts formulas that draw nothing: only spacing or style changes, empty groups, an accent or a line over nothing, \left. \right., and this SwiftMath's typesetter declines \color, \textcolor and \colorbox (no line at all). The typeset check only parsed, so such a display formula took the math path and drew an empty row, and an inline one an invisible attachment. MathInk.typesetsToSomething (parses, the parsed list has something to draw, the typesetter builds a line of some size) is now the engine's typesets, so display math falls back to the ```latex block on screen and in print alike (one substitution). The inline paths, screen and print, check the image they already rendered (a size, and something to draw) and fall back to the italic literal. A headless check of the vendored SwiftMath (ink scan of MathImage) agreed with the verdict on 36 formulas. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownExport.swift | 3 +- QuickMD/QuickMD/Views/MathBlockView.swift | 95 +++++++++++++++++-- .../BlockHeightMeasurerTests.swift | 33 +++++++ 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index e22615b..98bb744 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -921,7 +921,8 @@ enum PrintableMath { var mathImage = MathImage(latex: latex, fontSize: 14, textColor: .black, labelMode: .text, textAlignment: .left) let (error, image, info) = mathImage.asImage() - guard error == nil, let image else { return nil } + // Nothing drawn: the italic literal, as on screen (`MathInk`). + guard error == nil, let image, MathInk.drawsSomething(latex, image: image) else { return nil } return (image, info?.descent ?? 0) } diff --git a/QuickMD/QuickMD/Views/MathBlockView.swift b/QuickMD/QuickMD/Views/MathBlockView.swift index a61cd39..1ec8909 100644 --- a/QuickMD/QuickMD/Views/MathBlockView.swift +++ b/QuickMD/QuickMD/Views/MathBlockView.swift @@ -42,7 +42,9 @@ extension MathRendering { textAlignment: .left ) let (error, image, _) = mathImage.asImage() - guard error == nil, let image = image else { return nil } + // Nothing drawn (an empty or spacing-only `$…$`, an accent over + // nothing): the italic literal, like a formula that fails. + guard error == nil, let image, MathInk.drawsSomething(latex, image: image) else { return nil } return image }, displayHeight: { latex, theme, fontScale in @@ -55,17 +57,92 @@ extension MathRendering { guard error == nil, let image = image else { return 0 } return image.size.height }, - // Exactly the test `MTMathUILabel.latex` applies: a parse error is what - // leaves the label (and `MathImage`) with nothing to draw. Parsing only — - // no font, no typesetting, no bitmap — so the per-parse check costs a - // fraction of the `displayHeight` call that follows for the same row. - typesets: { latex in - var error: NSError? - return MTMathListBuilder.build(fromString: latex, error: &error) != nil && error == nil - } + // What leaves `MTMathUILabel` (and `MathImage`) with nothing to draw: a + // parse error, a formula that parses to nothing, or one the typesetter + // declines (`MathInk.typesetsToSomething`). No bitmap, so the check + // costs a fraction of the `displayHeight` call that follows for the + // same row. + typesets: { latex in MathInk.typesetsToSomething(latex) } ) } +// MARK: - Formulas that typeset to nothing + +/// `MTMathListBuilder` accepts formulas that draw nothing — empty, only +/// spacing or style changes (`\,`, `\quad`, `\displaystyle`), empty groups +/// (`{}`, `\mathrm{}`, `{}^{}_{}`), an accent or a line over nothing +/// (`\hat{}`, `\overline{}`), `\left. \right.` — and this SwiftMath's +/// typesetter declines some it parsed (`\color`, `\textcolor`, +/// `\colorbox`: no line at all). `MTMathUILabel` then draws an empty row, +/// which the display path replaces with the LaTeX source +/// (`substitutingUntypesettableMath`, screen and print) and the inline paths +/// with the italic literal. Checked against SwiftMath's own rendering (an +/// ink scan of `MathImage`) on 36 formulas: no disagreement. +enum MathInk { + /// Parses, has something to draw, and the typesetter builds a line of some + /// size from it. + static func typesetsToSomething(_ latex: String) -> Bool { + var error: NSError? + guard let list = MTMathListBuilder.build(fromString: latex, error: &error), error == nil, + draws(list), + let line = MTTypesetter.createLineForMathList( + list, font: MathFont.latinModernFont.mtfont(size: BlockLayout.Math.fontSize), style: .display) + else { return false } + return line.width > 0 && line.ascent + line.descent > 0 + } + + /// For a formula `MathImage` already rendered (the inline paths, which + /// must not typeset twice): a size, and something to draw in it. + static func drawsSomething(_ latex: String, image: NSImage) -> Bool { + guard image.size.width > 0, image.size.height > 0 else { return false } + var error: NSError? + return draws(MTMathListBuilder.build(fromString: latex, error: &error)) + } + + /// An atom with a visible glyph, a radical sign (drawn even over nothing), + /// or content that has one — a fraction's parts or delimiters, what an + /// accent, line, colour or table holds; scripts count on any atom. A + /// fraction bar, accent or line over nothing is zero wide. + static func draws(_ list: MTMathList?) -> Bool { + guard let list else { return false } + return list.atoms.contains(where: draws) + } + + static func draws(_ atom: MTMathAtom) -> Bool { + if draws(atom.subScript) || draws(atom.superScript) { return true } + switch atom { + case let fraction as MTFraction: + return draws(fraction.numerator) || draws(fraction.denominator) + || !fraction.leftDelimiter.isEmpty || !fraction.rightDelimiter.isEmpty + case is MTRadical: + return true + case let accent as MTAccent: + return draws(accent.innerList) + case let inner as MTInner: + return draws(inner.innerList) + || inner.leftBoundary.map(draws) == true || inner.rightBoundary.map(draws) == true + case let line as MTOverLine: + return draws(line.innerList) + case let line as MTUnderLine: + return draws(line.innerList) + case is MTMathSpace, is MTMathStyle: + return false + case let color as MTMathColor: + return draws(color.innerList) + case let color as MTMathTextColor: + return draws(color.innerList) + case let box as MTMathColorbox: + return draws(box.innerList) + case let table as MTMathTable: + return table.cells.contains { row in row.contains { draws($0) } } + default: + // A glyph: ordinary, number, variable, operators, relations, + // delimiters, punctuation, placeholder. `\left.` is an empty one. + return !atom.nucleus.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } +} + // MARK: - NSViewRepresentable Wrapper /// Wraps MTMathUILabel for use in SwiftUI. diff --git a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift index c7fc917..32a211d 100644 --- a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift +++ b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift @@ -926,6 +926,39 @@ extension BlockHeightMeasurerTests { } } + /// A formula that PARSES but draws nothing — only spacing, empty groups, + /// an accent over nothing, `\color` (which this SwiftMath parses but its + /// typesetter declines) — took the formula path and drew an empty row. It + /// now falls back like an unparseable one: the same substitution, which + /// screen and print share. The production verdict + /// (`MathInk.typesetsToSomething`, in the app target: SwiftMath loads its + /// fonts from the main bundle, which the test runner is not) agreed with + /// SwiftMath's own rendering on 36 formulas in a headless check; its + /// verdicts for these drive the engine here. (`$$ $$` and `$$\n$$` never + /// become a block at all.) + func testAFormulaThatDrawsNothingBecomesALatexCodeBlock() { + let drawsNothing: Set = [#"\,"#, #"\color{red}{x}"#, #"{}^{}_{}"#, #"\hat{}"#] + let engine = MathRendering(inlineImage: { _, _, _ in nil }, displayHeight: { _, _, _ in 40 }, + typesets: { !drawsNothing.contains($0) }) + let document = "$$\n\\,\n$$\n\n$$\\color{red}{x}$$\n\n$$\n{}^{}_{}\n$$\n\n$$\\hat{}$$\n\n$$x$$" + let parsed = parse(document) + let blocks = engine.substitutingUntypesettableMath(in: parsed) + XCTAssertEqual(blocks.map(\.id), parsed.map(\.id)) + XCTAssertEqual(blocks.count, 5) + for (index, latex) in [#"\,"#, #"\color{red}{x}"#, #"{}^{}_{}"#, #"\hat{}"#].enumerated() { + guard case .codeBlock(let code, let language) = blocks[index].content else { + XCTFail("\(latex) is still \(blocks[index].content)") + continue + } + XCTAssertEqual(code, latex, "the source, verbatim") + XCTAssertEqual(language, "latex") + } + guard case .mathBlock(let working) = blocks[4].content else { + return XCTFail("a formula that draws stays display math") + } + XCTAssertEqual(working, "x") + } + func testDocumentWithoutDisplayMathNeverConsultsTheEngine() { let parsed = parse("Text with $x$ inline only.\n\n```latex\n\\not\\le\n```") let engine = MathRendering(inlineImage: { _, _, _ in nil }, displayHeight: { _, _, _ in 0 }, From 9b032bd69f85cc9936b1ed4482a0c2de0a1167a6 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:35:35 +0100 Subject: [PATCH 023/105] fix(copy): a formula shown as its source copies as $$...$$ when covered whole Since 1.14.3 a display formula the engine cannot typeset is a ```latex code block, and copying it (a drag across it, Cmd-A) gave the bare LaTeX, where 1.14.2 copied the formula as $$...$$. The substitution now marks its block (MarkdownBlock.substitutesFormula); a selection covering that row whole copies it as display math again. A selection inside its text copies those characters, a user's own ```latex fence still copies as code, and search counts in the string the row shows, as before. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 2 +- QuickMD/QuickMD/MarkdownBlock.swift | 8 +++- QuickMD/QuickMD/SelectionRows.swift | 7 +++ .../BlockHeightMeasurerTests.swift | 45 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 8b6f58d..623dc40 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -643,7 +643,7 @@ struct MathRendering { guard case .mathBlock(let latex) = block.content, !typesets(latex) else { return block } return MarkdownBlock(id: block.id, content: .codeBlock(code: latex, language: "latex"), - sourceLine: block.sourceLine) + sourceLine: block.sourceLine, substitutesFormula: true) } } } diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift index fae325a..f954a37 100644 --- a/QuickMD/QuickMD/MarkdownBlock.swift +++ b/QuickMD/QuickMD/MarkdownBlock.swift @@ -127,14 +127,20 @@ struct MarkdownBlock: Identifiable, Sendable { /// own ```` ```yaml ```` fence on the first line is an ordinary code block. /// The rhythm gives it its own role (`BlockLayout.Spacing.Role.frontMatter`). let isFrontMatter: Bool + /// A display formula the math engine cannot typeset, shown as its source + /// in a ```` ```latex ```` code block + /// (`MathRendering.substitutingUntypesettableMath`). Copied whole, it + /// reads `$$…$$` again, as the formula it is (`SelectionRows`). + let substitutesFormula: Bool init(id: String, content: BlockContent, sourceLine: Int, joinsPrevious: Bool = false, - isFrontMatter: Bool = false) { + isFrontMatter: Bool = false, substitutesFormula: Bool = false) { self.id = id self.content = content self.sourceLine = sourceLine self.joinsPrevious = joinsPrevious self.isFrontMatter = isFrontMatter + self.substitutesFormula = substitutesFormula } enum BlockContent: Sendable { diff --git a/QuickMD/QuickMD/SelectionRows.swift b/QuickMD/QuickMD/SelectionRows.swift index 79c72e4..7e8695e 100644 --- a/QuickMD/QuickMD/SelectionRows.swift +++ b/QuickMD/QuickMD/SelectionRows.swift @@ -79,6 +79,13 @@ enum SelectionRows { case .codeBlock, .text, .blockquote, .alert, .heading: guard let string = text(block), let covered = selection.range(inRow: row, rowLength: string.length) else { return nil } + if block.substitutesFormula, case .codeBlock(let latex, _) = block.content, + covered.location == 0, covered.length == string.length { + // A formula shown as its source, covered whole (a drag across + // it, ⌘A): the formula, `$$…$$`, as 1.14.2 copied it. Part of + // its text copies as that text, like any selection. + return .displayMath(latex: latex) + } let part = string.attributedSubstring(from: covered) if case .codeBlock = block.content { return .code(part) } return .text(part) diff --git a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift index 32a211d..7af76c7 100644 --- a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift +++ b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift @@ -959,6 +959,51 @@ extension BlockHeightMeasurerTests { XCTAssertEqual(working, "x") } + /// A formula shown as its source copies as the formula it is when the + /// selection covers it whole — ⌘A, a drag across it — `$$…$$`, as 1.14.2 + /// copied it; a selection inside its text copies that text, like any + /// selection. A user's own ```latex block still copies as code, and search + /// still counts in the same string the row shows. + func testASubstitutedFormulaCopiesAsTheFormulaWhenCoveredWhole() { + let blocks = refusingNot.substitutingUntypesettableMath(in: parse(Self.failingFormulaDocument) + + parse("```latex\n\\not\\le\n```")) + guard case .codeBlock(let latex, _) = blocks[1].content, blocks[1].substitutesFormula else { + return XCTFail("fixture: row 1 was not substituted") + } + XCTAssertFalse(blocks[4].substitutesFormula, "a user's own latex fence") + func text(_ block: MarkdownBlock) -> NSAttributedString? { + switch block.content { + case .text(let attributed): + return BlockTextConverter.makeNSAttributedString(from: attributed, hasInlineMath: false, + theme: theme, fontScale: 1, math: .none) + case .codeBlock(let code, _): + return BlockTextConverter.plainCode(code, theme: theme, fontScale: 1) + default: + return nil + } + } + func copy(_ anchor: SelectionPoint, _ focus: SelectionPoint) -> String { + DocumentCopyBuilder.build(SelectionRows.pieces(of: DocumentSelection(anchor: anchor, focus: focus), + blocks: blocks, text: text, table: { _ in nil })).plain + } + let length = (latex as NSString).length + let outro = text(blocks[3])!.length + // ⌘A: intro, the formula as $$…$$, the working formula, outro, the fence. + let all = DocumentSelection.selectAll(rowCount: blocks.count) { text(blocks[$0])?.length ?? 1 }! + XCTAssertEqual(copy(all.anchor, all.focus), + "Intro paragraph.\n\n$$\n\(latex)\n$$\n\n$$\nx = \\frac{1}{2}\n$$\n\nOutro paragraph.\n\n\\not\\le") + // A drag from the intro into the outro covers the formula whole. + XCTAssertTrue(copy(SelectionPoint(row: 0, offset: 6), SelectionPoint(row: 3, offset: outro)) + .contains("\n\n$$\n\(latex)\n$$\n\n")) + // The formula's own text, start to end, from inside: still whole. + XCTAssertEqual(copy(SelectionPoint(row: 1, offset: 0), SelectionPoint(row: 1, offset: length)), + "$$\n\(latex)\n$$") + // Part of it: those characters. + XCTAssertEqual(copy(SelectionPoint(row: 1, offset: 0), SelectionPoint(row: 1, offset: 6)), "V(e_1)") + // Search counts in what the row shows. + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "not", theme: theme).matchBlockIds.count, 3) + } + func testDocumentWithoutDisplayMathNeverConsultsTheEngine() { let parsed = parse("Text with $x$ inline only.\n\n```latex\n\\not\\le\n```") let engine = MathRendering(inlineImage: { _, _, _ in nil }, displayHeight: { _, _, _ in 0 }, From f536e8483569e00e163f00f513248bd5e5b39742 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:35:53 +0100 Subject: [PATCH 024/105] fix(toc): a stored Contents depth outside the choices reads as the nearest one tocMaxHeadingLevel outside {2, 3, 4, 6} (only reachable through defaults write) matched no Picker tag, so the sidebar's context menu and Settings both showed no selection. ToCDepth.normalized maps it to the nearest choice (a tie goes to the deeper one) for the Pickers, the filter and the empty-state hint; the stored value is only written when the user picks. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/SectionExtractor.swift | 13 +++++++++++++ QuickMD/QuickMD/Views/SettingsView.swift | 3 ++- QuickMD/QuickMD/Views/TableOfContentsView.swift | 8 +++++--- QuickMD/QuickMDTests/SectionExtractorTests.swift | 10 ++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/SectionExtractor.swift b/QuickMD/QuickMD/SectionExtractor.swift index 563d209..5120060 100644 --- a/QuickMD/QuickMD/SectionExtractor.swift +++ b/QuickMD/QuickMD/SectionExtractor.swift @@ -27,6 +27,19 @@ enum ToCDepth { (all, "All Headings"), (2, "Up to H2"), (3, "Up to H3"), (4, "Up to H4"), ] + /// The stored value as one of `choices`. Only `defaults write` can store + /// anything else, and a Picker whose selection matches no tag shows blank + /// (both of them did). Nearest allowed level; a tie (5) goes to the + /// deeper one, so nothing the stored value listed disappears. + static func normalized(_ stored: Int) -> Int { + let levels = choices.map(\.maxLevel) + let stored = min(max(stored, 0), 100) // no overflow in the distances + return levels.min { a, b in + let da = abs(a - stored), db = abs(b - stored) + return da != db ? da < db : a > b + } ?? all + } + static func visible(_ headings: [ToCEntry], maxLevel: Int) -> [ToCEntry] { maxLevel >= all ? headings : headings.filter { $0.level <= maxLevel } } diff --git a/QuickMD/QuickMD/Views/SettingsView.swift b/QuickMD/QuickMD/Views/SettingsView.swift index 1f9134c..b4bf8b9 100644 --- a/QuickMD/QuickMD/Views/SettingsView.swift +++ b/QuickMD/QuickMD/Views/SettingsView.swift @@ -43,7 +43,8 @@ struct GeneralSettingsView: View { } Section { - Picker("Show", selection: $tocMaxLevel) { + Picker("Show", selection: Binding(get: { ToCDepth.normalized(tocMaxLevel) }, + set: { tocMaxLevel = $0 })) { ForEach(ToCDepth.choices, id: \.maxLevel) { choice in Text(choice.title).tag(choice.maxLevel) } diff --git a/QuickMD/QuickMD/Views/TableOfContentsView.swift b/QuickMD/QuickMD/Views/TableOfContentsView.swift index 0ee6c0e..5ee2d6d 100644 --- a/QuickMD/QuickMD/Views/TableOfContentsView.swift +++ b/QuickMD/QuickMD/Views/TableOfContentsView.swift @@ -18,7 +18,8 @@ struct TableOfContentsView: View { } var body: some View { - let shown = ToCDepth.visible(headings, maxLevel: maxLevel) + let level = ToCDepth.normalized(maxLevel) + let shown = ToCDepth.visible(headings, maxLevel: level) VStack(alignment: .leading, spacing: 0) { // Header HStack(spacing: 6) { @@ -64,7 +65,7 @@ struct TableOfContentsView: View { // Every heading is deeper than the chosen depth if shown.isEmpty { VStack(alignment: .leading, spacing: 6) { - Text("No headings up to H\(maxLevel)") + Text("No headings up to H\(level)") .foregroundColor(.secondary) Button("Show All Headings") { maxLevel = ToCDepth.all } .buttonStyle(.link) @@ -77,7 +78,8 @@ struct TableOfContentsView: View { } .background(theme.backgroundColor.opacity(0.5)) .contextMenu { - Picker("Show Headings", selection: $maxLevel) { + Picker("Show Headings", selection: Binding(get: { ToCDepth.normalized(maxLevel) }, + set: { maxLevel = $0 })) { ForEach(ToCDepth.choices, id: \.maxLevel) { choice in Text(choice.title).tag(choice.maxLevel) } diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index beb40c2..7f5c6c4 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -115,6 +115,16 @@ final class SectionExtractorTests: XCTestCase { XCTAssertEqual(ToCDepth.choices.first?.maxLevel, ToCDepth.all, "All Headings is the default and first") } + /// A value only `defaults write` can store (outside 2, 3, 4, 6) left both + /// Pickers without a selected item; it reads as the nearest choice. + func testContentsDepthOutsideTheChoicesReadsAsTheNearest() { + let tags = Set(ToCDepth.choices.map(\.maxLevel)) + for stored in [-3, 0, 1, 2, 3, 4, 5, 6, 7, 99, Int.max, Int.min] { + XCTAssertTrue(tags.contains(ToCDepth.normalized(stored)), "\(stored) must be a Picker tag") + } + XCTAssertEqual([0, 1, 2, 3, 4, 5, 6, 7, 42].map(ToCDepth.normalized), [2, 2, 2, 3, 4, 6, 6, 6, 6]) + } + // MARK: - Heading anchors (#43) private func anchorHeadings(of markdown: String) -> [(id: String, text: String)] { From 53fad31f177f80aabd3d85c89cb29411f3f5af89 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:37:19 +0100 Subject: [PATCH 025/105] fix(review): count the reading minute only while the reader is active The 60 s display run kept counting while the Mac sat idle, and the input check ran only when it completed. A reader who came back after a break could get the rating prompt seconds after a single touch (E2E, App Store build: 150 s idle, one mouse move, prompt 33.6 s later). The 1.14.3 notes promise it never asks right after the Mac sat idle. Every tick now needs input within the last 60 s as well as the window on screen; an idle tick ends the run like a hidden window does. A complete run is a minute of display in which the reader kept using the Mac. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/ReviewPrompt.swift | 23 +++++-- QuickMD/QuickMD/ReviewPromptTrigger.swift | 36 +++++------ QuickMD/QuickMDTests/ReviewPromptTests.swift | 65 +++++++++++++++++--- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/QuickMD/QuickMD/ReviewPrompt.swift b/QuickMD/QuickMD/ReviewPrompt.swift index 985c2bf..bb690ac 100644 --- a/QuickMD/QuickMD/ReviewPrompt.swift +++ b/QuickMD/QuickMD/ReviewPrompt.swift @@ -43,10 +43,10 @@ struct ReviewPrompt { /// The day set only has to answer "at least 3 days"; 30 keeps it bounded /// for a reader who opens documents every day for years. static let maximumStoredDays = 30 - /// At the moment of asking, the reader must have touched a key, the mouse - /// or the trackpad within this many seconds: a window left open on an - /// unattended Mac is not reading. - static let maximumIdleAtRequest: TimeInterval = 60 + /// On every tick of the 60 s display run, the reader must have touched a + /// key, the mouse or the trackpad within this many seconds: a window left + /// open on an unattended Mac is not reading. + static let maximumIdle: TimeInterval = 60 private static let secondsPerDay: TimeInterval = 86_400 @@ -116,7 +116,7 @@ struct ReviewPrompt { static func isUserActive(secondsSinceLastInput: TimeInterval) -> Bool { secondsSinceLastInput.isFinite && secondsSinceLastInput >= 0 - && secondsSinceLastInput < maximumIdleAtRequest + && secondsSinceLastInput < maximumIdle } // MARK: Stored values, garbage-tolerant @@ -211,6 +211,19 @@ struct ReviewPromptCountdown { return now - since >= required } + /// One tick of the trigger: it counts only while the window is displayed + /// AND the reader is active (`ReviewPrompt.isUserActive`). An idle tick + /// ends the run like a hidden window does, so a complete run is 60 s of + /// display during which the reader kept using the Mac. A run can never + /// run out while they are away and then complete on the first touch + /// after they return: that touch starts a new run from zero. + mutating func advance(isDisplayed: Bool, secondsSinceLastInput: TimeInterval, + now: TimeInterval) -> Bool { + advance(isDisplayed: isDisplayed + && ReviewPrompt.isUserActive(secondsSinceLastInput: secondsSinceLastInput), + now: now) + } + /// Ends the current run (the window stopped being key); the next /// displayed tick starts a new one from zero. mutating func reset() { diff --git a/QuickMD/QuickMD/ReviewPromptTrigger.swift b/QuickMD/QuickMD/ReviewPromptTrigger.swift index fbb67ce..b3b4cee 100644 --- a/QuickMD/QuickMD/ReviewPromptTrigger.swift +++ b/QuickMD/QuickMD/ReviewPromptTrigger.swift @@ -27,10 +27,10 @@ enum ReviewPromptSession { /// unoccluded, key, no sheet or app-modal panel. A tick that fails it, losing /// key, or a gap of more than 3 s between ticks (sleep, App Nap, a stalled /// main thread — measured on the monotonic `systemUptime`) restarts the 60 s. -/// At 60 s it also needs user input within the last 60 s; if the reader is -/// idle the run is reset, so someone returning after a long break is never -/// asked on their first touch — it takes a fresh 60 s of display that ends -/// with recent input. Source Edit stops it (the task id changes and the task returns +/// Every tick also needs user input within the last 60 s: an idle tick +/// restarts the 60 s too, so someone returning after a break is asked only +/// after a full minute of display in which they kept using the Mac, never +/// right after their first touch. Source Edit stops it (the task id changes and the task returns /// while editing; leaving starts a fresh 60 s), closing the window cancels it /// with the view's task. /// @@ -91,20 +91,12 @@ struct ReviewPromptTrigger: ViewModifier { guard await Self.waitUntilKey(window) else { return } continue } - let displayedLongEnough = countdown.advance(isDisplayed: Self.isDisplayed(window), - now: ProcessInfo.processInfo.systemUptime) - if displayedLongEnough { + let readLongEnough = countdown.advance(isDisplayed: Self.isDisplayed(window), + secondsSinceLastInput: Self.secondsSinceLastInput, + now: ProcessInfo.processInfo.systemUptime) + if readLongEnough { // Another window may have asked meanwhile, or the policy changed. guard Self.canRequest(policy) else { return } - // An unattended screen is not reading. Start over rather than - // keep the run complete: a reader coming back after a long - // idle must never get the prompt on their first touch — it - // takes another 60 s of display that ends with recent input. - guard Self.userIsActive else { - countdown.reset() - try? await Task.sleep(nanoseconds: Self.tick) - continue - } ReviewPromptSession.hasRequested = true #if DEBUG reviewLog.debug("requestReview for \(Self.appVersion, privacy: .public)") @@ -136,12 +128,12 @@ struct ReviewPromptTrigger: ViewModifier { !ReviewPromptSession.hasRequested && policy.shouldRequest(now: Date(), appVersion: appVersion) } - /// Any keyboard / mouse / trackpad event within `maximumIdleAtRequest`. - /// `~0` is `kCGAnyInputEventType`; an unconvertible type reads as idle. - private static var userIsActive: Bool { - guard let anyInput = CGEventType(rawValue: ~0) else { return false } - let idle = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyInput) - return ReviewPrompt.isUserActive(secondsSinceLastInput: idle) + /// Seconds since the last keyboard / mouse / trackpad event in this login + /// session. `~0` is `kCGAnyInputEventType`; an unconvertible type reads as + /// "never" (infinite), which `ReviewPrompt.isUserActive` treats as idle. + private static var secondsSinceLastInput: TimeInterval { + guard let anyInput = CGEventType(rawValue: ~0) else { return .infinity } + return CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyInput) } @MainActor diff --git a/QuickMD/QuickMDTests/ReviewPromptTests.swift b/QuickMD/QuickMDTests/ReviewPromptTests.swift index 7ee5cf6..3962d32 100644 --- a/QuickMD/QuickMDTests/ReviewPromptTests.swift +++ b/QuickMD/QuickMDTests/ReviewPromptTests.swift @@ -277,17 +277,62 @@ final class ReviewPromptTests: XCTestCase { XCTAssertFalse(countdown.advance(isDisplayed: true, now: uptime + 60)) } - /// The trigger's idle path: a run that completed while the reader was - /// idle is reset, so the first tick after they come back does NOT - /// complete — it takes another full 60 s of display. - func testCompletedRunResetForIdleNeedsAFullNewRun() { + // MARK: - Reading: display AND recent input on every tick + + /// Feeds the trigger's ticks one second apart, the window displayed, with + /// the reader's input at `inputs` (monotonic seconds). Returns the first + /// tick at which the run completed, or nil. + private func firstCompletion(_ countdown: inout ReviewPromptCountdown, + from start: TimeInterval, through end: TimeInterval, + inputs: [TimeInterval]) -> TimeInterval? { + var now = start + while now <= end { + let lastInput = inputs.filter { $0 <= now }.max() + let idle = lastInput.map { now - $0 } ?? .infinity + if countdown.advance(isDisplayed: true, secondsSinceLastInput: idle, now: now) { return now } + now += 1 + } + return nil + } + + /// A window left open on an unattended Mac never completes a run. + func testAnUnattendedWindowNeverCompletesARun() { var countdown = ReviewPromptCountdown(required: 60) - XCTAssertTrue(tick(&countdown, from: uptime, through: uptime + 60)) - XCTAssertFalse(ReviewPrompt.isUserActive(secondsSinceLastInput: 600)) - countdown.reset() - XCTAssertFalse(countdown.advance(isDisplayed: true, now: uptime + 61), "first tick after idle") - XCTAssertFalse(tick(&countdown, from: uptime + 62, through: uptime + 120)) - XCTAssertTrue(countdown.advance(isDisplayed: true, now: uptime + 121)) + XCTAssertNil(firstCompletion(&countdown, from: uptime, through: uptime + 600, + inputs: [uptime - 1_000])) + XCTAssertNil(firstCompletion(&countdown, from: uptime + 601, through: uptime + 900, inputs: [])) + } + + /// The E2E case (2026-11-04, App Store build): 150 s unattended, then a + /// single mouse move and nothing more. The old trigger asked 33.6 s after + /// that move, because the 60 s run had been counting while the reader was + /// away. One touch must never be enough. + func testASingleTouchAfterABreakIsNotEnough() { + var countdown = ReviewPromptCountdown(required: 60) + let touch = uptime + 150 + XCTAssertNil(firstCompletion(&countdown, from: uptime, through: touch + 300, + inputs: [uptime - 1_000, touch])) + } + + /// A reader who came back and keeps using the Mac (a scroll every 20 s) + /// completes the run exactly one minute after coming back, not before. + func testAnActiveReaderCompletesAMinuteAfterComingBack() { + var countdown = ReviewPromptCountdown(required: 60) + let back = uptime + 150 + let inputs = [uptime - 1_000] + stride(from: back, through: back + 200, by: 20).map { $0 } + XCTAssertEqual(firstCompletion(&countdown, from: uptime, through: back + 200, inputs: inputs), + back + 60) + } + + /// A pause that leaves a full minute without input ends the run before it + /// completes: the minute is counted again from the first touch after it. + func testAPauseOfAMinuteRestartsTheRun() { + var countdown = ReviewPromptCountdown(required: 60) + let start = uptime + 10 // one touch: active through start + 59, idle 60 s at start + 60 + let resume = start + 100 + let inputs = [start] + stride(from: resume, through: resume + 200, by: 20).map { $0 } + XCTAssertEqual(firstCompletion(&countdown, from: uptime, through: resume + 200, inputs: inputs), + resume + 60) } func testCountdownSurvivesTimeGoingBack() { From 1705b703b29667220fcb767d7c75a81e4bc2ff45 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:38:11 +0100 Subject: [PATCH 026/105] fix(selection): a double- or triple-click in a gap selects the word where a click there lands A double-click in the space between two blocks only collapsed the selection: in the upper half there is no text view to ask, and in the lower half TextKit was asked about a point above the text. It now acts where a single click in that gap lands (hit): the end of the row above (upper half) or the start of the row below (lower half), and selects the word there (the paragraph, or a table's cell, with a triple-click) the way TextKit selects next to an insertion point - stepping back over a table's terminating line break. A neighbouring image, diagram or formula is selected whole. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/SelectionRows.swift | 20 ++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 63 ++++++++++++++----- .../QuickMDTests/DocumentSelectionTests.swift | 30 +++++++++ 3 files changed, 96 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/SelectionRows.swift b/QuickMD/QuickMD/SelectionRows.swift index 7e8695e..2d62852 100644 --- a/QuickMD/QuickMD/SelectionRows.swift +++ b/QuickMD/QuickMD/SelectionRows.swift @@ -296,6 +296,26 @@ extension SelectionRows { } } +extension SelectionRows { + /// The character a double- or triple-click in the space between two + /// blocks is about. Such a click lands where a single click there does — + /// the start of the row below (`offset` 0) or the end of the row above — + /// and is about the word at that end: the first character, or the last + /// one that is not a line break (a table's string ends in one). Nil for + /// an empty string. + static func multiClickIndex(atRowBoundary offset: Int, in string: NSString) -> Int? { + guard string.length > 0 else { return nil } + guard offset > 0 else { return 0 } + var index = min(offset, string.length) - 1 + func isLineBreak(_ index: Int) -> Bool { + let unit = string.character(at: index) + return unit == 0x0A || unit == 0x0D || unit == 0x2028 || unit == 0x2029 + } + while index > 0, isLineBreak(index) { index -= 1 } + return index + } +} + extension NSTextView { /// The character whose glyph is under `local` (view coordinates), or nil /// past the end. With `requireInside`, only when the point is ON the diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 92f4aea..3d65d7e 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -2183,6 +2183,9 @@ final class SelectionController: NSObject { /// The row's text view, when the point maps through one. let textView: SelfSizingTextView? let pointInTextView: NSPoint? + /// In the space between two blocks: `point` is the end of the row above + /// (upper half) or the start of the row below (lower half). + var inGap = false } /// Maps a window point to a document position THROUGH THE TABLE: row under @@ -2221,8 +2224,9 @@ final class SelectionController: NSObject { let spacing = spacingAbove(row) if row > 0, RowGeometry.belongsToPreviousRow(y: point.y, rowRect: rowRect, spacingAbove: spacing) { return Hit(point: SelectionPoint(row: row - 1, offset: rowLength(row - 1)), isAtomic: false, - textView: nil, pointInTextView: nil) + textView: nil, pointInTextView: nil, inGap: true) } + let inGap = row > 0 && point.y < RowGeometry.topTarget(rowRect: rowRect, spacingAbove: spacing) let block = blocks[row] let upperHalf = RowGeometry.isInUpperHalf(y: point.y, rowRect: rowRect, spacingAbove: spacing) if SelectionRows.isAtomic(block) { @@ -2231,7 +2235,7 @@ final class SelectionController: NSObject { // middle, and so does dragging UP — symmetrical, like a very tall // character. return Hit(point: SelectionPoint(row: row, offset: upperHalf ? 0 : 1), - isAtomic: true, textView: nil, pointInTextView: nil) + isAtomic: true, textView: nil, pointInTextView: nil, inGap: inGap) } if let textView = registeredTextView(for: block.id, in: tableView) { @@ -2241,13 +2245,50 @@ final class SelectionController: NSObject { isTable: textView is TableTextView, characterIndex: textView.selectionCharacterIndex(at:)) return Hit(point: SelectionPoint(row: row, offset: offset), isAtomic: false, - textView: textView, pointInTextView: local) + textView: textView, pointInTextView: local, inGap: inGap) } // A text-kind row with no registered text view to ask (not yet // registered): its whole string, by half. return Hit(point: SelectionPoint(row: row, offset: upperHalf ? 0 : rowLength(row)), - isAtomic: false, textView: nil, pointInTextView: nil) + isAtomic: false, textView: nil, pointInTextView: nil, inGap: inGap) + } + + /// What a double- (word) or triple-click (paragraph; in a table, the + /// cell) selects, from where it was pressed. In the space between two + /// blocks it acts where a single click there lands (`hit`): the end of + /// the row above (upper half) or the start of the row below (lower half) + /// — the last or the first word, as a text view selects the word next to + /// its insertion point — and a neighbouring image, diagram or formula + /// whole. Nil when there is nothing to select (no text view to ask). + private func multiClickUnit(for start: Hit, clickCount: Int, + in tableView: NSTableView) -> (start: SelectionPoint, end: SelectionPoint)? { + let granularity: NSSelectionGranularity = clickCount == 2 ? .selectByWord : .selectByParagraph + let row = start.point.row + guard row >= 0, row < blocks.count else { return nil } + if start.inGap { + if SelectionRows.isAtomic(blocks[row]) { + return (SelectionPoint(row: row, offset: 0), SelectionPoint(row: row, offset: 1)) + } + guard let textView = registeredTextView(for: blocks[row].id, in: tableView), + let storage = textView.textStorage, + let index = SelectionRows.multiClickIndex(atRowBoundary: start.point.offset, + in: storage.string as NSString) else { return nil } + let range = textView.selectionRange(forProposedRange: NSRange(location: index, length: 0), + granularity: granularity) + return (SelectionPoint(row: row, offset: range.location), + SelectionPoint(row: row, offset: NSMaxRange(range))) + } + // Double-click = word, triple-click = paragraph, in that row — in a + // table the cell (one paragraph per cell, TS-D7). The character is + // checked against the hook's answer (`start`), so a click in a cell's + // padding is about that cell, not TextKit's nearest glyph. + guard let textView = start.textView, let local = start.pointInTextView, + let index = textView.multiClickCharacterIndex(at: local, insertion: start.point.offset, + inTableCell: textView is TableTextView) else { return nil } + let range = textView.selectionRange(forProposedRange: NSRange(location: index, length: 0), + granularity: granularity) + return (SelectionPoint(row: row, offset: range.location), SelectionPoint(row: row, offset: NSMaxRange(range))) } /// The registered view for `blockId` — if it is really on display in THIS @@ -2297,19 +2338,7 @@ final class SelectionController: NSObject { anchor = existing.anchor anchorIsAtomic = false setSelection(DocumentSelection(anchor: anchor, focus: start.point)) - } else if clickCount >= 2, let textView = start.textView, let local = start.pointInTextView, - let index = textView.multiClickCharacterIndex(at: local, insertion: start.point.offset, - inTableCell: textView is TableTextView) { - // Double-click = word, triple-click = paragraph, in that row — in - // a table the cell (one paragraph per cell, TS-D7). The character - // is checked against the hook's answer (`start`), so a click in a - // cell's padding is about that cell, not TextKit's nearest glyph. - let granularity: NSSelectionGranularity = clickCount == 2 ? .selectByWord : .selectByParagraph - let range = textView.selectionRange(forProposedRange: NSRange(location: index, length: 0), - granularity: granularity) - let row = start.point.row - let selected = (start: SelectionPoint(row: row, offset: range.location), - end: SelectionPoint(row: row, offset: NSMaxRange(range))) + } else if clickCount >= 2, let selected = multiClickUnit(for: start, clickCount: clickCount, in: tableView) { unit = selected setSelection(DocumentSelection(anchor: selected.start, focus: selected.end)) } else { diff --git a/QuickMD/QuickMDTests/DocumentSelectionTests.swift b/QuickMD/QuickMDTests/DocumentSelectionTests.swift index 6d065c0..791e110 100644 --- a/QuickMD/QuickMDTests/DocumentSelectionTests.swift +++ b/QuickMD/QuickMDTests/DocumentSelectionTests.swift @@ -492,6 +492,36 @@ final class DocumentSelectionTests: XCTestCase { XCTAssertEqual(DocumentClipboard.summary(for: "", locale: english), "Copied 0 characters") } + // MARK: - Double- and triple-click in the space between two blocks + + /// A double-click in the gap between two blocks acts where a single click + /// there lands: the upper half on the end of the row above, the lower + /// half on the start of the row below. It selects the word there (the + /// paragraph with a triple-click), as a text view selects the word next to + /// its insertion point — it never just collapses the selection. + func testAMultiClickInAGapSelectsTheWordAtTheRowsEnd() { + func unit(_ string: String, atBoundary offset: Int, _ granularity: NSSelectionGranularity) -> String? { + let textView = NSTextView() + textView.configureForSelfSizing() + textView.string = string + guard let index = SelectionRows.multiClickIndex(atRowBoundary: offset, + in: textView.string as NSString) else { return nil } + let range = textView.selectionRange(forProposedRange: NSRange(location: index, length: 0), + granularity: granularity) + return (textView.string as NSString).substring(with: range) + } + let row = "Paragraph above the gap ends here." + let end = (row as NSString).length + XCTAssertEqual(unit(row, atBoundary: end, .selectByWord), ".") + XCTAssertEqual(unit("A row ending in a word", atBoundary: 22, .selectByWord), "word") + XCTAssertEqual(unit("First word of the row below", atBoundary: 0, .selectByWord), "First") + XCTAssertEqual(unit("Line one\nline two", atBoundary: 17, .selectByParagraph), "line two") + // A table's string ends in a cell's line break: the cell's last word. + XCTAssertEqual(unit("a\tb\nc\tlast\n", atBoundary: 11, .selectByWord), "last") + XCTAssertNil(SelectionRows.multiClickIndex(atRowBoundary: 0, in: "")) + XCTAssertEqual(SelectionRows.multiClickIndex(atRowBoundary: 3, in: "x\n\n"), 0) + } + // MARK: - Auto-copy decision (S-D10) private func selection(_ from: Int, _ to: Int) -> DocumentSelection { From 83a5645b95aee64ba19fc4b9199df0252b1a9ae0 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:39:37 +0100 Subject: [PATCH 027/105] fix(front-matter): a list whose item contains a comma shows one item per line List items were always joined with ", ", so authors: ["Doe, Jane", Smith] read as three names. FrontMatter.joinedItems keeps ", " when no item has a comma (the common case looks unchanged) and otherwise separates the items with U+2028 LINE SEPARATOR: a line break inside the value cell's one paragraph, so the table layout invariant (one paragraph per cell), the measurer and print all see the same string. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/FrontMatter.swift | 22 +++++++++++++--- QuickMD/QuickMDTests/FrontMatterTests.swift | 29 +++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/QuickMD/QuickMD/FrontMatter.swift b/QuickMD/QuickMD/FrontMatter.swift index 347faeb..06e4338 100644 --- a/QuickMD/QuickMD/FrontMatter.swift +++ b/QuickMD/QuickMD/FrontMatter.swift @@ -26,8 +26,9 @@ enum FrontMatter { /// (or before a trailing `:`), spaces allowed, never starting with a YAML /// indicator. /// - `key:` followed by `- item` lines (indented or at column 0, one - /// indentation per list) → the items joined with ", "; `key:` alone → "". - /// - `key: [a, b, "c, d"]` → "a, b, c, d"; `[]` → "". + /// indentation per list) → the items joined (`joinedItems`); `key:` + /// alone → "". + /// - `key: [a, b]` → "a, b"; `[a, "c, d"]` → one item per line; `[]` → "". /// - `"…"` unescapes only `\"` and `\\`; `'…'` turns `''` into `'` (a `#` /// inside quotes is text); plain values are trimmed. /// - A comment anywhere in a value (` #`, or a value starting with `#`) → @@ -42,7 +43,7 @@ enum FrontMatter { func closeOpenKey() { guard let open = openKey else { return } - properties[open.index].value = open.items.joined(separator: ", ") + properties[open.index].value = joinedItems(open.items) openKey = nil } @@ -88,6 +89,19 @@ enum FrontMatter { // MARK: - Pieces + /// Separates list items when one of them holds a comma: U+2028 LINE + /// SEPARATOR, a line break INSIDE the value's paragraph — a table cell is + /// exactly one paragraph (`TableTextBuilder`, TS-D4), which a "\n" would + /// split (and `renderedCell` turns "\n" into a space). + static let itemLineSeparator = "\u{2028}" + + /// A list's items as one value: ", " between them — unless an item itself + /// contains a comma, then `"a, b"` and `c` would read as three items, so + /// each item gets its own line (`itemLineSeparator`). + static func joinedItems(_ items: [String]) -> String { + items.joined(separator: items.contains { $0.contains(",") } ? itemLineSeparator : ", ") + } + /// Characters that make a line or value something other than a plain /// scalar / plain key (YAML indicators). A key starting with one is out of /// the subset. @@ -227,7 +241,7 @@ enum FrontMatter { skipSpaces() guard index < text.endIndex else { return nil } if text[index] == "]" { - return isNothingAfter(text[text.index(after: index)...]) ? items.joined(separator: ", ") : nil + return isNothingAfter(text[text.index(after: index)...]) ? joinedItems(items) : nil } guard text[index] == "," else { return nil } index = text.index(after: index) diff --git a/QuickMD/QuickMDTests/FrontMatterTests.swift b/QuickMD/QuickMDTests/FrontMatterTests.swift index b20c803..65885b0 100644 --- a/QuickMD/QuickMDTests/FrontMatterTests.swift +++ b/QuickMD/QuickMDTests/FrontMatterTests.swift @@ -33,18 +33,43 @@ final class FrontMatterTests: XCTestCase { func testBlockSequencesIndentedOrAtColumnZero() { XCTAssertEqual(properties("tags:\n - a\n - b"), ["tags=a, b"]) XCTAssertEqual(properties("tags:\n- a\n- b\nnext: x"), ["tags=a, b", "next=x"]) - XCTAssertEqual(properties("tags:\n - \"quoted, item\"\n - 'it''s'"), ["tags=quoted, item, it's"]) + XCTAssertEqual(properties("tags:\n - \"quoted, item\"\n - 'it''s'"), ["tags=quoted, item\u{2028}it's"]) XCTAssertEqual(properties("tags:\n - a\n # between items\n\n - b"), ["tags=a, b"]) } func testFlowSequences() { XCTAssertEqual(properties("aliases: [Review 45]"), ["aliases=Review 45"]) - XCTAssertEqual(properties("tags: [a, b, \"c, d\"]"), ["tags=a, b, c, d"]) + XCTAssertEqual(properties("tags: [a, b, \"c, d\"]"), ["tags=a\u{2028}b\u{2028}c, d"]) XCTAssertEqual(properties("tags: ['x', y ] "), ["tags=x, y"]) XCTAssertEqual(properties("tags: []"), ["tags="]) XCTAssertEqual(properties("tags: [ ]"), ["tags="]) } + /// An item that itself contains a comma would read as two items in a + /// ", " list: then (and only then) every item of THAT list gets its own + /// line — U+2028 inside the cell's one paragraph. + func testListWithACommaInAnItemShowsOneItemPerLine() throws { + XCTAssertEqual(properties("authors:\n - \"Doe, Jane\"\n - Smith\ntags: [x, y]\nplaces: ['Paris, FR', Rome]"), + ["authors=Doe, Jane\u{2028}Smith", "tags=x, y", "places=Paris, FR\u{2028}Rome"]) + XCTAssertEqual(properties("a:\n - 1,5\n - 2"), ["a=1,5\u{2028}2"], "any comma, not only \", \"") + XCTAssertEqual(properties("note: one, two"), ["note=one, two"], "a scalar is not a list") + + // The table cell keeps it as one paragraph with a line break inside. + let blocks = parse("---\nauthors: [\"Doe, Jane\", Smith]\n---\n") + guard case .table(let headers, let rows, let alignments, let style) = blocks.first?.content else { + return XCTFail("expected a properties table") + } + let table = TableTextBuilder.build(headers: headers, rows: rows, alignments: alignments, style: style, + theme: MarkdownTheme.cached(for: .light), fontScale: 1) + let valueRange = table.layout.cellRanges[0][1] + XCTAssertEqual((table.string.string as NSString).substring(with: valueRange), "Doe, Jane\u{2028}Smith") + let wide = TableTextBuilder.height(of: table.string, width: 600) + let oneLine = TableTextBuilder.build(headers: [], rows: [["authors", "Doe Jane, Smith"]], + alignments: alignments, style: style, + theme: MarkdownTheme.cached(for: .light), fontScale: 1) + XCTAssertGreaterThan(wide, TableTextBuilder.height(of: oneLine.string, width: 600) + 10, "two lines") + } + func testQuotedScalars() { XCTAssertEqual(properties(#"a: "say \"hi\" \\ there""#), [#"a=say "hi" \ there"#]) XCTAssertEqual(properties("a: 'it''s # not a comment'"), ["a=it's # not a comment"]) From 7567c219e23f6f0f46aded4bdb2e6298e4554209 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:43:24 +0100 Subject: [PATCH 028/105] perf(copy): a heading-heavy document's copy and search reuse the measured heading titles Cmd-A Cmd-C converted every heading title again on the main thread (selectableText built a renderer and an NSAttributedString per heading), and every search rendered them again. The measurer already converts each title, the same way, to measure it; it now keeps it in MeasuredBlocks.converted, where copy (for the installed content version) and search find it, as they do for paragraphs. Release harness, median of 9, headings-2000 (4001 rows): - Cmd-A Cmd-C 73.9 -> 14.4 ms (pieces 59.6 -> 0.5 ms) - search 51.7 -> 2.8 ms - measure 249.4 -> 248.9 ms (unchanged) prose-5000 unchanged. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 15 +++++++++++---- QuickMD/QuickMD/DocumentSearch.swift | 3 ++- QuickMD/QuickMD/MarkdownView.swift | 5 ++++- .../QuickMDTests/BlockHeightMeasurerTests.swift | 7 +++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 623dc40..d3e3ccb 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -1032,7 +1032,9 @@ struct BlockHeightTable: Equatable { struct MeasuredBlocks: @unchecked Sendable { let table: BlockHeightTable /// Keyed by `MarkdownBlock.id`, for the blocks whose text renders through - /// `TextBlockView` (paragraphs/footnotes, quote bodies, alert bodies). + /// `TextBlockView` (paragraphs/footnotes, quote bodies, alert bodies, and + /// heading titles — what `HeadingBlockView` converts, kept so a copy and a + /// search of a heading-heavy document never convert them again). let converted: [String: NSAttributedString] /// Keyed by `MarkdownBlock.id`, for every `.table` block: the string and /// cell layout the row was measured with, which `TableTextView` installs @@ -1284,6 +1286,7 @@ enum BlockHeightMeasurer { case .heading(let level, let title, _): let heading = headingRow(title: title, level: level, renderer: renderer, contentWidth: contentWidth) + converted[block.id] = heading.string heights.append(heading.height) kinds.append(.reported) textEdges = heading.edges @@ -1562,17 +1565,21 @@ enum BlockHeightMeasurer { /// (`BlockLayout.Heading.titleGeometry`), and the row is at least as tall /// as the button's bottom edge. The row stays `.reported`: the placed view /// still has the last word. + /// `string` is the title exactly as the view shows and the selection + /// copies it: `BlockTextConverter.makeNSAttributedString` of the same + /// `renderHeader` output, which without inline math (a heading shows + /// `$…$` literally) is this one conversion. private static func headingRow(title: String, level: Int, renderer: MarkdownRenderer, contentWidth: CGFloat) - -> (height: CGFloat, edges: BlockLayout.Spacing.Edges) { + -> (height: CGFloat, edges: BlockLayout.Spacing.Edges, string: NSAttributedString) { let attributed = renderer.renderHeader(title, level: level) let ns = (try? NSAttributedString(attributed, including: \.appKit)) - ?? NSAttributedString(string: title) + ?? NSAttributedString(string: String(attributed.characters)) let geometry = BlockLayout.Heading.titleGeometry( titleFirstBaseline: BlockLayout.Heading.titleFirstBaseline(font: renderer.headerAppKitFont(level: level))) let ink = textInk(text: ns, width: BlockLayout.Heading.titleWrapWidth(rowWidth: contentWidth)) let height = max(geometry.titleTopInset + ink.height, geometry.minimumHeight) - return (height, .text(ink, top: geometry.titleTopInset, rowHeight: height)) + return (height, .text(ink, top: geometry.titleTopInset, rowHeight: height), ns) } } diff --git a/QuickMD/QuickMD/DocumentSearch.swift b/QuickMD/QuickMD/DocumentSearch.swift index f6feecd..10e3917 100644 --- a/QuickMD/QuickMD/DocumentSearch.swift +++ b/QuickMD/QuickMD/DocumentSearch.swift @@ -264,8 +264,9 @@ enum DocumentSearch { if let shown = converted[block.id] { return shown.string } return String(renderer.renderQuotedBody(content).characters) case .heading(let level, let title, _): - // Never measured into `converted` (a `.reported` row): rendered + // The measurer keeps the title it measured; without it, rendered // here exactly as `HeadingBlockView` renders it. + if let shown = converted[block.id] { return shown.string } return String(renderer.renderHeader(title, level: level).characters) case .table, .codeBlock, .image, .svgImage, .mathBlock, .mermaidDiagram: return nil diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index d4e3f18..103c2cc 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -1292,7 +1292,10 @@ struct MarkdownView: View { case .heading(let level, let title, _): // Exactly what `HeadingBlockView` hands its `TextBlockView`: the // same `renderHeader` output through the same converter (no - // inline math — a heading shows `$…$` literally, as before). + // inline math — a heading shows `$…$` literally, as before). The + // measurer kept it (`MeasuredBlocks.converted`): a ⌘C of a + // heading-heavy document no longer converts every heading again. + if let cached { return cached } return TextBlockView.makeNSAttributedString( from: MarkdownRenderer(theme: theme, fontScale: scale).renderHeader(title, level: level), hasInlineMath: false, theme: theme, fontScale: scale) diff --git a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift index 7af76c7..d2230f5 100644 --- a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift +++ b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift @@ -433,6 +433,13 @@ final class BlockHeightMeasurerTests: XCTestCase { XCTAssertNotNil(measured.converted[block.id], "missing conversion for \(block.id)") case .alert(_, let content) where !content.isEmpty: XCTAssertNotNil(measured.converted[block.id], "missing conversion for \(block.id)") + case .heading(let level, let title, _): + // `HeadingBlockView` hosts a `TextBlockView` too: the title as it + // converts it, kept for copy and search. + let shown = BlockTextConverter.makeNSAttributedString( + from: MarkdownRenderer(theme: theme, fontScale: 1).renderHeader(title, level: level), + hasInlineMath: false, theme: theme, fontScale: 1, math: .none) + XCTAssertEqual(measured.converted[block.id], shown, "heading \(block.id)") default: XCTAssertNil(measured.converted[block.id], "unexpected conversion for \(block.id)") } From f41634b67fbbb5cf4bf19b7c3e3760707ce0703d Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:43:37 +0100 Subject: [PATCH 029/105] fix(front-matter): a value's backslashes show as written A YAML plain scalar has no Markdown escapes, but the value cell went through renderInline, whose escape parser ate the backslash before an escapable character (C:\_tmp showed C:_tmp). MarkdownRenderer gains renderInline(_:literalBackslashes:), which skips the escape parser and leaves every other inline form (links, bold, code) as before; TableTextBuilder.valueCell uses it for the screen table, search's plain string and the printed properties row. Grid cells keep Markdown escapes. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownExport.swift | 3 +-- QuickMD/QuickMD/MarkdownRenderer.swift | 17 +++++++++++- QuickMD/QuickMD/TableTextBuilder.swift | 16 ++++++++--- QuickMD/QuickMDTests/FrontMatterTests.swift | 30 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index e22615b..24f4a38 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -268,8 +268,7 @@ struct PrintableTableView: View, TableAlignmentProvider { .padding(.horizontal, Self.horizontalPadding) .padding(.vertical, 4) Rectangle().fill(theme.borderColor) - Text(TableTextBuilder.printableCell(row.count > 1 ? row[1] : "", isHeader: false, - renderer: renderer)) + Text(TableTextBuilder.valueCell(row.count > 1 ? row[1] : "", renderer: renderer)) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, Self.horizontalPadding) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 6b34535..0641827 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -140,6 +140,12 @@ struct MarkdownRenderer: Sendable { /// (makeLink → renderInlineFormatting → tryParseLink → makeLink …) and /// overflowed the parse task's stack at a few hundred levels. private var linksDisabled = false + /// True for a front-matter VALUE (`renderInline(_:literalBackslashes:)`): + /// YAML has no Markdown escapes — a plain scalar's backslash is a + /// character (`C:\_tmp`) — so `\` starts no escape there and shows as + /// written; every other inline form still renders. Copied into the link + /// text renderer with the rest of `self`. + private var escapesDisabled = false // Static precompiled regex for parsing (avoid recompilation per line) private static let taskListRegex = try! NSRegularExpression(pattern: MarkdownTheme.taskListPattern) @@ -685,6 +691,15 @@ struct MarkdownRenderer: Sendable { renderInlineFormatting(text) } + /// `renderInline` with backslash escapes off when `literalBackslashes` + /// (see `escapesDisabled`). + func renderInline(_ text: String, literalBackslashes: Bool) -> AttributedString { + guard literalBackslashes else { return renderInlineFormatting(text) } + var literal = self + literal.escapesDisabled = true + return literal.renderInlineFormatting(text) + } + private func renderInlineFormatting(_ text: String) -> AttributedString { var result = AttributedString() // Native contiguous UTF-8 storage up front (a no-op unless the line is @@ -713,7 +728,7 @@ struct MarkdownRenderer: Sendable { // Try each inline format in order (escape first, then bold+italic to catch ***) var parsed: (AttributedString, Substring)? - if parsed == nil { parsed = tryParseEscape(&remaining) } + if parsed == nil, !escapesDisabled { parsed = tryParseEscape(&remaining) } if parsed == nil { parsed = tryParseInlineCode(&remaining) } if parsed == nil { parsed = tryParseFootnoteRef(&remaining) } if parsed == nil { parsed = tryParseBoldItalic(&remaining) } diff --git a/QuickMD/QuickMD/TableTextBuilder.swift b/QuickMD/QuickMD/TableTextBuilder.swift index a2b24a3..3154ce2 100644 --- a/QuickMD/QuickMD/TableTextBuilder.swift +++ b/QuickMD/QuickMD/TableTextBuilder.swift @@ -360,12 +360,13 @@ enum TableTextBuilder { } /// One cell's rendered text (AppKit scope), without its "\n". A - /// `.properties` key (column 0) is literal (`keyCell`), every other cell - /// inline Markdown. + /// `.properties` key (column 0) is literal (`keyCell`), its value + /// `valueCell`, every grid cell inline Markdown. private static func renderedCell(_ markdown: String, column: Int, style: TableStyle, renderer: MarkdownRenderer) -> NSMutableAttributedString { - let attributed = style == .properties && column == 0 - ? keyCell(markdown, renderer: renderer) : renderer.renderInline(markdown) + let attributed = style == .properties + ? (column == 0 ? keyCell(markdown, renderer: renderer) : valueCell(markdown, renderer: renderer)) + : renderer.renderInline(markdown) let cell = NSMutableAttributedString( attributedString: (try? NSAttributedString(attributed, including: \.appKit)) ?? NSAttributedString(string: markdown)) @@ -389,6 +390,13 @@ enum TableTextBuilder { return cell } + /// A `.properties` value: inline Markdown (a URL is a link, `**x**` is + /// bold) except that a backslash is literal, as in the YAML it came from + /// (`C:\_tmp` shows `C:\_tmp`, not `C:_tmp`). Screen and print. + static func valueCell(_ value: String, renderer: MarkdownRenderer) -> AttributedString { + renderer.renderInline(value, literalBackslashes: true) + } + /// Header cells are semibold. Only raises the weight: a `**bold**` run /// stays bold, italic and monospaced runs keep their face. A family /// without a semibold face (a Settings font) falls back to its bold. diff --git a/QuickMD/QuickMDTests/FrontMatterTests.swift b/QuickMD/QuickMDTests/FrontMatterTests.swift index 65885b0..51ceba0 100644 --- a/QuickMD/QuickMDTests/FrontMatterTests.swift +++ b/QuickMD/QuickMDTests/FrontMatterTests.swift @@ -70,6 +70,36 @@ final class FrontMatterTests: XCTestCase { XCTAssertGreaterThan(wide, TableTextBuilder.height(of: oneLine.string, width: 600) + 10, "two lines") } + /// A YAML scalar's backslash is a character, not a Markdown escape: the + /// value shows as written (`C:\_tmp`, not `C:_tmp`) on screen, in search + /// and in print (`valueCell`), while its other inline Markdown renders. + /// Grid cells keep Markdown escapes. + func testValueBackslashesShowAsWritten() throws { + let theme = MarkdownTheme.cached(for: .light) + let blocks = parse("---\npath: C:\\_tmp\\new\nnote: a **bold** \\[x\\] \\\\\n---\n| a |\n|---|\n| \\_g\\_ |\n") + guard case .table(let headers, let rows, let alignments, .properties) = blocks.first?.content else { + return XCTFail("expected a properties table") + } + let table = TableTextBuilder.build(headers: headers, rows: rows, alignments: alignments, + style: .properties, theme: theme, fontScale: 1) + let string = table.string.string as NSString + XCTAssertEqual(string.substring(with: table.layout.cellRanges[0][1]), "C:\\_tmp\\new") + XCTAssertEqual(string.substring(with: table.layout.cellRanges[1][1]), "a bold \\[x\\] \\\\") + let boldFont = try XCTUnwrap(table.string.attribute(.font, at: table.layout.cellRanges[1][1].location + 2, + effectiveRange: nil) as? NSFont) + XCTAssertTrue(boldFont.fontDescriptor.symbolicTraits.contains(.bold), "other inline Markdown renders") + XCTAssertEqual(TableTextBuilder.plainString(headers: headers, rows: rows, alignments: alignments, + style: .properties, theme: theme, fontScale: 1), + table.string.string, "search counts what is shown") + + let renderer = MarkdownRenderer(theme: theme) + XCTAssertEqual(String(TableTextBuilder.valueCell("C:\\_tmp", renderer: renderer).characters), "C:\\_tmp", + "print draws the same value") + + guard case .table(_, let gridRows, _, .grid) = blocks[1].content else { return XCTFail("grid table") } + XCTAssertEqual(String(renderer.renderInline(gridRows[0][0]).characters), "_g_", "grid keeps escapes") + } + func testQuotedScalars() { XCTAssertEqual(properties(#"a: "say \"hi\" \\ there""#), [#"a=say "hi" \ there"#]) XCTAssertEqual(properties("a: 'it''s # not a comment'"), ["a=it's # not a comment"]) From 4840f086a15bc328a4e5e1fcd7232a207bcdac13 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:45:36 +0100 Subject: [PATCH 030/105] fix(front-matter): the focused search match on a key uses the text colour Keys are drawn in the theme's secondary colour, which on the orange focus highlight read poorly (values use the text colour). applyNSSearchHighlight takes an optional focusedText (colour + ranges): the part of the focused match inside those ranges gets a temporary foreground, removed on every call. TableTextView passes the key cells (TableTextLayout.firstColumnRanges) and the theme's text colour for .properties tables only. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/DocumentSearch.swift | 24 ++++++++++++++-- QuickMD/QuickMD/TableTextBuilder.swift | 3 ++ QuickMD/QuickMD/Views/TableTextView.swift | 6 +++- QuickMD/QuickMDTests/SearchTests.swift | 35 +++++++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/DocumentSearch.swift b/QuickMD/QuickMD/DocumentSearch.swift index f6feecd..ff4f857 100644 --- a/QuickMD/QuickMD/DocumentSearch.swift +++ b/QuickMD/QuickMD/DocumentSearch.swift @@ -59,11 +59,20 @@ func searchMatchRanges(in string: String, term: String) -> [NSRange] { /// (called from updateNSView); not @MainActor for older-SDK compatibility (see /// the BlockHeightCache note in TextBlockView.swift). Lives here, not with the /// views, so the tests (which do not compile the views) can run it. -func applyNSSearchHighlight(in textView: NSTextView, term: String, focusedOccurrence: Int?) { +/// +/// `focusedText`: the part of the FOCUSED match inside `ranges` is drawn in +/// `color` (a temporary foreground, removed on the next call). A front-matter +/// key is secondary-colour text, which on the orange focus read poorly; the +/// properties table passes its key cells and the theme's text colour, so a +/// focused key reads like a focused value. +func applyNSSearchHighlight(in textView: NSTextView, term: String, focusedOccurrence: Int?, + focusedText: (color: NSColor, ranges: [NSRange])? = nil) { guard let layoutManager = textView.layoutManager, let storage = textView.textStorage else { return } let fullRange = NSRange(location: 0, length: storage.length) layoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: fullRange) + // Unconditionally: a reused view may have shown a properties table before. + layoutManager.removeTemporaryAttribute(.foregroundColor, forCharacterRange: fullRange) let yellow = NSColor.systemYellow.withAlphaComponent(0.55) let orange = NSColor.systemOrange @@ -71,8 +80,17 @@ func applyNSSearchHighlight(in textView: NSTextView, term: String, focusedOccurr for (occurrenceIndex, found) in searchMatchRanges(in: storage.string, term: term).enumerated() { let safeRange = NSIntersectionRange(found, fullRange) if safeRange.length > 0 { - let color = (focusedOccurrence == occurrenceIndex) ? orange : yellow - layoutManager.addTemporaryAttribute(.backgroundColor, value: color, forCharacterRange: safeRange) + let focused = focusedOccurrence == occurrenceIndex + layoutManager.addTemporaryAttribute(.backgroundColor, value: focused ? orange : yellow, + forCharacterRange: safeRange) + if focused, let focusedText { + for range in focusedText.ranges { + let part = NSIntersectionRange(safeRange, range) + guard part.length > 0 else { continue } + layoutManager.addTemporaryAttribute(.foregroundColor, value: focusedText.color, + forCharacterRange: part) + } + } } } } diff --git a/QuickMD/QuickMD/TableTextBuilder.swift b/QuickMD/QuickMD/TableTextBuilder.swift index 3154ce2..6369262 100644 --- a/QuickMD/QuickMD/TableTextBuilder.swift +++ b/QuickMD/QuickMD/TableTextBuilder.swift @@ -44,6 +44,9 @@ struct TableTextLayout: Equatable { var bandCount: Int { cellRanges.count } + /// Column 0 of every band — a `.properties` table's key cells. + var firstColumnRanges: [NSRange] { cellRanges.compactMap(\.first) } + /// UTF-16 length of the whole string (0 for a table with no bands). var length: Int { guard let last = cellRanges.last?.last else { return 0 } diff --git a/QuickMD/QuickMD/Views/TableTextView.swift b/QuickMD/QuickMD/Views/TableTextView.swift index c2f1152..4b8999d 100644 --- a/QuickMD/QuickMD/Views/TableTextView.swift +++ b/QuickMD/QuickMD/Views/TableTextView.swift @@ -119,7 +119,11 @@ private struct TableTextRepresentable: NSViewRepresentable { // counted this block on the same characters with the same matcher // (TS-D9), so the focused occurrence is the n-th highlight in // reading order. - applyNSSearchHighlight(in: textView, term: searchTerm, focusedOccurrence: focusedOccurrence) + // A focused match on a properties KEY (secondary colour) is drawn in + // the text colour, as on a value. + let keys = style == .properties ? textView.tableLayout?.firstColumnRanges : nil + applyNSSearchHighlight(in: textView, term: searchTerm, focusedOccurrence: focusedOccurrence, + focusedText: keys.map { (NSColor(theme.textColor), $0) }) textView.lastSearchTerm = searchTerm textView.lastFocusedOccurrence = focusedOccurrence } diff --git a/QuickMD/QuickMDTests/SearchTests.swift b/QuickMD/QuickMDTests/SearchTests.swift index de3241d..4e565ee 100644 --- a/QuickMD/QuickMDTests/SearchTests.swift +++ b/QuickMD/QuickMDTests/SearchTests.swift @@ -127,6 +127,41 @@ final class SearchTests: XCTestCase { return textView } + /// A front-matter key is secondary-colour text; the FOCUSED match on a key + /// is drawn in the theme's text colour (on the orange focus the secondary + /// colour read poorly), exactly what a focused value shows. Unfocused key + /// matches and value matches keep their colours; the next call clears it. + func testFocusedMatchOnAPropertiesKeyUsesTheTextColour() throws { + let table = onlyTable("---\nauthor: Ann\nnote: by author\n---\n") + XCTAssertEqual(table.style, .properties) + let built = build(table) + let textView = tableTextView(built) + let layoutManager = try XCTUnwrap(textView.layoutManager) + let keys = built.layout.firstColumnRanges + XCTAssertEqual(keys.map { (built.string.string as NSString).substring(with: $0) }, ["author", "note"]) + let textColor = NSColor(theme.textColor) + func temporaryForeground(at index: Int) -> NSColor? { + layoutManager.temporaryAttribute(.foregroundColor, atCharacterIndex: index, effectiveRange: nil) as? NSColor + } + let valueMatch = (built.string.string as NSString).range(of: "author", options: .backwards) + + applyNSSearchHighlight(in: textView, term: "author", focusedOccurrence: 0, + focusedText: (textColor, keys)) + XCTAssertEqual(temporaryForeground(at: keys[0].location), textColor, "focused key match") + XCTAssertEqual(temporaryForeground(at: NSMaxRange(keys[0]) - 1), textColor) + XCTAssertNil(temporaryForeground(at: valueMatch.location), "unfocused value match untouched") + + applyNSSearchHighlight(in: textView, term: "author", focusedOccurrence: 1, + focusedText: (textColor, keys)) + XCTAssertNil(temporaryForeground(at: keys[0].location), "an unfocused key keeps its secondary colour") + XCTAssertNil(temporaryForeground(at: valueMatch.location), "a value already has the text colour") + + applyNSSearchHighlight(in: textView, term: "author", focusedOccurrence: 0, + focusedText: (textColor, keys)) + applyNSSearchHighlight(in: textView, term: "author", focusedOccurrence: 0) + XCTAssertNil(temporaryForeground(at: keys[0].location), "cleared for a view reused by a grid table") + } + /// The temporary background runs `applyNSSearchHighlight` left, in order. private func highlights(in textView: NSTextView) -> [(range: NSRange, focused: Bool)] { guard let layoutManager = textView.layoutManager, let length = textView.textStorage?.length else { return [] } From 39f117ea124e4dbaaa0546974796eb222f97b4e4 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:46:35 +0100 Subject: [PATCH 031/105] fix(markdown): an item line that may not interrupt a paragraph is that paragraph's text CommonMark: only a bullet, a task item or an ordered item numbered 1 may interrupt a paragraph. cb25b5f kept `1984. A year` after paragraph text in the paragraph's row, but the renderer still drew it on a line of its own as a list item. joinSoftBreaks now treats such a line as paragraph continuation (joined with a space; after a hard break, a line of its own with the delimiter escaped), so it is drawn, copied and searched as the words it is. Ordered markers also take the `)` delimiter and at most nine digits; `1)` interrupts a paragraph like `1.`, and both are drawn `n.`, as GitHub draws them. A line that starts a block, and an item inside a list, is still an item whatever its number. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 4 +- QuickMD/QuickMD/MarkdownRenderer.swift | 45 +++++++++++++++---- .../QuickMDTests/VerticalRhythmTests.swift | 20 ++++++--- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index fd1ebb8..9e33c30 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -907,8 +907,8 @@ struct MarkdownBlockParser: Sendable { } // A list starts at an item that opens the row, or that may // interrupt the paragraph before it (CommonMark: a bullet, or an - // ordered item numbered 1). Any other item line is paragraph text - // the renderer draws as an item, as it always did. + // ordered item numbered 1). `joinSoftBreaks` has already made any + // other item line after paragraph text part of that text. let startsList = !inList && MarkdownRenderer.isListItem(line) && (row.isEmpty || MarkdownRenderer.canInterruptParagraph(line)) if startsList { flushRow() } diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 6b34535..83034a9 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -235,7 +235,7 @@ struct MarkdownRenderer: Sendable { /// `renderListItem` will emit it (indent spaces + normalised number). private static func orderedItem(_ line: String) -> (prefix: String, level: Int)? { let trimmed = line.trimmingCharacters(in: .whitespaces) - guard trimmed.range(of: #"^(\d+)\.\s"#, options: .regularExpression) != nil else { return nil } + guard trimmed.range(of: Self.orderedMarkerPattern, options: .regularExpression) != nil else { return nil } let level = listLevel(for: line.prefix(while: { $0 == " " || $0 == "\t" })) let number = Int(trimmed.prefix(while: { $0.isNumber })) ?? 1 return (indentSpaces(level) + "\(number). ", level) @@ -268,12 +268,16 @@ struct MarkdownRenderer: Sendable { static func joinSoftBreaksIndexed(_ lines: [String]) -> [(line: String, first: Int)] { var joined: [(line: String, first: Int)] = [] var openParagraph = false // last joined line can absorb a continuation + // Inside a paragraph that is not a list item's, until a blank or + // structural line — across hard breaks too. + var inPlainParagraph = false for (index, line) in lines.enumerated() { let kind = classify(line) guard kind != .structural else { joined.append((line, index)) openParagraph = false + inPlainParagraph = false continue } @@ -282,10 +286,21 @@ struct MarkdownRenderer: Sendable { while text.last == " " || text.last == "\t" { text.removeLast() } if hardBreak && text.hasSuffix("\\") { text.removeLast() } - if kind == .paragraph && openParagraph { + if kind == .listItem && inPlainParagraph && !canInterruptParagraph(line) { + // CommonMark: only a bullet or an ordered item numbered 1 may + // interrupt a paragraph. `1984. A year` after paragraph text + // is more of that text — drawn, copied and searched as such. + if openParagraph { + joined[joined.count - 1].line += " " + text.trimmingCharacters(in: .whitespaces) + } else { + // After a hard break: its own visual line, still text. + joined.append((asParagraphText(text), index)) + } + } else if kind == .paragraph && openParagraph { joined[joined.count - 1].line += " " + text.trimmingCharacters(in: .whitespaces) } else { joined.append((text, index)) + inPlainParagraph = kind == .paragraph } openParagraph = !hardBreak } @@ -307,15 +322,27 @@ struct MarkdownRenderer: Sendable { /// A bullet, ordered or task list item — `renderLine` renders it as one. static func isListItem(_ line: String) -> Bool { classify(line) == .listItem } + /// An ordered item's marker (CommonMark): one to nine digits, then `.` or + /// `)`, then white space. Drawn as `n.` whichever delimiter it had, as + /// GitHub draws both (an HTML list). + static let orderedMarkerPattern = #"^(\d{1,9})[.)]\s"# + + /// `line` (an ordered item line that may not interrupt a paragraph) as + /// paragraph text: its delimiter backslash-escaped, so `renderLine` draws + /// `1984. A year` as the words it is and never as an item. + static func asParagraphText(_ line: String) -> String { + line.replacingOccurrences(of: #"^(\s*\d{1,9})([.)])"#, with: "$1\\\\$2", options: .regularExpression) + } + /// Whether this item line may interrupt a paragraph (CommonMark): a - /// bullet or task item, or an ordered item numbered 1 — and not an empty - /// one. Any other item line after paragraph text (`1984. was a year`) - /// continues the paragraph: it is still drawn as an item, but it is no - /// block boundary, so the parser does not start a new row there. + /// bullet or task item, or an ordered item numbered 1 (`1.` or `1)`) — + /// and not an empty one. Any other item line after paragraph text + /// (`1984. was a year`) continues the paragraph: `joinSoftBreaks` makes + /// it paragraph text, so it is no item and no block boundary. static func canInterruptParagraph(_ line: String) -> Bool { guard isListItem(line) else { return false } let trimmed = line.trimmingCharacters(in: .whitespaces) - if let marker = trimmed.range(of: #"^\d+\."#, options: .regularExpression) { + if let marker = trimmed.range(of: #"^\d{1,9}[.)]"#, options: .regularExpression) { guard Int(trimmed[marker].dropLast()) == 1 else { return false } return !trimmed[marker.upperBound...].trimmingCharacters(in: .whitespaces).isEmpty } @@ -338,7 +365,7 @@ struct MarkdownRenderer: Sendable { if trimmed.range(of: MarkdownTheme.horizontalRulePattern, options: .regularExpression) != nil { return .structural } if taskListRegex.firstMatch(in: line, range: nsRange) != nil { return .listItem } if ["- ", "* ", "+ "].contains(where: { trimmed.hasPrefix($0) }) { return .listItem } - if trimmed.range(of: #"^(\d+)\.\s"#, options: .regularExpression) != nil { return .listItem } + if trimmed.range(of: orderedMarkerPattern, options: .regularExpression) != nil { return .listItem } return .paragraph } @@ -382,7 +409,7 @@ struct MarkdownRenderer: Sendable { } // Ordered list - if let match = trimmed.range(of: #"^(\d+)\.\s"#, options: .regularExpression) { + if let match = trimmed.range(of: Self.orderedMarkerPattern, options: .regularExpression) { let level = Self.listLevel(for: line.prefix(while: { $0 == " " || $0 == "\t" })) let number = Int(trimmed.prefix(while: { $0.isNumber })) ?? 1 let content = String(trimmed[match.upperBound...]) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 0989cad..79cd828 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -123,17 +123,25 @@ final class VerticalRhythmTests: XCTestCase { /// paragraph. `1984. …` after paragraph text continues the paragraph — /// drawn as an item as before, but no new row and no gap. func testOnlyAnItemThatMayInterruptAParagraphStartsARow() { - for line in ["- item", "* item", "+ item", "- [ ] task", "1. first"] { + for line in ["- item", "* item", "+ item", "- [ ] task", "1. first", "1) first"] { XCTAssertEqual(parse("Paragraph text.\n" + line).count, 2, line) } - for line in ["1984. A year", "2. second", "-", "1."] { + for line in ["1984. A year", "2. second", "2) second", "-", "1."] { XCTAssertEqual(parse("Paragraph text.\n" + line).count, 1, line) } - // Still drawn on its own line, as an item. - let year = plain(parse("Paragraph text.\n1984. A year\n1985. Another")[0]) - XCTAssertTrue(year.contains("\n1984.") && year.contains("\n1985."), year) - // A list that starts a row is still a list, whatever its number. + // CommonMark: the rest of the paragraph, drawn as its text — no item + // marker, no line of its own — and searched and copied as such. + let blocks = parse("Paragraph text.\n1984. A year\n1985) Another") + XCTAssertEqual(plain(blocks[0]), "Paragraph text. 1984. A year 1985) Another") + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "text. 1984. A year 1985)").matchBlockIds.count, 1) + // After a hard break: a line of its own, still text. + XCTAssertEqual(plain(parse("Paragraph text. \n1984. A year")[0]), "Paragraph text.\n1984. A year") + // A list that starts a row is still a list, whatever its number or + // delimiter (drawn `n.`, as GitHub draws both). XCTAssertEqual(parse("Text.\n\n3. third\n4. fourth").count, 2) + XCTAssertTrue(plain(parse("1984) A year")[0]).hasPrefix("1984. A year"), plain(parse("1984) A year")[0])) + // Inside a list, an item of any number is the next item. + XCTAssertTrue(plain(parse("1. one\n1984. two")[0]).contains("\n1984. two")) } // MARK: - Headings From 73bc0f7755610f5caf2813b6d087bfe8a54caca7 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:48:24 +0100 Subject: [PATCH 032/105] fix(headings): character references show as characters, ids match GitHub A heading's & / < / © were drawn as written and slugged to "amp", while GitHub decodes them first (Q&A is #qa). renderHeader now decodes the references HTMLImageSyntax.decodeEntities knows in plain and link text, after inline parsing (a decoded * is a literal *, a code span keeps the reference). Every heading consumer goes through renderHeader (view, measurer, ids, search, print), so they stay in step; the Contents sidebar decodes the same way. Section copy is unaffected (parser sourceLine). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownRenderer.swift | 16 ++++++++-- .../QuickMD/Views/TableOfContentsView.swift | 3 +- .../QuickMDTests/SectionExtractorTests.swift | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 0641827..72ef3b6 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -146,6 +146,13 @@ struct MarkdownRenderer: Sendable { /// written; every other inline form still renders. Copied into the link /// text renderer with the rest of `self`. private var escapesDisabled = false + /// True while rendering a HEADING (`renderHeader`): character references + /// in its plain text and link text (`&`, `<`, `©` — the set + /// `HTMLImageSyntax.decodeEntities` knows) show as the character, as on + /// GitHub, and so the heading's id slugs `Q&A`, not `Q&A`. Decoded + /// AFTER inline parsing, so `*` is a literal `*`, never emphasis; + /// code spans keep the reference as written. + private var decodesEntities = false // Static precompiled regex for parsing (avoid recompilation per line) private static let taskListRegex = try! NSRegularExpression(pattern: MarkdownTheme.taskListPattern) @@ -408,7 +415,9 @@ struct MarkdownRenderer: Sendable { // MARK: - Block Renderers func renderHeader(_ text: String, level: Int) -> AttributedString { - var attr = renderInlineFormatting(text) + var heading = self + heading.decodesEntities = true + var attr = heading.renderInlineFormatting(text) attr.setDualFont(size: headerFontSize(level: level), bold: true, fonts: theme.fonts) attr.setDualForeground(theme.textColor) return attr @@ -717,7 +726,8 @@ struct MarkdownRenderer: Sendable { // Helper to flush buffered plain text func flushPlainText() { guard !plainTextBuffer.isEmpty else { return } - var attr = AttributedString(plainTextBuffer) + var attr = AttributedString(decodesEntities ? HTMLImageSyntax.decodeEntities(plainTextBuffer) + : plainTextBuffer) attr.setDualFont(size: scaled(14), fonts: theme.fonts) attr.setDualForeground(theme.textColor) result.append(attr) @@ -934,7 +944,7 @@ struct MarkdownRenderer: Sendable { linkText.linksDisabled = true attr = linkText.renderInlineFormatting(text) } else { - attr = AttributedString(text) + attr = AttributedString(decodesEntities ? HTMLImageSyntax.decodeEntities(text) : text) attr.setDualFont(size: scaled(14), fonts: theme.fonts) } attr.setDualForeground(theme.linkColor) diff --git a/QuickMD/QuickMD/Views/TableOfContentsView.swift b/QuickMD/QuickMD/Views/TableOfContentsView.swift index 5ee2d6d..07e74be 100644 --- a/QuickMD/QuickMD/Views/TableOfContentsView.swift +++ b/QuickMD/QuickMD/Views/TableOfContentsView.swift @@ -104,7 +104,8 @@ private struct ToCEntryRow: View { Button { onSelect() } label: { - Text(entry.title) + // Character references as the heading shows them (`Q&A` → `Q&A`). + Text(HTMLImageSyntax.decodeEntities(entry.title)) .font(.system(size: fontSize(for: entry.level), weight: fontWeight(for: entry.level))) .foregroundColor(theme.textColor) .lineLimit(2) diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 7f5c6c4..0491fad 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -176,6 +176,35 @@ final class SectionExtractorTests: XCTestCase { ]) } + /// Character references in a heading show as their characters (GitHub + /// decodes them before its slug) — ids from GitHub's renderer + /// (`gh api markdown`, 2026-11-04). A code span keeps the reference, and a + /// decoded `*` is a literal `*`, not emphasis. + func testHeadingEntitiesAreDecodedLikeGitHub() { + let headings = anchorHeadings(of: """ + # Q&A <tag> © + ## Tom & Jerry + ## `a & b` + ## R&D  x + ## a *b* + ## [Q&A](#x) here + ## **Tom & Jerry** + ## 5 < 6 + """) + XCTAssertEqual(headings.map(\.text), [ + "Q&A ©", "Tom & Jerry", "a & b", "R&D \u{00A0}x", "a *b*", + "Q&A here", "Tom & Jerry", "5 < 6", + ]) + XCTAssertEqual(HeadingAnchors.ids(for: headings.map(\.text)), [ + "qa-tag-", "tom--jerry", "a-amp-b", "rd-x", "a-b", "qa-here", "tom--jerry-1", "5--6", + ]) + + // The heading view, search, print and the measurer all draw `renderHeader`. + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) + let italic = renderer.renderHeader("a *b*", level: 2) + XCTAssertEqual(italic.runs.count, 1, "no emphasis run from a decoded asterisk") + } + func testFragmentFindsItsHeading() { let headings = anchorHeadings(of: """ # Guide From 9bbf6ae65a22f92dc70aa22e8b74c20d3aea757c Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:50:22 +0100 Subject: [PATCH 033/105] fix(markdown): a line of no-break spaces is text, not a blank line CommonMark's blank line is nothing but spaces and tabs. Every blank test used CharacterSet.whitespaces, which is all Unicode space separators, so a line of no-break spaces (U+00A0) ended a paragraph, split a list in two and broke a quote into paragraphs. MarkdownRenderer.isBlankLine is now the one blank test of the parser (rows, fences, definition lists), the renderer (line classes, quote and alert bodies on screen and in print) and section copy; continuation lines and quote lines lose spaces and tabs only. Such a line continues the paragraph it follows, and alone it is a paragraph of its own (GitHub:

 

), one empty-looking line. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 20 +++++++---- QuickMD/QuickMD/MarkdownExport.swift | 4 +-- QuickMD/QuickMD/MarkdownRenderer.swift | 27 ++++++++++++--- QuickMD/QuickMD/SectionExtractor.swift | 2 +- .../QuickMDTests/VerticalRhythmTests.swift | 34 +++++++++++++++++-- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 9e33c30..0bacb88 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -145,7 +145,7 @@ struct MarkdownBlockParser: Sendable { // Closing fence must be same char, same or greater length, nothing else while i < lines.count { let closeLine = lines[i].trimmingCharacters(in: .whitespaces) - if closeLine.hasPrefix(openingFence) && closeLine.drop(while: { $0 == fenceChar }).trimmingCharacters(in: .whitespaces).isEmpty { + if closeLine.hasPrefix(openingFence) && MarkdownRenderer.isBlankLine(closeLine.drop(while: { $0 == fenceChar })) { i += 1 break } @@ -337,7 +337,9 @@ struct MarkdownBlockParser: Sendable { var previousGroupWasQuote = false while i < lines.count { - let qLine = lines[i].trimmingCharacters(in: .whitespaces) + // Spaces and tabs only: a quote line of no-break spaces + // (`> \u{00A0}`) is text, not an empty quote line. + let qLine = lines[i].trimmingCharacters(in: Self.spacesAndTabs) guard qLine.hasPrefix(">") else { break } // Count nesting level (number of leading > chars) @@ -361,7 +363,7 @@ struct MarkdownBlockParser: Sendable { i += 1 while i < lines.count { - let nextLine = lines[i].trimmingCharacters(in: .whitespaces) + let nextLine = lines[i].trimmingCharacters(in: Self.spacesAndTabs) guard nextLine.hasPrefix(">") else { break } var nextLevel = 0 @@ -603,11 +605,17 @@ struct MarkdownBlockParser: Sendable { // whitespace is kept: two trailing spaces are a hard break, and the // renderer's soft-break pass has to still see them. let text = String(scanner.drop(while: { $0 == " " || $0 == "\t" })) - return text.trimmingCharacters(in: .whitespaces).isEmpty ? nil : text + return MarkdownRenderer.isBlankLine(text) ? nil : text } + /// CommonMark's white space around block markers: spaces and tabs, not + /// every Unicode space separator (`isBlank`). + static let spacesAndTabs = CharacterSet(charactersIn: " \t") + + /// CommonMark's blank line (`MarkdownRenderer.isBlankLine`): a line of + /// no-break spaces is text, not a separator. private static func isBlank(_ line: String) -> Bool { - line.trimmingCharacters(in: .whitespaces).isEmpty + MarkdownRenderer.isBlankLine(line) } /// Can this line be a definition-list TERM? Non-blank, not a definition @@ -622,7 +630,7 @@ struct MarkdownBlockParser: Sendable { /// block, so it must not be a term either. private func isTermCandidate(_ line: String, references: [String: String]) -> Bool { let trimmed = line.trimmingCharacters(in: .whitespaces) - guard !trimmed.isEmpty, Self.definitionText(of: line) == nil else { return false } + guard !Self.isBlank(line), Self.definitionText(of: line) == nil else { return false } // Fences, quotes, display math if trimmed.hasPrefix("```") || trimmed.hasPrefix("~~~") { return false } diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index 98bb744..241f7fc 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -568,7 +568,7 @@ struct PrintableBlockquoteView: View { // quote written one sentence per line prints as one paragraph. VStack(alignment: .leading, spacing: 4) { ForEach(Array(MarkdownRenderer.joinSoftBreaks(content.components(separatedBy: "\n")).enumerated()), id: \.offset) { _, line in - if line.trimmingCharacters(in: .whitespaces).isEmpty { + if MarkdownRenderer.isBlankLine(line) { Text(" ").font(renderer.theme.fonts.swiftUI(size: 12)) } else { Text(renderer.renderInline(line)) @@ -607,7 +607,7 @@ struct PrintableAlertView: View { // Soft breaks joined first — mirrors AlertBlockView on screen. ForEach(Array(MarkdownRenderer.joinSoftBreaks(content.components(separatedBy: "\n")).enumerated()), id: \.offset) { _, line in - if line.trimmingCharacters(in: .whitespaces).isEmpty { + if MarkdownRenderer.isBlankLine(line) { Text(" ").font(renderer.theme.fonts.swiftUI(size: 12)) } else { Text(renderer.renderInline(line)) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 83034a9..7024773 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -291,13 +291,13 @@ struct MarkdownRenderer: Sendable { // interrupt a paragraph. `1984. A year` after paragraph text // is more of that text — drawn, copied and searched as such. if openParagraph { - joined[joined.count - 1].line += " " + text.trimmingCharacters(in: .whitespaces) + joined[joined.count - 1].line += " " + continuationText(text) } else { // After a hard break: its own visual line, still text. joined.append((asParagraphText(text), index)) } } else if kind == .paragraph && openParagraph { - joined[joined.count - 1].line += " " + text.trimmingCharacters(in: .whitespaces) + joined[joined.count - 1].line += " " + continuationText(text) } else { joined.append((text, index)) inPlainParagraph = kind == .paragraph @@ -322,6 +322,23 @@ struct MarkdownRenderer: Sendable { /// A bullet, ordered or task list item — `renderLine` renders it as one. static func isListItem(_ line: String) -> Bool { classify(line) == .listItem } + /// CommonMark's blank line: nothing but spaces and tabs — the ONE blank + /// test of the parser and the renderers. Not "any white space": + /// `CharacterSet.whitespaces` is every Unicode space separator, so it + /// counted a line of no-break spaces (U+00A0) blank. Such a line is + /// paragraph text (how authors write an empty-looking paragraph): after + /// text it continues the paragraph, alone it is a paragraph of its own; + /// it never ends a block or separates two. + static func isBlankLine(_ line: S) -> Bool { + line.allSatisfy { $0 == " " || $0 == "\t" } + } + + /// A continuation line's text: CommonMark strips its leading spaces and + /// tabs, and nothing else (a no-break space is text). + private static func continuationText(_ text: String) -> String { + String(text.drop(while: { $0 == " " || $0 == "\t" })) + } + /// An ordered item's marker (CommonMark): one to nine digits, then `.` or /// `)`, then white space. Drawn as `n.` whichever delimiter it had, as /// GitHub draws both (an HTML list). @@ -358,8 +375,8 @@ struct MarkdownRenderer: Sendable { /// Must mirror the dispatch in `renderLine`, or a structural line could /// get glued into the paragraph before it. private static func classify(_ line: String) -> LineKind { + if isBlankLine(line) { return .structural } let trimmed = line.trimmingCharacters(in: .whitespaces) - if trimmed.isEmpty { return .structural } let nsRange = NSRange(line.startIndex..., in: line) if headerRegex.firstMatch(in: line, range: nsRange) != nil { return .structural } if trimmed.range(of: MarkdownTheme.horizontalRulePattern, options: .regularExpression) != nil { return .structural } @@ -418,7 +435,7 @@ struct MarkdownRenderer: Sendable { } // Empty line - if trimmed.isEmpty { + if Self.isBlankLine(line) { return AttributedString("\n") } @@ -464,7 +481,7 @@ struct MarkdownRenderer: Sendable { var result = AttributedString() let lines = Self.joinSoftBreaks(content.components(separatedBy: "\n")) for (index, line) in lines.enumerated() { - if line.trimmingCharacters(in: .whitespaces).isEmpty { + if Self.isBlankLine(line) { result.append(AttributedString(" ")) } else { result.append(renderInline(line)) diff --git a/QuickMD/QuickMD/SectionExtractor.swift b/QuickMD/QuickMD/SectionExtractor.swift index 563d209..f038db9 100644 --- a/QuickMD/QuickMD/SectionExtractor.swift +++ b/QuickMD/QuickMD/SectionExtractor.swift @@ -165,7 +165,7 @@ enum SectionExtractor { guard end > start else { return nil } var section = Array(lines[start.. 

`). + /// It ends no block and separates none. + func testALineOfNoBreakSpacesIsTextNotABlankLine() { + let nbsp = "\u{00A0}" + let continued = parse("Para one\n\(nbsp)\nPara two") + XCTAssertEqual(continued.count, 1) + XCTAssertEqual(plain(continued[0]), "Para one \(nbsp) Para two") + let alone = parse("Para one\n\n\(nbsp)\(nbsp)\n\nPara two") + XCTAssertEqual(alone.map(plain), ["Para one", "\(nbsp)\(nbsp)", "Para two"]) + XCTAssertEqual(measure(alone).table.heights[1], measure(alone).table.heights[0], "one line") + // No separator in a list: one tight list. + XCTAssertEqual(parse("- a\n\(nbsp)\n- b").count, 1) + // A quote body: one paragraph. + guard case .blockquote(let body, _) = parse("> a\n> \(nbsp)\n> b")[0].content else { + return XCTFail("not a quote") + } + XCTAssertEqual(String(MarkdownRenderer(theme: theme).renderQuotedBody(body).characters), "a \(nbsp) b") + // Spaces and tabs are still a blank line. + XCTAssertEqual(parse("Para one\n \t \nPara two").count, 2) + XCTAssertTrue(MarkdownRenderer.isBlankLine(" \t ")) + XCTAssertTrue(MarkdownRenderer.isBlankLine("")) + XCTAssertFalse(MarkdownRenderer.isBlankLine(nbsp)) + XCTAssertFalse(MarkdownRenderer.isBlankLine(" \(nbsp)\t")) + } + func testRowsAreCommonMarkBlocks() { let blocks = parse("Intro line one\nintro line two.\n- item a\n- item b\n\nOutro.\n***\nAfter the rule.") XCTAssertEqual(blocks.map(plain).count, 5, "\(blocks.map(plain))") @@ -119,9 +147,9 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertTrue(blocks.allSatisfy { !$0.joinsPrevious }) } - /// CommonMark: only a bullet, or an ordered item numbered 1, interrupts a - /// paragraph. `1984. …` after paragraph text continues the paragraph — - /// drawn as an item as before, but no new row and no gap. + /// CommonMark: only a bullet, or an ordered item numbered 1 (`1.` or + /// `1)`), interrupts a paragraph. `1984. …` after paragraph text continues + /// the paragraph — its text, in its row, with no gap. func testOnlyAnItemThatMayInterruptAParagraphStartsARow() { for line in ["- item", "* item", "+ item", "- [ ] task", "1. first", "1) first"] { XCTAssertEqual(parse("Paragraph text.\n" + line).count, 2, line) From 46a500198ed9a577cb842ba90479c2ec063ffe14 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:53:50 +0100 Subject: [PATCH 034/105] fix(references): reference links and images resolve in quotes, alerts and table cells Paragraphs are rendered in the parser with the document's definitions, but blockquote / alert bodies and table cells keep their markdown and are rendered again by the views, the height measurer, search and print, each with a definition-less MarkdownRenderer, so [text][id] and ![alt][id] stayed literal there. Instead of threading the definitions through every consumer (and keeping the measurer's exact heights in step), the parser now inlines resolvable references in those bodies once (ReferenceLinks): [text](), with the destination escaped so InlineSyntaxScanner reads back the exact URL. The scan mirrors the renderer (escapes, code spans, ***/~~ spans, inline links, full/collapsed/shortcut order, link-wrapped images); a test checks that every rewritten line renders exactly like the definition-aware renderer, characters and links. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 6 + QuickMD/QuickMD/MarkdownBlockParser.swift | 15 +- QuickMD/QuickMD/ReferenceLinks.swift | 180 ++++++++++++++++++++++ QuickMD/QuickMDTests/ParserTests.swift | 71 +++++++++ 4 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 QuickMD/QuickMD/ReferenceLinks.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 3c98c6e..abf3d2a 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -42,6 +42,8 @@ A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A0 /* VerticalRhythm.swift */; }; A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A0 /* VerticalRhythm.swift */; }; + A1B2C3D401000301 /* ReferenceLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000301 /* ReferenceLinks.swift */; }; + A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000301 /* ReferenceLinks.swift */; }; A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C1 /* TableSelectionTests.swift */; }; A1B2C3D40100000E /* ImageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000012 /* ImageBlockView.swift */; }; A1B2C3D40100000F /* BlockquoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000013 /* BlockquoteView.swift */; }; @@ -235,6 +237,7 @@ A1B2C3D400000200 /* PrintableTableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintableTableTests.swift; sourceTree = ""; }; A1B2C3D4000001C0 /* SelectionRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRows.swift; sourceTree = ""; }; A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; + A1B2C3D400000301 /* ReferenceLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceLinks.swift; sourceTree = ""; }; A1B2C3D4000001C1 /* TableSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSelectionTests.swift; sourceTree = ""; }; A1B2C3D400000012 /* ImageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlockView.swift; sourceTree = ""; }; A1B2C3D400000013 /* BlockquoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockquoteView.swift; sourceTree = ""; }; @@ -387,6 +390,7 @@ A1B2C3D4000000C0 /* DocumentSelection.swift */, A1B2C3D4000001C0 /* SelectionRows.swift */, A1B2C3D4000002A0 /* VerticalRhythm.swift */, + A1B2C3D400000301 /* ReferenceLinks.swift */, A1B2C3D400000110 /* DocumentReadingPosition.swift */, A1B2C3D400000120 /* DocumentFileFormat.swift */, A1B2C3D400000121 /* DocumentFileWriter.swift */, @@ -685,6 +689,7 @@ A1B2C3D4010000C0 /* DocumentSelection.swift in Sources */, A1B2C3D4010001C0 /* SelectionRows.swift in Sources */, A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */, + A1B2C3D401000301 /* ReferenceLinks.swift in Sources */, A1B2C3D401000110 /* DocumentReadingPosition.swift in Sources */, A1B2C3D401000120 /* DocumentFileFormat.swift in Sources */, A1B2C3D401000122 /* DocumentFileWriter.swift in Sources */, @@ -739,6 +744,7 @@ A1B2C3D4010000C1 /* DocumentSelection.swift in Sources (Tests) */, A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */, A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */, + A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */, A1B2C3D4010000C2 /* DocumentSelectionTests.swift in Sources */, A1B2C3D401000111 /* DocumentReadingPosition.swift in Sources (Tests) */, A1B2C3D401000112 /* ExternalEditorLineTests.swift in Sources */, diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 2a42e6a..22c371d 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -319,6 +319,11 @@ struct MarkdownBlockParser: Sendable { headers = normalizeArray(headers, to: columnCount, default: "") alignments = normalizeArray(alignments, to: columnCount, default: .leading) rows = rows.map { normalizeArray($0, to: columnCount, default: "") } + // Cells are rendered later without the definitions (`ReferenceLinks`). + if !referenceDefinitions.isEmpty { + headers = headers.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions) } + rows = rows.map { $0.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions) } } + } // Header row line — `i` has already walked past the body rows. blocks.append(.table(index: blockIndex, headers: headers, rows: rows, alignments: alignments, @@ -379,15 +384,17 @@ struct MarkdownBlockParser: Sendable { i += 1 } - let content = groupLines.joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines) + // Quote and alert bodies are rendered later without the + // definitions, so references are resolved here (`ReferenceLinks`). + let content = ReferenceLinks.inlined(groupLines.joined(separator: "\n") + .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions) if !content.isEmpty { // GFM alert: level-1 quote whose first line is exactly // `[!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / // `[!CAUTION]` (marker alone on the line, per GitHub spec). if groupLevel == 1, let kind = AlertKind(markerLine: groupLines[0]) { - let body = groupLines.dropFirst().joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines) + let body = ReferenceLinks.inlined(groupLines.dropFirst().joined(separator: "\n") + .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions) blocks.append(.alert(index: blockIndex, kind: kind, content: body, sourceLine: groupSourceLine)) previousGroupWasQuote = false } else { diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift new file mode 100644 index 0000000..a47c982 --- /dev/null +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -0,0 +1,180 @@ +import Foundation + +// MARK: - Reference links inside quotes, alerts and table cells + +/// Rewrites reference-style links and images (`[text][id]`, `[text][]`, +/// `[text]`, `![alt][id]`, `![alt][]`, `![alt]`) whose definition exists into +/// their inline form (`[text]()`), for the block kinds whose RAW markdown +/// is stored and rendered later: blockquotes, alerts and table cells. +/// +/// Why rewrite instead of handing those renderers the definitions: paragraphs +/// are rendered inside the parser with the document's definitions, but a +/// quote, alert or table keeps its markdown and is rendered again by every +/// consumer — the views, `BlockHeightMeasurer` (whose `.exact` heights must +/// match the view), search, print — each with a definition-less +/// `MarkdownRenderer`. Resolving the references once, where the definitions +/// are known, gives every one of them the same text with no new plumbing, so +/// they cannot disagree. +/// +/// The scan mirrors `MarkdownRenderer.renderInlineFormatting` where it +/// matters: escapes and code spans are copied as written, `***…***` and +/// `~~…~~` (rendered without inline parsing) too, inline links are copied +/// whole, the reference rules are `tryParseLink`'s (full, collapsed, then +/// shortcut, ids lowercased) and `InlineSyntaxScanner.image`'s, and a link's +/// text is only rewritten when it holds an image (`[![b][img]][link]`), as +/// only then does the renderer parse it. The destination is `` with +/// `\ < > ( )` backslash-escaped, which `InlineSyntaxScanner` reads back as +/// exactly `url` (an escaped paren does not count for pairing either). +enum ReferenceLinks { + + /// `text` with every resolvable reference inlined, line by line (the + /// renderers take quote bodies line by line, so a reference never spans + /// lines). Unchanged when there are no definitions or no `[`. + static func inlined(_ text: String, definitions: [String: String]) -> String { + guard !definitions.isEmpty, text.contains("[") else { return text } + return text.split(separator: "\n", omittingEmptySubsequences: false) + .map { inlinedLine(String($0), definitions: definitions, linksDisabled: false) } + .joined(separator: "\n") + } + + private static func inlinedLine(_ line: String, definitions: [String: String], + linksDisabled: Bool) -> String { + guard line.contains("[") else { return line } + var text = line + text.makeContiguousUTF8() + let pairs = InlineSyntaxScanner.PairTable(text) + var out = "" + var remaining = text[...] + + while let first = remaining.first { + // Escapes first, as the renderer: `\[` never opens anything. + if first == "\\", remaining.count >= 2 { + let escaped = remaining[remaining.index(after: remaining.startIndex)] + if MarkdownTheme.escapableChars.contains(escaped) { + out += remaining.prefix(2) + remaining = remaining.dropFirst(2) + continue + } + } + if first == "`" { + let span = codeSpanLength(remaining) + out += remaining.prefix(span) + remaining = remaining.dropFirst(span) + continue + } + if let verbatim = unparsedSpanLength(remaining) { + out += remaining.prefix(verbatim) + remaining = remaining.dropFirst(verbatim) + continue + } + if remaining.hasPrefix("![") { + if let inline = InlineSyntaxScanner.image(atStartOf: remaining, references: [:], pairs: pairs) { + out += remaining[.. (String, Substring)? { + guard let closeBracket = InlineSyntaxScanner.closingBracket(in: remaining, pairs: pairs) else { return nil } + let afterBracket = remaining.index(after: closeBracket) + let linkText = String(remaining[remaining.index(after: remaining.startIndex).. (String, Substring) { + let destination = inlineDestination(url) + // The renderer parses a link's text only when it holds an image + // (and then with links disabled) — rewrite it under the same rule. + let text = linkText.contains("![") + ? inlinedLine(linkText, definitions: definitions, linksDisabled: true) : linkText + return ("[\(text)](\(destination))", rest) + } + + // Full / collapsed reference: [text][id], [text][] + if remaining[afterBracket...].hasPrefix("[") { + let afterSecondOpen = remaining.index(after: afterBracket) + if afterSecondOpen < remaining.endIndex, + let closeSecondBracket = remaining[afterSecondOpen...].firstIndex(of: "]") { + let refId = String(remaining[afterSecondOpen.. Int { + let run = remaining.prefix(while: { $0 == "`" }).count + guard run <= 2 else { return run } + let marker = String(repeating: "`", count: run) + let afterOpening = remaining.dropFirst(run) + guard let close = afterOpening.range(of: marker) else { return 1 } + if run == 2, close.upperBound < afterOpening.endIndex, afterOpening[close.upperBound] == "`" { + return 1 + } + return remaining.distance(from: remaining.startIndex, to: close.upperBound) + } + + /// `***…***` / `___…___` and `~~…~~`: the renderer draws their content as + /// plain text, so a reference inside stays as written there too. + private static func unparsedSpanLength(_ remaining: Substring) -> Int? { + for marker in ["***", "___", "~~"] where remaining.hasPrefix(marker) { + let after = remaining.dropFirst(marker.count) + guard let close = after.range(of: marker) else { return nil } + return remaining.distance(from: remaining.startIndex, to: close.upperBound) + } + return nil + } + + /// ``, which `InlineSyntaxScanner.linkDestination` reads back as + /// exactly `url`: the characters that would end the pointy form or pair + /// up with the link's own parentheses are backslash-escaped (the scanner + /// unescapes ASCII punctuation). A definition is one line, so no line break. + private static func inlineDestination(_ url: String) -> String { + var escaped = "<" + for character in url { + if "\\<>()".contains(character) { escaped.append("\\") } + escaped.append(character) + } + return escaped + ">" + } +} diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index d2c0a4d..1605b54 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -414,6 +414,77 @@ final class ParserTests: XCTestCase { XCTAssertEqual(rows[4], ["`p\\|q`", "c"]) } + // MARK: - Reference links in quotes, alerts and table cells + + /// Quotes, alerts and table cells keep their markdown and are rendered + /// later by renderers without the document's definitions, so the parser + /// inlines resolvable references (`ReferenceLinks`) — they used to show + /// as `[text][id]`. + func testReferenceLinksResolveInQuotesAlertsAndTableCells() { + let md = """ + > See [the docs][docs], ![logo][img] and [Docs]. + + > [!NOTE] + > Read [the guide][]. + + | A | B | + |---|---| + | [x][docs] | ![i][img] | + + [docs]: https://example.com/docs_(v2) + [img]: img/logo.png + [the guide]: https://example.com/guide + """ + let blocks = parse(md) + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) // no definitions + func links(_ attributed: AttributedString) -> [String] { + attributed.runs.compactMap { $0.link?.absoluteString } + } + + guard case .blockquote(let quote, 1) = blocks[0].content else { return XCTFail("quote") } + XCTAssertEqual(quote, "See [the docs](), ![logo]() " + + "and [Docs]().") + let shown = renderer.renderQuotedBody(quote) + XCTAssertEqual(String(shown.characters), "See the docs, [Image: logo] and Docs.") + XCTAssertEqual(links(shown).first, "https://example.com/docs_(v2)") + + guard case .alert(.note, let alert) = blocks[1].content else { return XCTFail("alert") } + XCTAssertEqual(links(renderer.renderQuotedBody(alert)), ["https://example.com/guide"]) + + guard case .table(_, let rows, _, .grid) = blocks[2].content else { return XCTFail("table") } + XCTAssertEqual(rows[0], ["[x]()", "![i]()"]) + XCTAssertEqual(links(renderer.renderInline(rows[0][0])), ["https://example.com/docs_(v2)"]) + } + + /// The rewrite must draw exactly what the paragraph renderer (which HAS the + /// definitions) draws for the same line: characters and links. + func testReferenceInliningMatchesTheDefinitionAwareRenderer() { + let theme = MarkdownTheme.cached(for: .light) + let definitions = ["docs": "https://example.com/docs_(v2)", "img": "img/My Pic.png", + "a b": "https://example.com/ab", "bad": "https://example.com/a\\\\c", "paren": "https://e.x/a)b(c"] + let withDefinitions = MarkdownRenderer(theme: theme, referenceDefinitions: definitions) + let plain = MarkdownRenderer(theme: theme) + let lines = [ + "[docs] and [Docs][] and [text][DOCS] and [A B]", + "`[docs]` in code, ``[docs]`` too, \\[docs] escaped, \\[x\\][docs]", + "***[docs]*** and ~~[docs]~~ stay plain, **[docs]** is a link", + "[![badge][img]][docs] and ![img] and ![alt][]", + "[inline](https://x.y/(a)) next to [docs]", + "[unknown][nope] [unknown] [docs][nope]", + "[bad] and [paren] escape what the <…> form needs", + "a ` lone backtick [docs] then ``` three ``` [docs]", + ] + for line in lines { + let inlined = ReferenceLinks.inlined(line, definitions: definitions) + let expected = withDefinitions.renderInline(line) + let actual = plain.renderInline(inlined) + XCTAssertEqual(String(actual.characters), String(expected.characters), line) + XCTAssertEqual(actual.runs.compactMap { $0.link?.absoluteString }, + expected.runs.compactMap { $0.link?.absoluteString }, line) + } + XCTAssertEqual(ReferenceLinks.inlined("[docs]\n[img]", definitions: [:]), "[docs]\n[img]") + } + // MARK: - Blockquotes func testBlockquoteNestingLevels() { From ff81e5b0899174b9b90c7e24958e363d49c1d7dd Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:56:46 +0100 Subject: [PATCH 035/105] fix(links): build link URLs the same on every macOS, keeping the fragment's # URL(string:) rejects a destination with a space or non-ASCII character on macOS 13 (14+ percent-encodes it), and the renderer's 13-only fallback encoded the whole string with .urlPathAllowed, the # included: a same-file README.md#Install Guide became a file named "README.md#Install Guide" and the jump failed silently. MarkdownRenderer.linkURL(for:) hands URL(string:) only strings that are already valid (RFC 3986 characters, % only as an escape), so the OS never encodes anything; otherwise it encodes the part before the first # and the fragment separately. Used for links and inline image links. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownRenderer.swift | 77 +++++++++++++++++-- .../QuickMDTests/SectionExtractorTests.swift | 37 +++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 72ef3b6..474039b 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -950,15 +950,82 @@ struct MarkdownRenderer: Sendable { attr.setDualForeground(theme.linkColor) attr.setDualUnderline() - if let parsedUrl = URL(string: url) { - attr.link = parsedUrl - } else if let encoded = url.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed), - let parsedUrl = URL(string: encoded) { + if let parsedUrl = Self.linkURL(for: url) { attr.link = parsedUrl } return (attr, remaining) } + /// The URL of a link destination as written, the SAME on every macOS. + /// + /// `URL(string:)` rejects a space or non-ASCII character on macOS 13 but + /// percent-encodes it on 14+, and the old fallback for 13 encoded the + /// whole string with `.urlPathAllowed` — the `#` too, so + /// `README.md#Install Guide` became a file named `README.md#Install Guide` + /// and the jump failed. Here a destination made only of characters a URL + /// may contain goes to `URL(string:)` as is (nothing for the OS to + /// encode); anything else is encoded by us, the part before the first `#` + /// and the fragment separately, so the `#` stays the fragment delimiter. + /// A `%` followed by two hex digits is an escape and kept; any other `%` + /// becomes `%25`. + static func linkURL(for destination: String) -> URL? { + let scalars = destination.unicodeScalars[...] + if !needsEncoding(scalars) { + return URL(string: destination) + } + guard let hash = scalars.firstIndex(of: "#") else { + return URL(string: percentEncoded(scalars)) + } + return URL(string: percentEncoded(scalars[.. Bool { + var index = scalars.startIndex + while index < scalars.endIndex { + if !urlCharacters.contains(scalars[index]), !isEscape(scalars, at: index) { return true } + index = scalars.index(after: index) + } + return false + } + + /// One part of a destination (no `#` delimiter inside): legal characters + /// and valid escapes kept, everything else — `#`, `[`, `]` too, which a + /// URL allows only in specific places — as UTF-8 `%XX`. + private static func percentEncoded(_ scalars: Scalars) -> String { + var out = "" + var index = scalars.startIndex + while index < scalars.endIndex { + let scalar = scalars[index] + let keep = scalar == "%" ? isEscape(scalars, at: index) + : scalar != "#" && scalar != "[" && scalar != "]" && urlCharacters.contains(scalar) + if keep { + out.unicodeScalars.append(scalar) + } else { + for byte in String(scalar).utf8 { out += String(format: "%%%02X", byte) } + } + index = scalars.index(after: index) + } + return out + } + /// Inline images stay text — an italic "[Image: alt]" placeholder (real /// pictures inside a paragraph would break the `.exact` text rows). Same /// grammar as standalone image blocks (`InlineSyntaxScanner`): inline @@ -975,7 +1042,7 @@ struct MarkdownRenderer: Sendable { attr.setDualForeground(theme.secondaryTextColor) // No link for an embedded `data:` image: clicking it would hand a // multi-megabyte URL to the "Open link?" alert. - if !InlineSyntaxScanner.isDataURI(image.url), let url = URL(string: image.url) { + if !InlineSyntaxScanner.isDataURI(image.url), let url = Self.linkURL(for: image.url) { attr.link = url } return (attr, remaining[image.end...]) diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 0491fad..6b8396f 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -248,4 +248,41 @@ final class SectionExtractorTests: XCTestCase { XCTAssertNil(fragment("file:///tmp/a.md#install")) XCTAssertNil(fragment("docs/install")) } + + /// A link destination becomes the same URL on every macOS: macOS 13's + /// `URL(string:)` rejects spaces / non-ASCII (14+ encodes them), and the + /// old 13 fallback encoded the `#` too, so `README.md#Install Guide` was a + /// file named "README.md#Install Guide". Every result must also parse + /// WITHOUT the OS encoding anything (strict parse on 14+), i.e. the + /// string handed to `URL(string:)` is already valid as macOS 13 sees it. + func testLinkURLKeepsTheFragmentDelimiterOnEveryMacOS() throws { + func link(_ destination: String) throws -> URL { + let url = try XCTUnwrap(MarkdownRenderer.linkURL(for: destination), destination) + if #available(macOS 14, *) { + XCTAssertNotNil(URL(string: url.absoluteString, encodingInvalidCharacters: false), + "\(destination) must not rely on the OS encoding it") + } + return url + } + + let sameFile = try link("README.md#Install Guide") + XCTAssertEqual(sameFile.relativeString, "README.md#Install%20Guide") + XCTAssertEqual(sameFile.path, "README.md") + XCTAssertEqual(sameFile.fragment(percentEncoded: false), "Install Guide") + + let other = try link("Zażółć notes.md#Sekcja Ł#2") + XCTAssertEqual(other.path, "Zażółć notes.md") + XCTAssertEqual(other.fragment(percentEncoded: false), "Sekcja Ł#2", "a later # belongs to the fragment") + + XCTAssertEqual(HeadingAnchors.inDocumentFragment(of: try link("#Install Guide")), "Install Guide") + + let web = try link("https://example.com/a b?q=x y#frag z") + XCTAssertEqual(web.host, "example.com") + XCTAssertEqual(web.fragment(percentEncoded: false), "frag z") + + XCTAssertEqual(try link("100% done.md").relativeString, "100%25%20done.md", "a bare % is not an escape") + XCTAssertEqual(try link("a%20b.md#x").relativeString, "a%20b.md#x", "valid URLs are untouched") + XCTAssertEqual(try link("https://example.com/docs_(v2)#top").absoluteString, + "https://example.com/docs_(v2)#top") + } } From f200a8f9f8a68a2b7b08fbb69c8b8674443be62f Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 20:59:57 +0100 Subject: [PATCH 036/105] =?UTF-8?q?fix(search):=20=E2=8C=98G=20centres=20t?= =?UTF-8?q?he=20match=20inside=20a=20block=20taller=20than=20the=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A search jump centred the BLOCK, so in a big table or a long code block the focused match could stay below the window, and a step to another match of the same block scrolled nothing at all. - RowGeometry.searchTarget: a block taller than the content area puts the focused match's middle at the centre; one that fits is centred as before. Where the match is comes from the block's text view (NSTextView.searchMatchRect, the matcher its highlight draws with). - A block whose view is not on display yet: the jump goes to the block and aims at the match when it lands (one forced layout, then up to two run-loop turns), unless the reader scrolled on meanwhile. - A step within the same block (DocumentSearch.stepScroll) sends a `.match` request: a tall block brings the new match to the centre, a block that fits stays where it is. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/DocumentSearch.swift | 31 +++++++ QuickMD/QuickMD/MarkdownView.swift | 11 ++- QuickMD/QuickMD/VerticalRhythm.swift | 22 +++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 85 +++++++++++++++++-- QuickMD/QuickMDTests/SearchTests.swift | 38 +++++++++ .../QuickMDTests/VerticalRhythmTests.swift | 41 +++++++++ 6 files changed, 219 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMD/DocumentSearch.swift b/QuickMD/QuickMD/DocumentSearch.swift index 10e3917..0b511d4 100644 --- a/QuickMD/QuickMD/DocumentSearch.swift +++ b/QuickMD/QuickMD/DocumentSearch.swift @@ -77,6 +77,23 @@ func applyNSSearchHighlight(in textView: NSTextView, term: String, focusedOccurr } } +extension NSTextView { + /// Where the `occurrence`-th match of `term` is in this view's text (view + /// coordinates) — the matcher its highlight draws with, so the index is + /// the block's focused occurrence. Nil past the last match. + func searchMatchRect(term: String, occurrence: Int) -> NSRect? { + guard let layoutManager, let textContainer, let storage = textStorage else { return nil } + let ranges = searchMatchRanges(in: storage.string, term: term) + guard occurrence >= 0, occurrence < ranges.count else { return nil } + let range = NSIntersectionRange(ranges[occurrence], NSRange(location: 0, length: storage.length)) + guard range.length > 0 else { return nil } + let glyphs = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) + layoutManager.ensureLayout(forGlyphRange: glyphs) + let rect = layoutManager.boundingRect(forGlyphRange: glyphs, in: textContainer) + return rect.offsetBy(dx: textContainerOrigin.x, dy: textContainerOrigin.y) + } +} + // MARK: - Document Search /// Pure search-match computation over parsed blocks. Free of any UI state so @@ -120,6 +137,20 @@ enum DocumentSearch { trigger == .termChanged && matchCount > 0 } + /// What a ⌘G / ⇧⌘G step that moved the focus from `oldBlockId` to + /// `newBlockId` scrolls: the new block into view, or — the same block — + /// the new match within it (which moves only a block taller than the + /// window). Nil without a focus. + enum StepScroll: Equatable { + case toBlock + case withinBlock + } + + static func stepScroll(from oldBlockId: String?, to newBlockId: String?) -> StepScroll? { + guard let newBlockId else { return nil } + return newBlockId == oldBlockId ? .withinBlock : .toBlock + } + /// The focused block and the occurrence within it that the block's /// highlight paints orange: match `index` is the k-th entry of its block /// in `matchBlockIds` (one entry per occurrence, reading order). Nil when diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index 103c2cc..d32322c 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -1432,9 +1432,16 @@ struct MarkdownView: View { currentMatchIndex = (currentMatchIndex - 1 + matchBlockIds.count) % matchBlockIds.count } updateFocusState() - // Only scroll when moving to a different block - if focusedBlockId != oldBlockId { + switch DocumentSearch.stepScroll(from: oldBlockId, to: focusedBlockId) { + case .toBlock: scrollTrigger += 1 + case .withinBlock: + // Another match of the same block: the list brings it to the eye + // when the block is taller than the window (a big table, a long + // code block); a block that fits stays where it is. + if let blockId = focusedBlockId { requestScroll(to: blockId, anchor: .match, animated: true) } + case nil: + break } } diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 56ea8f7..27654d5 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -218,6 +218,28 @@ enum RowGeometry { contentRect(rowRect: rowRect, spacingAbove: spacingAbove).midY - visibleHeight / 2 } + /// Where a search jump (⌘G) puts the content top. The block's middle at + /// the centre — unless the block is taller than the content area (a big + /// table, a long code block): then the focused MATCH's middle + /// (`matchMidY`, document y) at the centre, so every match in the block is + /// brought to the eye and stepping between them scrolls. `needsMatch`: a + /// tall block whose match is not known yet (its view is not on display) — + /// the jump goes to the block and aims at the match when it lands. + /// + /// `withinBlock`: a step between two matches of the same block. A block + /// that fits the content area is not moved for it (nil) — it was centred + /// when the search arrived in it. + static func searchTarget(rowRect: NSRect, spacingAbove: CGFloat, visibleHeight: CGFloat, + matchMidY: CGFloat?, withinBlock: Bool = false) -> (target: CGFloat, needsMatch: Bool)? { + let block = contentRect(rowRect: rowRect, spacingAbove: spacingAbove) + guard block.height > visibleHeight else { + if withinBlock { return nil } + return (centerTarget(rowRect: rowRect, spacingAbove: spacingAbove, visibleHeight: visibleHeight), false) + } + if let matchMidY { return (matchMidY - visibleHeight / 2, false) } + return (centerTarget(rowRect: rowRect, spacingAbove: spacingAbove, visibleHeight: visibleHeight), true) + } + /// The scroll anchor: how far below the row's BLOCK top the content top /// is. Negative when the content top is in the space above the block. /// Relative to the block, not to the row's edge, because that space diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 3d65d7e..6c493a9 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -94,8 +94,12 @@ struct VirtualBlockList: NSViewRepresentable { enum Anchor: Equatable { /// Row top at the top of the content area (ToC). case top - /// Row centre at the centre of the content area (search). + /// Row centre at the centre of the content area (search) — the focused + /// match's, in a block taller than that area. case center + /// ⌘G to another match of the SAME block: a block taller than the + /// content area brings that match to the centre; one that fits stays. + case match } /// One programmatic scroll. `token` is the trigger: the coordinator acts when @@ -400,6 +404,9 @@ struct VirtualBlockList: NSViewRepresentable { /// clip; the install puts the reader back on it. private var isSwitchingLayout = false private var layoutSwitchGeneration = 0 + /// A search jump into a block taller than the content area whose match + /// could not be located yet (`aimAtPendingMatch`): that block's id. + private var pendingMatchRow: String? #if DEBUG /// Where the last `setContentTop` left the content top, to tell a clip /// move this list made from one something else made (logged with the @@ -576,6 +583,7 @@ struct VirtualBlockList: NSViewRepresentable { self.anchor = nil isJumping = false isSwitchingLayout = false + pendingMatchRow = nil reportedBlocks = [] blocks = newBlocks @@ -778,6 +786,7 @@ struct VirtualBlockList: NSViewRepresentable { completion = { [weak self] in guard let self, self.jumpGeneration == jump else { return } self.isJumping = false + self.aimAtPendingMatch() } } else { isJumping = false @@ -932,26 +941,83 @@ struct VirtualBlockList: NSViewRepresentable { // it that reports its real height while the scroll is in flight, or // after it landed, moves the content top with it (`reportHeight`). let target: CGFloat + var needsMatch = false switch request.anchor { case .top: // The block's top, below the space above it: the content inset // is then the gap above it, as above the document's first block. target = blockTopY(row) - case .center: + case .center, .match: // The BLOCK's middle (not its row's, which includes the space // above it) at the centre of the CONTENT area (between the // insets), not of the clip view — with equal top/bottom insets - // these coincide. + // these coincide. A block taller than that area: its focused + // match (`RowGeometry.searchTarget`). let insets = scrollView.contentInsets let contentHeight = scrollView.contentView.bounds.height - insets.top - insets.bottom - target = RowGeometry.centerTarget(rowRect: tableView.rect(ofRow: row), - spacingAbove: table.spacing(row), - visibleHeight: contentHeight) + guard let search = RowGeometry.searchTarget(rowRect: tableView.rect(ofRow: row), + spacingAbove: table.spacing(row), + visibleHeight: contentHeight, + matchMidY: focusedMatchMidY(row: row), + withinBlock: request.anchor == .match) + else { return } + target = search.target + needsMatch = search.needsMatch } #if DEBUG - listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(request.anchor == .top ? "top" : "center", privacy: .public) target=\(target, format: .fixed(precision: 1))") + listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(String(describing: request.anchor), privacy: .public) target=\(target, format: .fixed(precision: 1))") #endif place(row: row, offset: target - blockTopY(row), animated: request.animated) + pendingMatchRow = needsMatch ? request.blockId : nil + if needsMatch, !isJumping { + // No glide to wait for (not animated, or already there). + DispatchQueue.main.async { [weak self] in self?.aimAtPendingMatch() } + } + } + + /// The focused match's middle (document y) in `row`, from its text view + /// — nil when the row is not the focused block or its view is not on + /// display. + private func focusedMatchMidY(row: Int) -> CGFloat? { + guard let tableView, row < blocks.count, !searchText.isEmpty, + let occurrence = focusedOccInBlock, focusedBlockId == blocks[row].id, + let textView = selection.displayedTextView(for: blocks[row].id, in: tableView), + let rect = textView.searchMatchRect(term: searchText, occurrence: occurrence) else { return nil } + return textView.convert(rect, to: tableView).midY + } + + /// A search jump into a block taller than the content area landed on + /// the block, whose view was not on display to ask where the match is: + /// now it is — glide on to the match. The row's cells are built by + /// the next layout pass; one is forced, and failing that it is asked + /// again on the next turns of the run loop. + private func aimAtPendingMatch(retries: Int = 2) { + guard let blockId = pendingMatchRow else { return } + // Not if the reader has scrolled on meanwhile. + guard let tableView, let scrollView, let row = rowForBlockId[blockId], + anchoredRow(contentTop: contentTopY()) == row else { + pendingMatchRow = nil + return + } + var matchMidY = focusedMatchMidY(row: row) + if matchMidY == nil { + tableView.layoutSubtreeIfNeeded() + matchMidY = focusedMatchMidY(row: row) + } + guard let matchMidY else { + if retries > 0 { + DispatchQueue.main.async { [weak self] in self?.aimAtPendingMatch(retries: retries - 1) } + } else { + pendingMatchRow = nil + } + return + } + pendingMatchRow = nil + let insets = scrollView.contentInsets + let contentHeight = scrollView.contentView.bounds.height - insets.top - insets.bottom + let target = matchMidY - contentHeight / 2 + guard abs(target - contentTopY()) > 0.5 else { return } + place(row: row, offset: target - blockTopY(row), animated: true) } // MARK: - Scroll offset arithmetic @@ -2291,6 +2357,11 @@ final class SelectionController: NSObject { return (SelectionPoint(row: row, offset: range.location), SelectionPoint(row: row, offset: NSMaxRange(range))) } + /// The text view of `blockId`'s row, if it is on display in `tableView`. + func displayedTextView(for blockId: String, in tableView: NSTableView) -> SelfSizingTextView? { + registeredTextView(for: blockId, in: tableView) + } + /// The registered view for `blockId` — if it is really on display in THIS /// table (a recycled view can outlive its registration by a layout pass). private func registeredTextView(for blockId: String, in tableView: NSTableView) -> SelfSizingTextView? { diff --git a/QuickMD/QuickMDTests/SearchTests.swift b/QuickMD/QuickMDTests/SearchTests.swift index de3241d..c40feed 100644 --- a/QuickMD/QuickMDTests/SearchTests.swift +++ b/QuickMD/QuickMDTests/SearchTests.swift @@ -603,4 +603,42 @@ extension SearchTests { XCTAssertEqual(results.matchBlockIds, [blocks[0].id, blocks[0].id]) XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "land").matchBlockIds, [blocks[0].id]) } + + // MARK: - ⌘G within a block + + /// A step to another match of the SAME block used to scroll nothing — in + /// a block taller than the window the next match could be off screen. + func testAStepToAnotherMatchOfTheSameBlockScrollsWithinIt() { + XCTAssertEqual(DocumentSearch.stepScroll(from: "a", to: "a"), .withinBlock) + XCTAssertEqual(DocumentSearch.stepScroll(from: "a", to: "b"), .toBlock) + XCTAssertEqual(DocumentSearch.stepScroll(from: nil, to: "b"), .toBlock) + XCTAssertNil(DocumentSearch.stepScroll(from: "a", to: nil)) + } + + /// Where the list centres a match: the rect of the occurrence the + /// highlight paints orange, in the text view's coordinates (its container + /// inset included). + func testTheMatchRectIsWhereTheFocusedHighlightIs() throws { + let lines = (0..<120).map { $0 == 3 || $0 == 100 ? "row \($0) needle here" : "row \($0) filler text" } + let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 400, height: 10)) + textView.textContainerInset = NSSize(width: 7, height: 11) + textView.isVerticallyResizable = true + textView.string = lines.joined(separator: "\n") + textView.sizeToFit() + + let first = try XCTUnwrap(textView.searchMatchRect(term: "NEEDLE", occurrence: 0)) + let second = try XCTUnwrap(textView.searchMatchRect(term: "NEEDLE", occurrence: 1)) + XCTAssertNil(textView.searchMatchRect(term: "needle", occurrence: 2)) + XCTAssertNil(textView.searchMatchRect(term: "absent", occurrence: 0)) + + let ranges = searchMatchRanges(in: textView.string, term: "needle") + for (rect, range) in zip([first, second], ranges) { + XCTAssertGreaterThan(rect.width, 0) + let index = textView.characterIndexForInsertion(at: NSPoint(x: rect.midX, y: rect.midY)) + XCTAssertTrue(NSLocationInRange(index, NSRange(location: range.location, length: range.length + 1)), + "\(index) not in \(range)") + } + XCTAssertGreaterThan(second.midY - first.midY, 90 * 12) + XCTAssertGreaterThanOrEqual(first.minY, 11) + } } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 5729f4e..a2c661a 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -464,6 +464,47 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertFalse(RowGeometry.isInUpperHalf(y: 140, rowRect: row, spacingAbove: 20)) } + /// ⌘G into a block taller than the window (a big table, a long code + /// block) used to centre the BLOCK: a match near its end stayed below the + /// window. The match is what is centred now; a block that fits is still + /// centred as a whole, and a step within it does not move it. + func testASearchJumpIntoABlockTallerThanTheWindowCentresTheMatch() throws { + let visible: CGFloat = 800 + let tall = NSRect(x: 0, y: 1000, width: 600, height: 3016) // block 1016...4016 + let matchMidY: CGFloat = 3700 + + let jump = try XCTUnwrap(RowGeometry.searchTarget(rowRect: tall, spacingAbove: 16, visibleHeight: visible, + matchMidY: matchMidY)) + XCTAssertEqual(jump.target, 3300) + XCTAssertFalse(jump.needsMatch) + XCTAssertTrue((jump.target...(jump.target + visible)).contains(matchMidY)) + // The old target (the block's middle) left that match off screen. + let blockCentre = RowGeometry.centerTarget(rowRect: tall, spacingAbove: 16, visibleHeight: visible) + XCTAssertFalse((blockCentre...(blockCentre + visible)).contains(matchMidY)) + + // Stepping to another match of the same tall block moves to it. + let step = try XCTUnwrap(RowGeometry.searchTarget(rowRect: tall, spacingAbove: 16, visibleHeight: visible, + matchMidY: 1900, withinBlock: true)) + XCTAssertEqual(step.target, 1500) + + // The match not known yet (the block's view is not on display): the + // block first, then the match when it lands. + let blind = try XCTUnwrap(RowGeometry.searchTarget(rowRect: tall, spacingAbove: 16, visibleHeight: visible, + matchMidY: nil)) + XCTAssertEqual(blind.target, blockCentre) + XCTAssertTrue(blind.needsMatch) + + // A block that fits: centred as before, the match ignored; a step + // within it does not scroll. + let short = NSRect(x: 0, y: 100, width: 500, height: 60) + let fits = try XCTUnwrap(RowGeometry.searchTarget(rowRect: short, spacingAbove: 20, visibleHeight: 400, + matchMidY: 150)) + XCTAssertEqual(fits.target, -60) + XCTAssertFalse(fits.needsMatch) + XCTAssertNil(RowGeometry.searchTarget(rowRect: short, spacingAbove: 20, visibleHeight: 400, + matchMidY: 150, withinBlock: true)) + } + // MARK: - Scroll anchor across layouts (Reading Mode, width, zoom, jumps) /// `VirtualBlockList.Coordinator`'s scrolling over a model column, step by From 732c4ffb690fba774aea6a1222491e75a70658e6 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:00:05 +0100 Subject: [PATCH 037/105] fix(links): other.md#section scrolls the other document to its heading A link to another Markdown file opened it through NSWorkspace and dropped the fragment, so the document always opened at its top. The fragment is now parked under the file's path (CrossDocumentAnchors, 30 s lifetime) and followed like an in-document link by the window that shows the file: a new window once its first parse has a height table (a scroll request before that finds no row and is dropped), an already open window at once through a notification. A freshly opened window jumps without animation. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 6 ++ QuickMD/QuickMD/CrossDocumentAnchors.swift | 63 +++++++++++++++++++ QuickMD/QuickMD/MarkdownView.swift | 37 +++++++++-- .../QuickMDTests/SectionExtractorTests.swift | 30 +++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 QuickMD/QuickMD/CrossDocumentAnchors.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index abf3d2a..ee59e7a 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000002A0 /* VerticalRhythm.swift */; }; A1B2C3D401000301 /* ReferenceLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000301 /* ReferenceLinks.swift */; }; A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000301 /* ReferenceLinks.swift */; }; + A1B2C3D401000303 /* CrossDocumentAnchors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000303 /* CrossDocumentAnchors.swift */; }; + A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000303 /* CrossDocumentAnchors.swift */; }; A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C1 /* TableSelectionTests.swift */; }; A1B2C3D40100000E /* ImageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000012 /* ImageBlockView.swift */; }; A1B2C3D40100000F /* BlockquoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000013 /* BlockquoteView.swift */; }; @@ -238,6 +240,7 @@ A1B2C3D4000001C0 /* SelectionRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRows.swift; sourceTree = ""; }; A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; A1B2C3D400000301 /* ReferenceLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceLinks.swift; sourceTree = ""; }; + A1B2C3D400000303 /* CrossDocumentAnchors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDocumentAnchors.swift; sourceTree = ""; }; A1B2C3D4000001C1 /* TableSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSelectionTests.swift; sourceTree = ""; }; A1B2C3D400000012 /* ImageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlockView.swift; sourceTree = ""; }; A1B2C3D400000013 /* BlockquoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockquoteView.swift; sourceTree = ""; }; @@ -391,6 +394,7 @@ A1B2C3D4000001C0 /* SelectionRows.swift */, A1B2C3D4000002A0 /* VerticalRhythm.swift */, A1B2C3D400000301 /* ReferenceLinks.swift */, + A1B2C3D400000303 /* CrossDocumentAnchors.swift */, A1B2C3D400000110 /* DocumentReadingPosition.swift */, A1B2C3D400000120 /* DocumentFileFormat.swift */, A1B2C3D400000121 /* DocumentFileWriter.swift */, @@ -690,6 +694,7 @@ A1B2C3D4010001C0 /* SelectionRows.swift in Sources */, A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */, A1B2C3D401000301 /* ReferenceLinks.swift in Sources */, + A1B2C3D401000303 /* CrossDocumentAnchors.swift in Sources */, A1B2C3D401000110 /* DocumentReadingPosition.swift in Sources */, A1B2C3D401000120 /* DocumentFileFormat.swift in Sources */, A1B2C3D401000122 /* DocumentFileWriter.swift in Sources */, @@ -745,6 +750,7 @@ A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */, A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */, A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */, + A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */, A1B2C3D4010000C2 /* DocumentSelectionTests.swift in Sources */, A1B2C3D401000111 /* DocumentReadingPosition.swift in Sources (Tests) */, A1B2C3D401000112 /* ExternalEditorLineTests.swift in Sources */, diff --git a/QuickMD/QuickMD/CrossDocumentAnchors.swift b/QuickMD/QuickMD/CrossDocumentAnchors.swift new file mode 100644 index 0000000..19e2edc --- /dev/null +++ b/QuickMD/QuickMD/CrossDocumentAnchors.swift @@ -0,0 +1,63 @@ +import Foundation + +// MARK: - `other.md#section` links + +/// Hands the fragment of a link to ANOTHER Markdown document to the window +/// that shows that document, so it scrolls to the heading the way an +/// in-document `#section` link does (before, the fragment was dropped and the +/// other document opened at its top). +/// +/// The link's view cannot reach the target: the document opens through +/// `NSWorkspace` (DocumentGroup creates the window, or brings an already open +/// one forward). So the fragment is parked here under the file's path: +/// - a NEW window takes it once its first parse is laid out (a scroll request +/// before the list has its height table finds no row and is dropped); +/// - an already open window hears `requested` and takes it at once. +/// A request is only honoured for `lifetime` seconds, so a link whose file +/// failed to open cannot move a window the user opens much later. +/// +/// Locked rather than `@MainActor`: the views call it from helpers, which are +/// nonisolated on the older SDK CI builds with (constraints.md). +final class CrossDocumentAnchors: @unchecked Sendable { + static let shared = CrossDocumentAnchors() + + /// Posted on the main thread by `request`; `object` is the target's `key`. + static let requested = Notification.Name("QuickMDCrossDocumentAnchorRequested") + + /// Long enough for a large document's open + parse + first layout. + static let lifetime: TimeInterval = 30 + + private let lock = NSLock() + private var pending: [String: (fragment: String, date: Date)] = [:] + + /// The identity of a document file: the path as `handleLinkActivation` + /// compares documents (`standardizedFileURL`). + static func key(for fileURL: URL) -> String { fileURL.standardizedFileURL.path } + + /// The fragment of a link to another document, percent-decoded (`#Install + /// Guide` arrives as `Install%20Guide`); nil when the link has none. + static func fragment(of link: URL) -> String? { + link.fragment(percentEncoded: false) + } + + /// Parks `fragment` for `fileURL` (replacing an older one) and tells an + /// already open window. + func request(_ fragment: String, for fileURL: URL, now: Date = Date()) { + let key = Self.key(for: fileURL) + lock.lock() + pending[key] = (fragment, now) + lock.unlock() + NotificationCenter.default.post(name: Self.requested, object: key) + } + + /// The fragment parked for `fileURL`, removed — nil when there is none or + /// it is older than `lifetime`. + func take(for fileURL: URL?, now: Date = Date()) -> String? { + guard let fileURL else { return nil } + lock.lock() + let entry = pending.removeValue(forKey: Self.key(for: fileURL)) + lock.unlock() + guard let entry, now.timeIntervalSince(entry.date) <= Self.lifetime else { return nil } + return entry.fragment + } +} diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index 732a384..c5671f4 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -728,6 +728,7 @@ struct MarkdownView: View { if let target = landing.blocksInstalled(blocks) { requestScroll(to: target, anchor: .top, animated: false) } + followCrossDocumentAnchor() // The find bar's matches index the PREVIOUS parse's blocks: count // again over these (and the measured strings just installed). rerunSearchAfterReparse() @@ -776,7 +777,15 @@ struct MarkdownView: View { fontScale: scale, contentWidth: width, math: .swiftMath, reusing: reusableStrings) - if firstTableForParse { rerunSearchAfterReparse() } + if firstTableForParse { + rerunSearchAfterReparse() + followCrossDocumentAnchor() + } + } + .onReceive(NotificationCenter.default.publisher(for: CrossDocumentAnchors.requested)) { note in + // `other.md#section` clicked in another window while this one shows other.md. + guard let documentURL, note.object as? String == CrossDocumentAnchors.key(for: documentURL) else { return } + followCrossDocumentAnchor() } .onChange(of: searchText) { newValue in searchDebounce?.cancel() @@ -1069,14 +1078,26 @@ struct MarkdownView: View { /// A ToC click. While editing, the EDITOR scrolls to the heading's source /// line (S-D13) — best effort until the next save, since the headings come /// from the last saved parse; the hidden list stays where the reader was. - private func selectHeading(_ targetId: String) { + private func selectHeading(_ targetId: String, animated: Bool = true) { if editSession.isActive { if let entry = headings.first(where: { $0.id == targetId }) { editSession.editor.scroll(toLine: entry.sourceLine) } return } - requestScroll(to: targetId, anchor: .top, animated: true) + requestScroll(to: targetId, anchor: .top, animated: animated) + } + + /// The fragment of an `other.md#section` link that opened (or brought + /// forward) this document, followed like an in-document link — once the + /// list has a height table for these blocks: a scroll request before that + /// finds no row and is dropped, so a new window waits for its first + /// layout (parse transaction or the first heights task). A freshly opened + /// window jumps without animation. + private func followCrossDocumentAnchor() { + guard !cachedBlocks.isEmpty, measured.table.count == cachedBlocks.count, + let fragment = CrossDocumentAnchors.shared.take(for: documentURL) else { return } + followAnchor(fragment, animated: false) } // MARK: - Block Rendering @@ -1558,6 +1579,10 @@ struct MarkdownView: View { // Open the resolved file URL let ext = finalURL.pathExtension.lowercased() if ext == "md" || ext == "markdown" || ext == "mdown" || ext == "mkd" { + // `other.md#section`: the window that shows other.md scrolls there. + if let fragment = CrossDocumentAnchors.fragment(of: url) { + CrossDocumentAnchors.shared.request(fragment, for: finalURL) + } // Force open with our own app let config = NSWorkspace.OpenConfiguration() config.activates = true @@ -1571,14 +1596,14 @@ struct MarkdownView: View { /// Scrolls to the heading an in-document link names, as a ToC click does /// (`HeadingAnchors.target`: `#` and an unclaimed `#top` are the top); an /// id no heading has beeps. - private func followAnchor(_ fragment: String) { + private func followAnchor(_ fragment: String, animated: Bool = true) { let renderer = MarkdownRenderer(theme: theme, fontScale: CGFloat(renderedFontScale)) let headings = HeadingAnchors.headings(of: cachedBlocks, renderer: renderer) switch HeadingAnchors.target(forFragment: fragment, in: headings) { case .heading(let id): - selectHeading(id) + selectHeading(id, animated: animated) case .top where !cachedBlocks.isEmpty: - requestScroll(to: cachedBlocks[0].id, anchor: .top, animated: true) + requestScroll(to: cachedBlocks[0].id, anchor: .top, animated: animated) case .top, .none: NSSound.beep() } diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 6b8396f..f6835aa 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -249,6 +249,36 @@ final class SectionExtractorTests: XCTestCase { XCTAssertNil(fragment("docs/install")) } + /// `other.md#section`: the fragment is parked for the other document's + /// window (which takes it once laid out, or at once when already open). + func testCrossDocumentAnchorIsHandedToTheOtherDocument() throws { + let anchors = CrossDocumentAnchors() + let link = try XCTUnwrap(MarkdownRenderer.linkURL(for: "other.md#Install Guide")) + let fragment = try XCTUnwrap(CrossDocumentAnchors.fragment(of: link)) + XCTAssertEqual(fragment, "Install Guide") + XCTAssertNil(CrossDocumentAnchors.fragment(of: try XCTUnwrap(MarkdownRenderer.linkURL(for: "other.md")))) + XCTAssertEqual(CrossDocumentAnchors.fragment(of: try XCTUnwrap(MarkdownRenderer.linkURL(for: "other.md#"))), "", + "an empty fragment is the top, as in-document") + + let target = URL(fileURLWithPath: "/docs/guide/other.md") + let posted = expectation(forNotification: CrossDocumentAnchors.requested, object: nil) { note in + note.object as? String == "/docs/guide/other.md" + } + let now = Date() + anchors.request(fragment, for: URL(fileURLWithPath: "/docs/guide/./sub/../other.md"), now: now) + wait(for: [posted], timeout: 1) + + XCTAssertNil(anchors.take(for: URL(fileURLWithPath: "/docs/guide/another.md"), now: now)) + XCTAssertNil(anchors.take(for: nil, now: now)) + XCTAssertEqual(anchors.take(for: target, now: now.addingTimeInterval(2)), "Install Guide") + XCTAssertNil(anchors.take(for: target, now: now), "taken once") + + anchors.request("intro", for: target, now: now) + XCTAssertNil(anchors.take(for: target, now: now.addingTimeInterval(CrossDocumentAnchors.lifetime + 1)), + "a stale request never moves a window opened much later") + XCTAssertNil(anchors.take(for: target, now: now), "and it is gone") + } + /// A link destination becomes the same URL on every macOS: macOS 13's /// `URL(string:)` rejects spaces / non-ASCII (14+ encodes them), and the /// old 13 fallback encoded the `#` too, so `README.md#Install Guide` was a From 299d408a9c5fe091b282303e9872951e156b4521 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:02:52 +0100 Subject: [PATCH 038/105] fix(watcher): an unreadable document says it can't be read, not that it is gone FileWatcher reported EACCES / EPERM like ENOENT, so a chmod 000 document (or a sandbox denial) showed "File no longer exists". It now fires onFileUnreadable for a file that is there but cannot be opened (at start, after an atomic replace, or when a missing file comes back locked); still no polling for that case. The banner reads "File can't be read at " (FileWatcher.Problem); "Close Tab" stays. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/FileWatchManager.swift | 47 +++++++++++++++++---- QuickMD/QuickMD/MarkdownView.swift | 9 +++- QuickMD/QuickMDTests/FileWatcherTests.swift | 36 +++++++++++++++- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/QuickMD/QuickMD/FileWatchManager.swift b/QuickMD/QuickMD/FileWatchManager.swift index d59015d..ad26fbe 100644 --- a/QuickMD/QuickMD/FileWatchManager.swift +++ b/QuickMD/QuickMD/FileWatchManager.swift @@ -17,9 +17,11 @@ import Foundation /// (debounced) change. `onFileMissing` fires once per disappearance, never per /// poll. Polling happens only while the path is ABSENT (ENOENT / ENOTDIR): a /// path that exists but cannot be opened or seen (EACCES / EPERM — a sandbox -/// or permission denial) would fail the same way every second for the life of -/// the tab, so the watcher then stays missing without polling, as it always -/// did before polling existed. +/// or permission denial, `chmod 000`) would fail the same way every second for +/// the life of the tab, so the watcher then reports `onFileUnreadable` instead +/// and does not poll (⌘R re-arms it). Until 1.14.4 that case was reported as +/// missing, and the window said "File no longer exists" about a file that is +/// right there. /// /// All callbacks fire on the main queue (the DispatchSource and the debounce /// both target `.main`). Changes are debounced 250 ms so an editor that @@ -39,6 +41,11 @@ final class FileWatcher { /// follows. var onFileMissing: (() -> Void)? + /// Fired when the file is at its path but cannot be opened (EACCES / + /// EPERM) — at start, after an atomic replace, or when a missing file + /// comes back without read permission. Not polled. + var onFileUnreadable: (() -> Void)? + private var source: DispatchSourceFileSystemObject? private var url: URL? private var debounce: DispatchWorkItem? @@ -94,10 +101,15 @@ final class FileWatcher { error == ENOENT || error == ENOTDIR } - /// Reports the disappearance ONCE; the poll that follows is silent. + /// Reports the disappearance ONCE; the poll that follows is silent. A file + /// that is there but off-limits is reported as unreadable, not missing. private func fileWentMissing(_ error: Int32) { + guard Self.isAbsence(error) else { + onFileUnreadable?() + return + } onFileMissing?() - if Self.isAbsence(error) { startMissingPoll() } + startMissingPoll() } private func startMissingPoll() { @@ -122,8 +134,11 @@ final class FileWatcher { guard let url else { return stopMissingPoll() } if access(url.path, F_OK) != 0 { // Still absent: keep waiting. Not visible for another reason - // (a parent folder became unreadable): give up, stay missing. - if !Self.isAbsence(errno) { stopMissingPoll() } + // (a parent folder became unreadable): give up, say so. + if !Self.isAbsence(errno) { + stopMissingPoll() + onFileUnreadable?() + } return } let error = arm() @@ -132,8 +147,9 @@ final class FileWatcher { scheduleChange() } else if !Self.isAbsence(error) { // Back, but cannot be opened (EACCES / EPERM): retrying every - // second would never succeed — stay missing. + // second would never succeed — stop, and say it is unreadable. stopMissingPoll() + onFileUnreadable?() } // Absent again between `access` and `open`: already reported, keep polling. } @@ -156,6 +172,21 @@ final class FileWatcher { } } + // MARK: - What the window says + + /// The banner a document window shows for a file the watcher lost. + enum Problem: Equatable { + case missing, unreadable + + func bannerText(path: String?) -> String { + let location = path ?? "this location" + switch self { + case .missing: return "File no longer exists at \(location)" + case .unreadable: return "File can\u{2019}t be read at \(location)" + } + } + } + private func scheduleChange() { debounce?.cancel() let work = DispatchWorkItem { [weak self] in diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index c5671f4..ed60ecd 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -30,6 +30,8 @@ struct MarkdownView: View { @State private var fileWatcher: FileWatcher? /// The watched file disappeared from its path (moved/deleted). @State private var fileMissing = false + /// What the `fileMissing` banner says (gone, or there but unreadable). + @State private var fileProblem: FileWatcher.Problem = .missing @State private var cachedBlocks: [MarkdownBlock] = [] /// Incremented on every successful parse. Text blocks use it to invalidate /// their NSAttributedString caches — a font-size-only change leaves the @@ -366,7 +368,7 @@ struct MarkdownView: View { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.yellow) - Text("File no longer exists at \(documentURL?.path ?? "this location")") + Text(fileProblem.bannerText(path: documentURL?.path)) .font(.system(size: 12)) .lineLimit(1) .truncationMode(.middle) @@ -931,6 +933,11 @@ struct MarkdownView: View { let watcher = FileWatcher() watcher.onChange = { reloadFromDisk() } watcher.onFileMissing = { + fileProblem = .missing + withAnimation(.easeInOut(duration: 0.2)) { fileMissing = true } + } + watcher.onFileUnreadable = { + fileProblem = .unreadable withAnimation(.easeInOut(duration: 0.2)) { fileMissing = true } } watcher.start(watching: url) diff --git a/QuickMD/QuickMDTests/FileWatcherTests.swift b/QuickMD/QuickMDTests/FileWatcherTests.swift index 0e194ce..f5e6296 100644 --- a/QuickMD/QuickMDTests/FileWatcherTests.swift +++ b/QuickMD/QuickMDTests/FileWatcherTests.swift @@ -169,10 +169,12 @@ final class FileWatcherTests: XCTestCase { } /// The file comes back but cannot be opened (mode 000 → EACCES): the poll - /// stops instead of failing every second, and the watcher stays missing — + /// stops instead of failing every second and reports it unreadable, once — /// restoring the permissions later is NOT noticed (pre-poll behaviour). func testPermissionDeniedAfterReturnStopsThePoll() throws { var missingCount = 0 + var unreadableCount = 0 + watcher.onFileUnreadable = { unreadableCount += 1 } let missing = expectation(description: "onFileMissing after delete") watcher.onFileMissing = { missingCount += 1 @@ -197,6 +199,38 @@ final class FileWatcherTests: XCTestCase { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { settle.fulfill() } wait(for: [settle], timeout: 3) XCTAssertEqual(missingCount, 1) + XCTAssertEqual(unreadableCount, 1, "back, but off-limits: unreadable, not missing") + } + + /// `chmod 000` on the open document (or a sandbox denial): the file is + /// there, so the window must not say it no longer exists. + func testUnreadableFileIsNotReportedAsMissing() throws { + watcher.onFileMissing = { XCTFail("an unreadable file is not a missing one") } + watcher.onChange = { XCTFail("nothing changed") } + var unreadableCount = 0 + watcher.onFileUnreadable = { unreadableCount += 1 } + XCTAssertEqual(chmod(fileURL.path, 0), 0) + defer { chmod(fileURL.path, 0o644) } + + watcher.start(watching: fileURL) + XCTAssertEqual(unreadableCount, 1, "reported at once, synchronously") + + // An atomic replace by an unreadable file: the same answer. + XCTAssertEqual(chmod(fileURL.path, 0o644), 0) + watcher.start(watching: fileURL) + XCTAssertEqual(unreadableCount, 1, "armed") + let replacement = tempDir.appendingPathComponent("replacement.md") + try "locked".write(to: replacement, atomically: false, encoding: .utf8) + XCTAssertEqual(chmod(replacement.path, 0), 0) + XCTAssertEqual(rename(replacement.path, fileURL.path), 0) + let settle = expectation(description: "rename handled, no poll") + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { settle.fulfill() } + wait(for: [settle], timeout: 3) + XCTAssertEqual(unreadableCount, 2) + + XCTAssertEqual(FileWatcher.Problem.missing.bannerText(path: "/a/b.md"), "File no longer exists at /a/b.md") + XCTAssertEqual(FileWatcher.Problem.unreadable.bannerText(path: "/a/b.md"), "File can\u{2019}t be read at /a/b.md") + XCTAssertEqual(FileWatcher.Problem.unreadable.bannerText(path: nil), "File can\u{2019}t be read at this location") } func testStopWhileMissingCancelsThePoll() throws { From 47de67b19c16f807d976312ee0fc5f202ee24fa5 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:05:24 +0100 Subject: [PATCH 039/105] fix(lists): an ordered list keeps one text edge across rows and nested blocks Ordered items hang under the widest marker of their list, so the text of every item starts at one x. 1.14.4 took the widest marker per text ROW: a list cut at the 30-line cap had two text edges (32.2 pt above the cut, 38.3 pt below it in the test), and the steps of a list whose items hold a code block, each a row of its own, hung under their own numbers: eleven steps, eleven edges between 29.8 and 38.3 pt. The branch already computed the hangs per text run (one buffer between two non-text blocks), which fixed the cut. Now the text rows are rendered once the whole document is parsed and the hangs are worked out over the document: the rows' lines in order, every other block standing in as one line with its indentation, so a code block or picture indented under an item continues the list and one that is not ends it. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 96 +++++++++++++++---- .../QuickMDTests/VerticalRhythmTests.swift | 55 +++++++++++ 2 files changed, 130 insertions(+), 21 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 0bacb88..0f29947 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -116,6 +116,10 @@ struct MarkdownBlockParser: Sendable { // Each buffered line keeps the index it had in `allLines` so a flushed // chunk can report the ORIGINAL first line, not its position in `lines`. var textBuffer: [(line: String, originalIndex: Int)] = [] + // Text rows are rendered once the whole document is parsed + // (`renderPendingRows`): an ordered list's items hang under the + // LIST's widest marker, and a list goes on past a block nested in it. + var pendingRows: [PendingTextRow] = [] while i < lines.count { let line = lines[i] @@ -127,7 +131,7 @@ struct MarkdownBlockParser: Sendable { // Fenced code block — check BEFORE math to protect $$ inside code blocks if trimmedLine.hasPrefix("```") || trimmedLine.hasPrefix("~~~") { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) // Store the fence characters (just the backticks/tildes prefix) guard let fenceChar = trimmedLine.first else { i += 1; continue } @@ -169,7 +173,7 @@ struct MarkdownBlockParser: Sendable { // Display math block $$...$$ — after code block check to protect $$ inside code if trimmedLine.hasPrefix("$$") { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) let afterDollar = String(trimmedLine.dropFirst(2)).trimmingCharacters(in: .whitespaces) @@ -212,7 +216,7 @@ struct MarkdownBlockParser: Sendable { if let match = Self.headerRegex.firstMatch(in: line, range: nsRange), let hashRange = Range(match.range(at: 1), in: line), let contentRange = Range(match.range(at: 2), in: line) { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) let level = min(max(line[hashRange].count, 1), 6) let title = Self.withoutClosingSequence( String(line[contentRange]).trimmingCharacters(in: .whitespaces)) @@ -226,7 +230,7 @@ struct MarkdownBlockParser: Sendable { // becomes its own `.image` block, all on this line's `sourceLine` // (`![a](x.png) ![b](y.png)` is two pictures, not one bogus url). if let images = InlineSyntaxScanner.standaloneImages(in: line[...], references: referenceDefinitions) { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) for image in images { blocks.append(.image(index: blockIndex, url: image.url, alt: image.alt, width: nil, sourceLine: sourceLine)) blockIndex += 1 @@ -256,7 +260,7 @@ struct MarkdownBlockParser: Sendable { textBuffer[last].line += " " } } else { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) for tag in html.images { blocks.append(.image(index: blockIndex, url: tag.url, alt: tag.alt, width: tag.width, sourceLine: lineMap[i + tag.lineOffset])) @@ -273,7 +277,7 @@ struct MarkdownBlockParser: Sendable { let nextLine = lines[i + 1].trimmingCharacters(in: .whitespaces) if nextLine.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) // Setext: the TEXT line owns the heading, not the `===` underline. blocks.append(.heading(index: blockIndex, level: 1, title: trimmed, sourceLine: sourceLine)) blockIndex += 1 @@ -288,7 +292,7 @@ struct MarkdownBlockParser: Sendable { // separator can't match setextH2Pattern anyway, so the guard // added nothing. (Caught by ParserTests.testSetextHeadings.) if nextLine.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) blocks.append(.heading(index: blockIndex, level: 2, title: trimmed, sourceLine: sourceLine)) blockIndex += 1 i += 2 @@ -299,7 +303,7 @@ struct MarkdownBlockParser: Sendable { // Table detection if line.filter({ $0 == "|" }).count >= 2 && !trimmed.isEmpty { if !isTableSeparator(trimmed) && i + 1 < lines.count && isTableSeparator(lines[i + 1]) { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) var alignments = parseTableAlignments(lines[i + 1]) // The separator row is authoritative for column count: a header row @@ -330,7 +334,7 @@ struct MarkdownBlockParser: Sendable { // Blockquote detection - group consecutive > lines by nesting level if trimmed.hasPrefix(">") { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) // A later nesting group of this run continues the quote row // before it (`MarkdownBlock.joinsPrevious`): `> a` then `>> b` // is one quote on the page, drawn as two rows. @@ -418,7 +422,7 @@ struct MarkdownBlockParser: Sendable { // term line is NOT part of it (PHP Markdown Extra reads // "para line\n: def" as term + definition). var preceding = Array(textBuffer[..= 0 && block.sourceLine < allLines.count ? allLines[block.sourceLine] : "" + lines.append(String(first.prefix(while: { $0 == " " || $0 == "\t" })) + Self.blockStandIn) + } + } + let hangs = renderer.widestOrderedPrefixes(lines) + for (row, start) in zip(pending, starts) { + blocks[row.position] = .text(index: row.index, + renderer.render(joined: row.lines, + hangs: Array(hangs[start..<(start + row.lines.count)])), + sourceLine: row.sourceLine, joinsPrevious: row.joinsPrevious) + } + } } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index a2c661a..d59d2db 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -417,6 +417,61 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(headIndents.first, tightIndent) } + /// The paragraph head indent of every ordered item in the text rows, in + /// reading order — where each item's text starts. + private func orderedItemIndents(_ blocks: [MarkdownBlock]) throws -> [CGFloat] { + var indents: [CGFloat] = [] + for block in blocks { + guard case .text(let attributed) = block.content else { continue } + let string = try NSAttributedString(attributed, including: \.appKit) + let text = string.string as NSString + text.enumerateSubstrings(in: NSRange(location: 0, length: text.length), options: .byParagraphs) { + paragraph, range, _, _ in + guard let paragraph, paragraph.range(of: #"^\s*\d+\.\s"#, options: .regularExpression) != nil + else { return } + let style = string.attribute(.paragraphStyle, at: range.location, effectiveRange: nil) + indents.append((style as? NSParagraphStyle)?.headIndent ?? -1) + } + } + return indents + } + + /// A list longer than a text row may be (`textChunkLineLimit`) is cut into + /// rows; 1.14.4 measured the widest marker per ROW, so items 1–8 above + /// the cut hung under "8. " and items 9–12 below it under "12. " — two + /// text edges in one list. The widest marker is the LIST's. + func testAnOrderedListCutIntoRowsKeepsOneTextEdge() throws { + let items = (1...12).map { n in + "\(n). item \(n)\n - sub a\n - sub b\n - sub c" + } + let blocks = parse(items.joined(separator: "\n")) + XCTAssertEqual(blocks.count, 2) + XCTAssertEqual(blocks.map(\.joinsPrevious), [false, true]) + let indents = try orderedItemIndents(blocks) + XCTAssertEqual(indents.count, 12) + XCTAssertEqual(Set(indents).count, 1, "\(indents)") + } + + /// The steps of a list whose items each hold a code block: every step is + /// a row of its own between two code blocks, and 1.14.4 hung each under + /// its OWN number — the digits are proportional, so "1. " to "9. " each + /// started the text at a different x and "10. " further right still. + func testOrderedStepsWithCodeBlocksKeepOneTextEdge() throws { + let steps = (1...11).map { "\($0). Step \($0)\n ```sh\n run \($0)\n ```" } + let blocks = parse(steps.joined(separator: "\n")) + XCTAssertEqual(blocks.filter { if case .codeBlock = $0.content { return true }; return false }.count, 11) + let indents = try orderedItemIndents(blocks) + XCTAssertEqual(indents.count, 11) + XCTAssertEqual(Set(indents).count, 1, "\(indents)") + + // A block NOT under the list (not indented) ends it: the list after + // it hangs on its own. + let separate = try orderedItemIndents(parse("1. a\n2. b\n\n```\ncode\n```\n\n10. c")) + XCTAssertEqual(separate.count, 3) + XCTAssertEqual(separate[0], separate[1]) + XCTAssertNotEqual(separate[1], separate[2]) + } + // MARK: - Partial re-measures carry the column @MainActor From 246cde8ab5a75e9bbb8a7d9c7698ece22dd34f94 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:07:24 +0100 Subject: [PATCH 040/105] fix(mermaid): a rendered diagram is as tall as it is, no 50 pt floor The web view's height report and the measurer's formula both kept a diagram at least 50 pt tall. A flat diagram at a small zoom or in a narrow column (35 pt at 50 %, 25 pt fitted into 120 pt) stood in a 50 pt box, blank below it. - BlockLayout.Mermaid.diagramHeight has no floor. - BlockLayout.Mermaid.acceptedHeight: a diagram that rendered (its geometry was posted just before the height) takes the reported height exactly, and a report of nothing is ignored; a failed render (Mermaid's error text) keeps the 50 pt minimum (failedRenderMinimumHeight). Print/PDF is untouched (MermaidPDFRenderer has its own handler). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 21 ++++++++++++--- QuickMD/QuickMD/Views/MermaidBlockView.swift | 9 +++++-- .../QuickMDTests/VerticalRhythmTests.swift | 27 +++++++++++++++++-- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index d3e3ccb..2f74390 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -516,8 +516,9 @@ enum BlockLayout { /// Height a diagram starts at when nothing has measured it yet. static let defaultHeight: CGFloat = 200 static let cornerRadius: CGFloat = 6 - /// The least height the web view reports (`MermaidWebView`). - static let minimumHeight: CGFloat = 50 + /// The least height of a diagram that did NOT render (Mermaid's error + /// text, or nothing): a rendered one is as tall as it is. + static let failedRenderMinimumHeight: CGFloat = 50 /// What a rendered diagram's height follows from, as the inline web view /// posts it (`mermaidGeometry` in mermaid-template.html): the SVG's @@ -531,13 +532,25 @@ enum BlockLayout { /// The diagram's height in a web view `contentWidth` wide at /// `fontScale` — the template's `resizeDiagram`: the natural size × /// zoom, never wider than the view (`window.innerWidth`, whole CSS - /// px), plus the page's overhead, and at least `minimumHeight`. + /// px), plus the page's overhead. No floor: a flat diagram at a small + /// zoom is that flat (1.14.4 kept it at least 50 pt, blank below it). static func diagramHeight(_ geometry: Geometry, fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat { let viewport = floor(contentWidth) guard geometry.naturalWidth > 0, geometry.naturalHeight > 0, viewport > 0 else { return defaultHeight } let fit = viewport / geometry.naturalWidth let factor = min(fontScale * min(1, fit), fit) - return max(minimumHeight, geometry.naturalHeight * factor + geometry.overhead) + return geometry.naturalHeight * factor + geometry.overhead + } + + /// The height a diagram's web view reported, as its row takes it. + /// `rendered`: the diagram rendered (its geometry was posted) — the + /// height is exact, as `diagramHeight` computes it; a report of + /// nothing is ignored (nil). Otherwise the page shows Mermaid's error + /// text (or nothing): at least `failedRenderMinimumHeight`. + static func acceptedHeight(reported: Double, rendered: Bool) -> CGFloat? { + guard reported.isFinite else { return nil } + guard rendered else { return max(CGFloat(reported), failedRenderMinimumHeight) } + return reported > 0 ? CGFloat(reported) : nil } /// A diagram row: the diagram, the view's padding and the row's. diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index c99e9e7..634bd63 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -362,8 +362,13 @@ private struct MermaidWebView: NSViewRepresentable { source: source, isDark: isDark) return } - guard let height = message.body as? Double else { return } - let newHeight = max(CGFloat(height), 50) + // A diagram that rendered posted its geometry just before this + // (`reportGeometry`); a failed render posts none. + guard let height = message.body as? Double, let source = lastSource, let isDark = lastIsDark, + let newHeight = BlockLayout.Mermaid.acceptedHeight( + reported: height, + rendered: MermaidGeometryStore.geometry(source: source, isDark: isDark) != nil) + else { return } if abs(newHeight - parent.diagramHeight) > 1 { DispatchQueue.main.async { self.parent.diagramHeight = newHeight diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index d59d2db..2ae3f31 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -961,12 +961,15 @@ final class VerticalRhythmTests: XCTestCase { /// A diagram's height at any width and zoom from the geometry its web view /// posted — the template's own formula: × zoom while it fits, never wider - /// than the view, plus the page's overhead, at least 50 pt. + /// than the view, plus the page's overhead. No floor (1.14.4 had 50 pt). func testADiagramsHeightFollowsTheTemplatesFormula() { typealias Mermaid = BlockLayout.Mermaid let small = Mermaid.Geometry(naturalWidth: 240, naturalHeight: 70, overhead: 0) XCTAssertEqual(Mermaid.diagramHeight(small, fontScale: 1.25, contentWidth: 819), 87.5) - XCTAssertEqual(Mermaid.diagramHeight(small, fontScale: 0.5, contentWidth: 819), 50) // the minimum + XCTAssertEqual(Mermaid.diagramHeight(small, fontScale: 0.5, contentWidth: 819), 35) + // A flat diagram in a narrow column: fitted to 120 of 480 px. + let flat = Mermaid.Geometry(naturalWidth: 480, naturalHeight: 100, overhead: 0) + XCTAssertEqual(Mermaid.diagramHeight(flat, fontScale: 1, contentWidth: 120), 25) let wide = Mermaid.Geometry(naturalWidth: 1600, naturalHeight: 400, overhead: 4) XCTAssertEqual(Mermaid.diagramHeight(wide, fontScale: 1, contentWidth: 800), 204) // fitted: 0.5 XCTAssertEqual(Mermaid.diagramHeight(wide, fontScale: 1.5, contentWidth: 800), 204) // still fitted @@ -976,6 +979,26 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNil(MermaidGeometryStore.geometry(source: "graph TD; Never-->Rendered", isDark: false)) } + /// What the row takes from the height the web view reports: a diagram + /// that rendered is as tall as it is — the 25 pt flat one stayed 50 pt + /// tall in 1.14.4, 25 pt of it blank, and the measurer (exact since the + /// placeholder fix) and the view disagreed. A failed render (Mermaid's + /// error text) keeps a minimum; a report of nothing is not taken. + func testARenderedDiagramTakesItsReportedHeightExactly() { + typealias Mermaid = BlockLayout.Mermaid + XCTAssertEqual(Mermaid.acceptedHeight(reported: 25, rendered: true), 25) + XCTAssertEqual(Mermaid.acceptedHeight(reported: 87.5, rendered: true), 87.5) + XCTAssertNil(Mermaid.acceptedHeight(reported: 0, rendered: true)) + XCTAssertNil(Mermaid.acceptedHeight(reported: .nan, rendered: true)) + XCTAssertEqual(Mermaid.acceptedHeight(reported: 31, rendered: false), Mermaid.failedRenderMinimumHeight) + XCTAssertEqual(Mermaid.acceptedHeight(reported: 0, rendered: false), Mermaid.failedRenderMinimumHeight) + XCTAssertEqual(Mermaid.acceptedHeight(reported: 120, rendered: false), 120) + // The view and the measurer agree on a flat diagram. + let flat = Mermaid.Geometry(naturalWidth: 480, naturalHeight: 100, overhead: 0) + XCTAssertEqual(Mermaid.acceptedHeight(reported: 25, rendered: true), + Mermaid.diagramHeight(flat, fontScale: 1, contentWidth: 120)) + } + /// A diagram view without a snapshot at its zoom and width finds the /// latest one of the same diagram in the same theme as its stand-in, so a /// re-created row never shows the empty box the web view is for the half From 817907b0b7ced76108c0922dd17d81aaf0d08055 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:08:48 +0100 Subject: [PATCH 041/105] fix(reading-mode): no heading copy icon in Reading Mode, same heading layout HeadingBlockView had no reading-mode input, so the hover copy icon still appeared while reading. It now takes isReadingMode (one call site in MarkdownView) and drops the button overlay then, while the title keeps its reserved trailing strip (BlockLayout.Heading.titleTrailingPadding), so the wrap width, the measurer and the row heights are unchanged by the toggle. Visible cells pick it up through applyLayoutStyle's refreshMaterializedRootViews, which runs after the new content closure is installed. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 8 +++ QuickMD/QuickMD/MarkdownView.swift | 3 +- QuickMD/QuickMD/Views/ChromeButtons.swift | 51 ++++++++++-------- QuickMD/QuickMDTests/HeadingLayoutTests.swift | 54 +++++++++++++++++++ 4 files changed, 92 insertions(+), 24 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 6ecb963..a5fd558 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -234,6 +234,14 @@ enum BlockLayout { /// title at the same reduced width. static let copyButtonReservedWidth: CGFloat = copyButtonSpacing + copyButtonWidth + /// Reading Mode hides the copy button (no chrome while reading)… + static func showsCopyButton(isReadingMode: Bool) -> Bool { !isReadingMode } + + /// …but NOT its strip: the title is padded by the same width in both + /// modes, so its wrap width, this measurer's heights and the row + /// heights do not change when the mode toggles. + static func titleTrailingPadding(isReadingMode: Bool) -> CGFloat { copyButtonReservedWidth } + // MARK: Horizontal placement of the copy button /// The width the title wraps at inside a heading row `rowWidth` wide — diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index ed60ecd..7823db0 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -1220,7 +1220,8 @@ struct MarkdownView: View { let section = SectionExtractor.extractSection(from: currentText, entry: entry, headings: headings) { copyToClipboard(section) } - } + }, + isReadingMode: isReadingMode ) case .mathBlock(let latex): diff --git a/QuickMD/QuickMD/Views/ChromeButtons.swift b/QuickMD/QuickMD/Views/ChromeButtons.swift index 3943eb5..835482f 100644 --- a/QuickMD/QuickMD/Views/ChromeButtons.swift +++ b/QuickMD/QuickMD/Views/ChromeButtons.swift @@ -33,6 +33,9 @@ struct HeadingBlockView: View { let focusedOccurrence: Int? let onLink: (URL) -> Void let onCopySection: () -> Void + /// Reading Mode: no copy button; the title keeps its reserved strip + /// (`Metrics.titleTrailingPadding`), so nothing re-wraps. + var isReadingMode: Bool = false @State private var isHovered = false @State private var hideWorkItem: DispatchWorkItem? @@ -63,36 +66,38 @@ struct HeadingBlockView: View { cacheScope: "\(level)|\(title)", onLink: onLink ) - .padding(.trailing, Metrics.copyButtonReservedWidth) + .padding(.trailing, Metrics.titleTrailingPadding(isReadingMode: isReadingMode)) .padding(.top, geometry.titleTopInset) .frame(minHeight: geometry.minimumHeight, alignment: .top) .overlay { // The reader only reads the row width the cell already gave this // view; nothing here sizes the row. Its empty area is not hittable, // so drags on the title still reach the text view. - GeometryReader { proxy in - // `buttonTopInset` puts the icon's baseline on the title's first - // baseline, as the old `.firstTextBaseline` HStack did. - Button { - onCopySection() - } label: { - Image(systemName: "doc.on.doc") - .font(.system(size: Metrics.copyButtonIconFontSize)) - .foregroundColor(.secondary.opacity(0.7)) - .padding(Metrics.copyButtonIconPadding) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .help("Copy section") - // The button lies on the title's text view, whose I-beam would - // otherwise show over the icon; re-set on every move, as the - // text view updates its cursor on mouse moves too. - .onContinuousHover { phase in - if case .active = phase { NSCursor.arrow.set() } + if Metrics.showsCopyButton(isReadingMode: isReadingMode) { + GeometryReader { proxy in + // `buttonTopInset` puts the icon's baseline on the title's first + // baseline, as the old `.firstTextBaseline` HStack did. + Button { + onCopySection() + } label: { + Image(systemName: "doc.on.doc") + .font(.system(size: Metrics.copyButtonIconFontSize)) + .foregroundColor(.secondary.opacity(0.7)) + .padding(Metrics.copyButtonIconPadding) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help("Copy section") + // The button lies on the title's text view, whose I-beam would + // otherwise show over the icon; re-set on every move, as the + // text view updates its cursor on mouse moves too. + .onContinuousHover { phase in + if case .active = phase { NSCursor.arrow.set() } + } + .opacity(isHovered ? 1 : 0) + .padding(.top, geometry.buttonTopInset) + .padding(.leading, copyButtonLeading(rowWidth: proxy.size.width, headingAttr: headingAttr)) } - .opacity(isHovered ? 1 : 0) - .padding(.top, geometry.buttonTopInset) - .padding(.leading, copyButtonLeading(rowWidth: proxy.size.width, headingAttr: headingAttr)) } } .contentShape(Rectangle()) diff --git a/QuickMD/QuickMDTests/HeadingLayoutTests.swift b/QuickMD/QuickMDTests/HeadingLayoutTests.swift index 8b84afc..8e3725f 100644 --- a/QuickMD/QuickMDTests/HeadingLayoutTests.swift +++ b/QuickMD/QuickMDTests/HeadingLayoutTests.swift @@ -239,6 +239,60 @@ final class HeadingLayoutTests: XCTestCase { } } + // MARK: - Reading Mode: no copy button, same strip + + /// The `HeadingBlockView` composition in a given mode, from the same + /// `Metrics` decisions the view takes (the view is not compiled here). + private func modeHeadingHeight(_ ns: NSAttributedString, firstFont: NSFont?, width: CGFloat, + isReadingMode: Bool) -> CGFloat { + let geometry = Metrics.titleGeometry(titleFirstBaseline: Metrics.titleFirstBaseline(font: firstFont)) + return hostedHeight( + TextKitHeightView(attributed: ns) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, Metrics.titleTrailingPadding(isReadingMode: isReadingMode)) + .padding(.top, geometry.titleTopInset) + .frame(minHeight: geometry.minimumHeight, alignment: .top) + .overlay(alignment: .topTrailing) { + if Metrics.showsCopyButton(isReadingMode: isReadingMode) { + copyButton.padding(.top, geometry.buttonTopInset) + } + } + .frame(width: width) + .fixedSize(horizontal: false, vertical: true) + ) + } + + /// Reading Mode hides the copy icon but keeps its reserved strip: the + /// title wraps at the same width, so the heading's height — the view's and + /// the measurer's — is identical in both modes, and toggling the mode + /// never re-wraps a heading. + func testReadingModeHidesTheCopyButtonButKeepsTheHeadingHeight() { + XCTAssertTrue(Metrics.showsCopyButton(isReadingMode: false)) + XCTAssertFalse(Metrics.showsCopyButton(isReadingMode: true), "no copy icon in Reading Mode") + XCTAssertEqual(Metrics.titleTrailingPadding(isReadingMode: true), Metrics.copyButtonReservedWidth) + XCTAssertEqual(Metrics.titleTrailingPadding(isReadingMode: false), Metrics.copyButtonReservedWidth) + + let theme = MarkdownTheme.cached(for: .light) + let markdown = "# Heading\n## \(Self.longTitle)\n###### \(Self.longTitle)" + let blocks = MarkdownBlockParser(theme: theme).parse(markdown) + for width: CGFloat in [260, 720] { + let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: width, math: nil).table.heights + for (index, block) in blocks.enumerated() { + guard case .heading(let level, let title, _) = block.content else { return XCTFail("heading") } + let (_, ns) = headingString(title, level: level, theme: theme, fontScale: 1) + let font = MarkdownRenderer(theme: theme).headerAppKitFont(level: level) + let standard = modeHeadingHeight(ns, firstFont: font, width: width, isReadingMode: false) + let reading = modeHeadingHeight(ns, firstFont: font, width: width, isReadingMode: true) + XCTAssertEqual(reading, standard, "H\(level) w=\(width): the view's height") + // The measurer has no mode input: its height is the one row + // height for both, and it still matches the view. + XCTAssertEqual(measured[index], standard, accuracy: 1 + 2 * CGFloat(lineCount(ns, width: width)), + "H\(level) w=\(width): measurer vs view") + } + } + } + // MARK: - Copy button: right after the title (as in v1.10) func testCopyButtonLeadingFollowsTheTitleAndStopsAtTheEdge() { From b32c10bb90b5983835ea1171ecb780e03d699a03 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:09:02 +0100 Subject: [PATCH 042/105] fix(anchor): tables without a header get a distinctive anchor signature After a reload the scroll anchor is found again by the block's content. A table's signature was its row count and header text, so every header-less table (blank header cells) and the front-matter properties table (no header) with as many rows had the same one: `b|2|` and `b|2|\u{1F}` for the three tables in the test. The signature now holds the header AND the first row. The signature moves from a private function in VirtualBlockList to MarkdownBlock.anchorSignature, so the tests can reach it. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlock.swift | 41 ++++++++++++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 47 ++----------------- .../QuickMDTests/VerticalRhythmTests.swift | 38 +++++++++++++++ 3 files changed, 82 insertions(+), 44 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift index f954a37..23c2a74 100644 --- a/QuickMD/QuickMD/MarkdownBlock.swift +++ b/QuickMD/QuickMD/MarkdownBlock.swift @@ -143,6 +143,47 @@ struct MarkdownBlock: Identifiable, Sendable { self.substitutesFormula = substitutesFormula } + /// A cheap content fingerprint of the block, to find the scroll anchor + /// again after the document changed shape (`VirtualBlockList`). Neither + /// identity the block already has can do that job: + /// + /// * `id` is POSITIONAL (`text-17`), so inserting one block above + /// renumbers every block below it. + /// * `sourceLine` moves with any edit above it. + /// + /// The characters are what the reader was actually looking at, so they + /// are the identity matched on. 80 characters distinguish paragraphs + /// without copying the document, and the kind tag in front keeps a + /// heading from matching a paragraph with the same words. A table is its + /// header AND first row: a header-less table (blank header cells) and the + /// front-matter properties table (no header) were only `b||`, so + /// any two of them with as many rows matched each other. + var anchorSignature: String { + func head(_ text: String) -> String { String(text.prefix(80)) } + switch content { + case .text(let attributed): + return "t|" + head(String(attributed.characters)) + case .table(let headers, let rows, _, _): + return "b|\(rows.count)|" + head((headers + (rows.first ?? [])).joined(separator: "\u{1F}")) + case .codeBlock(let code, let language): + return "c|\(language)|" + head(code) + case .image(let url, let alt, _): + return "i|\(head(url))|" + head(alt) + case .blockquote(let content, let level): + return "q|\(level)|" + head(content) + case .alert(let kind, let content): + return "a|\(kind.rawValue)|" + head(content) + case .heading(let level, let title, _): + return "h|\(level)|" + head(title) + case .mathBlock(let latex): + return "m|" + head(latex) + case .mermaidDiagram(let source): + return "d|" + head(source) + case .svgImage(let source): + return "s|" + head(source) + } + } + enum BlockContent: Sendable { case text(AttributedString) /// `.properties` (front matter): `headers` empty, every row `[key, value]`. diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 6c493a9..40f70aa 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -47,47 +47,6 @@ private let listLog = Logger(subsystem: "pl.falami.studio.QuickMD", category: "V private let listSignpost = OSSignposter(subsystem: "pl.falami.studio.QuickMD", category: "VirtualBlockList") #endif -// MARK: - Block identity for anchor restoration - -/// A cheap content fingerprint of one block. -/// -/// Used only to find the scroll anchor again after the document changed shape. -/// Neither of the two identities a block already has can do that job: -/// -/// * `MarkdownBlock.id` is POSITIONAL (`text-17`), so inserting one block above -/// renumbers every block below it. -/// * `sourceLine` moves with any edit above it. -/// -/// The characters are what the reader was actually looking at, so they are the -/// identity we match on. 80 characters distinguishes paragraphs without copying -/// the document, and the kind tag in front keeps a heading from matching a -/// paragraph with the same words. -private func blockSignature(_ block: MarkdownBlock) -> String { - func head(_ text: String) -> String { String(text.prefix(80)) } - switch block.content { - case .text(let attributed): - return "t|" + head(String(attributed.characters)) - case .table(let headers, let rows, _, _): - return "b|\(rows.count)|" + head(headers.joined(separator: "\u{1F}")) - case .codeBlock(let code, let language): - return "c|\(language)|" + head(code) - case .image(let url, let alt, _): - return "i|\(head(url))|" + head(alt) - case .blockquote(let content, let level): - return "q|\(level)|" + head(content) - case .alert(let kind, let content): - return "a|\(kind.rawValue)|" + head(content) - case .heading(let level, let title, _): - return "h|\(level)|" + head(title) - case .mathBlock(let latex): - return "m|" + head(latex) - case .mermaidDiagram(let source): - return "d|" + head(source) - case .svgImage(let source): - return "s|" + head(source) - } -} - struct VirtualBlockList: NSViewRepresentable { /// Where a programmatic scroll parks the target row. @@ -689,7 +648,7 @@ struct VirtualBlockList: NSViewRepresentable { if preferringLastAnchor, let anchor, let row = anchoredRow(contentTop: top) { return CapturedAnchor(row: row, offsetFromBlockTop: anchor.offset, sourceLine: blocks[row].sourceLine, - signature: blockSignature(blocks[row])) + signature: blocks[row].anchorSignature) } var row = tableView.row(at: NSPoint(x: 0, y: max(0, top))) if row < 0 { @@ -715,7 +674,7 @@ struct VirtualBlockList: NSViewRepresentable { contentTop: top, rowRect: tableView.rect(ofRow: row), spacingAbove: table.spacing(row)), sourceLine: blocks[row].sourceLine, - signature: blockSignature(blocks[row])) + signature: blocks[row].anchorSignature) } /// ⌘E's target line (E-D1): the selection's first row, else the row the @@ -824,7 +783,7 @@ struct VirtualBlockList: NSViewRepresentable { for (index, block) in blocks.enumerated() { // Past the anchor, distance only grows: nothing left can win. if index > row, index - row >= bestDistance { break } - guard blockSignature(block) == signature else { continue } + guard block.anchorSignature == signature else { continue } let distance = abs(index - row) if distance < bestDistance { best = index diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 2ae3f31..9c88de5 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -472,6 +472,44 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNotEqual(separate[1], separate[2]) } + // MARK: - Anchor signatures + + /// The scroll anchor is found again after a reload by the block's content + /// (`anchorSignature`). A table's was its row count and header: every + /// header-less table (blank header cells) and the front-matter properties + /// table (no header) of the same length had the same one, so a reload + /// could put the reader on the wrong table. + func testHeaderlessTablesAndFrontMatterHaveDistinctAnchorSignatures() { + let markdown = """ + --- + title: Notes + author: Ann + --- + + | | | + |---|---| + | alpha | 1 | + | beta | 2 | + + Between. + + | | | + |---|---| + | gamma | 3 | + | delta | 4 | + """ + let tables = parse(markdown).filter { if case .table = $0.content { return true }; return false } + XCTAssertEqual(tables.count, 3) + XCTAssertEqual(Set(tables.map(\.anchorSignature)).count, 3, "\(tables.map(\.anchorSignature))") + // Stable across a re-parse, and still distinctive with an edit above. + let again = parse("Edited above.\n\n" + markdown) + .filter { if case .table = $0.content { return true }; return false } + XCTAssertEqual(again.map(\.anchorSignature).suffix(2), tables.map(\.anchorSignature).suffix(2)) + // A table with a header keeps it in front. + let headed = parse("| Name | Value |\n|---|---|\n| a | 1 |")[0] + XCTAssertTrue(headed.anchorSignature.hasPrefix("b|1|Name\u{1F}Value\u{1F}a"), headed.anchorSignature) + } + // MARK: - Partial re-measures carry the column @MainActor From 1cc4046fc2e29efe73837a425459c259fc05d28f Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:11:54 +0100 Subject: [PATCH 043/105] fix(settings): remember the Settings tab by identifier, migrating the old index The Settings TabView had no selection, so SwiftUI remembered the tab by index (com_apple_SwiftUI_Settings_selectedTabIndex) and inserting a tab shifted everyone's remembered tab. SettingsView now binds an @AppStorage SettingsTab (general/themes/fonts/editor, key settingsSelectedTab) with a tag per tab. While that key is absent, the legacy index is mapped to the tab at that position (missing or out of range: General), so nobody's tab changes on update. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 6 +++ QuickMD/QuickMD/SettingsTab.swift | 38 +++++++++++++++++++ QuickMD/QuickMD/Views/SettingsView.swift | 10 ++++- .../QuickMDTests/SectionExtractorTests.swift | 23 +++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 QuickMD/QuickMD/SettingsTab.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index ee59e7a..4c4ad5f 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -46,6 +46,8 @@ A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000301 /* ReferenceLinks.swift */; }; A1B2C3D401000303 /* CrossDocumentAnchors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000303 /* CrossDocumentAnchors.swift */; }; A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000303 /* CrossDocumentAnchors.swift */; }; + A1B2C3D401000305 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000305 /* SettingsTab.swift */; }; + A1B2C3D401000306 /* SettingsTab.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000305 /* SettingsTab.swift */; }; A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C1 /* TableSelectionTests.swift */; }; A1B2C3D40100000E /* ImageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000012 /* ImageBlockView.swift */; }; A1B2C3D40100000F /* BlockquoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000013 /* BlockquoteView.swift */; }; @@ -241,6 +243,7 @@ A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; A1B2C3D400000301 /* ReferenceLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceLinks.swift; sourceTree = ""; }; A1B2C3D400000303 /* CrossDocumentAnchors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDocumentAnchors.swift; sourceTree = ""; }; + A1B2C3D400000305 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; A1B2C3D4000001C1 /* TableSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSelectionTests.swift; sourceTree = ""; }; A1B2C3D400000012 /* ImageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlockView.swift; sourceTree = ""; }; A1B2C3D400000013 /* BlockquoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockquoteView.swift; sourceTree = ""; }; @@ -395,6 +398,7 @@ A1B2C3D4000002A0 /* VerticalRhythm.swift */, A1B2C3D400000301 /* ReferenceLinks.swift */, A1B2C3D400000303 /* CrossDocumentAnchors.swift */, + A1B2C3D400000305 /* SettingsTab.swift */, A1B2C3D400000110 /* DocumentReadingPosition.swift */, A1B2C3D400000120 /* DocumentFileFormat.swift */, A1B2C3D400000121 /* DocumentFileWriter.swift */, @@ -695,6 +699,7 @@ A1B2C3D4010002A0 /* VerticalRhythm.swift in Sources */, A1B2C3D401000301 /* ReferenceLinks.swift in Sources */, A1B2C3D401000303 /* CrossDocumentAnchors.swift in Sources */, + A1B2C3D401000305 /* SettingsTab.swift in Sources */, A1B2C3D401000110 /* DocumentReadingPosition.swift in Sources */, A1B2C3D401000120 /* DocumentFileFormat.swift in Sources */, A1B2C3D401000122 /* DocumentFileWriter.swift in Sources */, @@ -751,6 +756,7 @@ A1B2C3D4010002A1 /* VerticalRhythm.swift in Sources (Tests) */, A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */, A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */, + A1B2C3D401000306 /* SettingsTab.swift in Sources (Tests) */, A1B2C3D4010000C2 /* DocumentSelectionTests.swift in Sources */, A1B2C3D401000111 /* DocumentReadingPosition.swift in Sources (Tests) */, A1B2C3D401000112 /* ExternalEditorLineTests.swift in Sources */, diff --git a/QuickMD/QuickMD/SettingsTab.swift b/QuickMD/QuickMD/SettingsTab.swift new file mode 100644 index 0000000..8fe045f --- /dev/null +++ b/QuickMD/QuickMD/SettingsTab.swift @@ -0,0 +1,38 @@ +import Foundation + +// MARK: - Settings: the remembered tab + +/// The Settings window's tabs, remembered by IDENTIFIER. +/// +/// Until 1.14.4 the `TabView` had no selection, and SwiftUI remembered the +/// selected tab by INDEX (`legacyIndexKey`): inserting "General" at 0 shifted +/// every user's remembered tab by one. The tags here survive reordering and +/// new tabs. +enum SettingsTab: String, CaseIterable { + // Declaration order = the order the tabs show = the legacy index. + case general, themes, fonts, editor + + static let defaultsKey = "settingsSelectedTab" + + /// Where SwiftUI's `Settings` scene kept the selected tab's index, read from + /// the app's own defaults domain (2026-11-04: present in the App Store + /// build's container and the E2E build, an Int). + static let legacyIndexKey = "com_apple_SwiftUI_Settings_selectedTabIndex" + + /// The tab a stored legacy index meant — in the order the tabs had when it + /// was written, which is this enum's order (the index was last remapped + /// by the "General" insertion itself). Missing or out of range → General. + static func fromLegacyIndex(_ index: Int?) -> SettingsTab { + guard let index, allCases.indices.contains(index) else { return .general } + return allCases[index] + } + + /// The tab to open with: the stored identifier, else (first launch after + /// the update) the tab SwiftUI remembered by index, so nobody's tab moves. + static func initial(in defaults: UserDefaults) -> SettingsTab { + if let raw = defaults.string(forKey: defaultsKey), let tab = SettingsTab(rawValue: raw) { + return tab + } + return fromLegacyIndex(defaults.object(forKey: legacyIndexKey) as? Int) + } +} diff --git a/QuickMD/QuickMD/Views/SettingsView.swift b/QuickMD/QuickMD/Views/SettingsView.swift index b4bf8b9..c745d03 100644 --- a/QuickMD/QuickMD/Views/SettingsView.swift +++ b/QuickMD/QuickMD/Views/SettingsView.swift @@ -3,16 +3,24 @@ import SwiftUI /// Settings window (⌘,): general behaviour + theme picker + document fonts + /// external editor selection. struct SettingsView: View { + /// Remembered by identifier (`SettingsTab`); absent → the index SwiftUI + /// remembered before, so an update does not move anyone's tab. + @AppStorage(SettingsTab.defaultsKey) private var selectedTab: SettingsTab = SettingsTab.initial(in: .standard) + var body: some View { - TabView { + TabView(selection: $selectedTab) { GeneralSettingsView() .tabItem { Label("General", systemImage: "gearshape") } + .tag(SettingsTab.general) ThemePickerView() .tabItem { Label("Themes", systemImage: "paintpalette") } + .tag(SettingsTab.themes) FontPickerView() .tabItem { Label("Fonts", systemImage: "textformat") } + .tag(SettingsTab.fonts) ExternalEditorPickerView() .tabItem { Label("Editor", systemImage: "pencil") } + .tag(SettingsTab.editor) } } } diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index f6835aa..4b7061c 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -125,6 +125,29 @@ final class SectionExtractorTests: XCTestCase { XCTAssertEqual([0, 1, 2, 3, 4, 5, 6, 7, 42].map(ToCDepth.normalized), [2, 2, 2, 3, 4, 6, 6, 6, 6]) } + // MARK: - Settings tab (remembered by identifier) + + /// The Settings tab was remembered by INDEX; the update reads that index + /// once (when the identifier key is absent) and maps it to the same tab. + func testSettingsTabMigratesTheLegacyIndex() throws { + XCTAssertEqual(SettingsTab.allCases, [.general, .themes, .fonts, .editor], "legacy index order") + XCTAssertEqual([0, 1, 2, 3].map(SettingsTab.fromLegacyIndex), [.general, .themes, .fonts, .editor]) + XCTAssertEqual([-1, 4, 99].map(SettingsTab.fromLegacyIndex), [.general, .general, .general]) + XCTAssertEqual(SettingsTab.fromLegacyIndex(nil), .general) + + let suite = try TemporaryDefaults("SettingsTab") + defer { suite.remove() } + let defaults = suite.defaults + XCTAssertEqual(SettingsTab.initial(in: defaults), .general, "fresh install") + defaults.set(2, forKey: SettingsTab.legacyIndexKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "the tab SwiftUI remembered") + defaults.set(SettingsTab.editor.rawValue, forKey: SettingsTab.defaultsKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .editor, "the identifier wins once written") + defaults.set("removed-tab", forKey: SettingsTab.defaultsKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "an unknown identifier falls back") + XCTAssertEqual(SettingsTab.legacyIndexKey, "com_apple_SwiftUI_Settings_selectedTabIndex") + } + // MARK: - Heading anchors (#43) private func anchorHeadings(of markdown: String) -> [(id: String, text: String)] { From d1ed1bb296db9f91dd21ea82c7dcc8ac0c56b025 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:13:07 +0100 Subject: [PATCH 044/105] docs(links): inDocumentFragment comment describes linkURL, not the old %23 fallback Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/SectionExtractor.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/SectionExtractor.swift b/QuickMD/QuickMD/SectionExtractor.swift index 5120060..080bd6e 100644 --- a/QuickMD/QuickMD/SectionExtractor.swift +++ b/QuickMD/QuickMD/SectionExtractor.swift @@ -137,10 +137,14 @@ enum HeadingAnchors { } /// The percent-decoded fragment of an in-document link (`#install`), nil - /// for any other link. Read from the link as written: a destination - /// `URL(string:)` rejects (non-ASCII or spaces on macOS 13) arrives - /// percent-encoded, its `#` as `%23`. The accepted cost: a file whose - /// name starts with `#`, linked as `%23notes.md`, reads as an anchor. + /// for any other link. Read from the link as written, decoded: link URLs + /// come from `MarkdownRenderer.linkURL(for:)`, which keeps the `#` and + /// encodes only the rest (`#Install Guide` → `#Install%20Guide`) on every + /// macOS. Until 1.14.4 the renderer's macOS 13 fallback encoded the `#` + /// itself as `%23`, and decoding before the `#` test is what still made + /// those links work; it is kept for any URL that arrives encoded that + /// way. Its cost: a file whose name starts with `#`, linked as + /// `%23notes.md`, reads as an anchor. static func inDocumentFragment(of url: URL) -> String? { guard url.scheme == nil else { return nil } let written = url.relativeString.removingPercentEncoding ?? url.relativeString From d6e28a25ecb796587ca5ad436d0220c8fa315b9c Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:15:35 +0100 Subject: [PATCH 045/105] fix(footnotes): footnote references resolve in quotes, alerts and table cells Those bodies are rendered without the document's footnote definitions, so [^note] stayed literal there. ReferenceLinks now also rewrites a known [^note] into [^N], N being its number in definition order, and tryParseFootnoteRef draws that form as the same superscript a paragraph gets, with no definitions needed. Code spans and escapes are left alone; the equivalence test now covers footnotes against the definition-aware renderer. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 13 +++--- QuickMD/QuickMD/MarkdownRenderer.swift | 13 +++++- QuickMD/QuickMD/ReferenceLinks.swift | 49 ++++++++++++++++++----- QuickMD/QuickMDTests/ParserTests.swift | 37 ++++++++++++++++- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 22c371d..08feff8 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -320,9 +320,10 @@ struct MarkdownBlockParser: Sendable { alignments = normalizeArray(alignments, to: columnCount, default: .leading) rows = rows.map { normalizeArray($0, to: columnCount, default: "") } // Cells are rendered later without the definitions (`ReferenceLinks`). - if !referenceDefinitions.isEmpty { - headers = headers.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions) } - rows = rows.map { $0.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions) } } + if !referenceDefinitions.isEmpty || !footnoteDefinitions.isEmpty { + let ids = footnoteDefinitions.map(\.id) + headers = headers.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions, footnoteIds: ids) } + rows = rows.map { $0.map { ReferenceLinks.inlined($0, definitions: referenceDefinitions, footnoteIds: ids) } } } // Header row line — `i` has already walked past the body rows. @@ -387,14 +388,16 @@ struct MarkdownBlockParser: Sendable { // Quote and alert bodies are rendered later without the // definitions, so references are resolved here (`ReferenceLinks`). let content = ReferenceLinks.inlined(groupLines.joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions) + .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions, + footnoteIds: footnoteDefinitions.map(\.id)) if !content.isEmpty { // GFM alert: level-1 quote whose first line is exactly // `[!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / // `[!CAUTION]` (marker alone on the line, per GitHub spec). if groupLevel == 1, let kind = AlertKind(markerLine: groupLines[0]) { let body = ReferenceLinks.inlined(groupLines.dropFirst().joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions) + .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions, + footnoteIds: footnoteDefinitions.map(\.id)) blocks.append(.alert(index: blockIndex, kind: kind, content: body, sourceLine: groupSourceLine)) previousGroupWasQuote = false } else { diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 474039b..fc1a14b 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1085,6 +1085,12 @@ struct MarkdownRenderer: Sendable { // MARK: - Footnote References + /// Opens the id of a footnote reference whose NUMBER is already known + /// (`[^\u{E000}2]`, written by `ReferenceLinks` into quote, alert and + /// table-cell markdown). A private-use code point: no document's own + /// footnote id starts with it in practice. + static let numberedFootnoteMarker: Unicode.Scalar = "\u{E000}" + private func tryParseFootnoteRef(_ remaining: inout Substring) -> (AttributedString, Substring)? { guard remaining.hasPrefix("[^") else { return nil } @@ -1094,10 +1100,15 @@ struct MarkdownRenderer: Sendable { let fnId = String(afterBracket[.. 0 { + number = numbered } else { return nil // Unknown footnote reference, don't parse } diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index a47c982..ea3f00e 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -27,18 +27,32 @@ import Foundation /// exactly `url` (an escaped paren does not count for pairing either). enum ReferenceLinks { + /// What the document defines: link/image definitions (ids lowercased) + /// and footnote ids in definition order (a reference's number is its + /// 1-based position, as `tryParseFootnoteRef` numbers it). + private struct Sources { + let definitions: [String: String] + let footnoteIds: [String] + } + /// `text` with every resolvable reference inlined, line by line (the /// renderers take quote bodies line by line, so a reference never spans - /// lines). Unchanged when there are no definitions or no `[`. - static func inlined(_ text: String, definitions: [String: String]) -> String { - guard !definitions.isEmpty, text.contains("[") else { return text } + /// lines). Unchanged when there is nothing defined or no `[`. + /// + /// Footnote references (`[^note]`) become `[^N]` + /// (`MarkdownRenderer.numberedFootnoteMarker` + the number): Markdown has + /// no inline form for "footnote number N", and a definition-less renderer + /// draws that form as the same superscript a paragraph's `[^note]` gets. + static func inlined(_ text: String, definitions: [String: String], footnoteIds: [String] = []) -> String { + guard !definitions.isEmpty || !footnoteIds.isEmpty, text.contains("[") else { return text } + let sources = Sources(definitions: definitions, footnoteIds: footnoteIds) return text.split(separator: "\n", omittingEmptySubsequences: false) - .map { inlinedLine(String($0), definitions: definitions, linksDisabled: false) } + .map { inlinedLine(String($0), sources: sources, linksDisabled: false) } .joined(separator: "\n") } - private static func inlinedLine(_ line: String, definitions: [String: String], - linksDisabled: Bool) -> String { + private static func inlinedLine(_ line: String, sources: Sources, linksDisabled: Bool) -> String { + let definitions = sources.definitions guard line.contains("[") else { return line } var text = line text.makeContiguousUTF8() @@ -67,6 +81,12 @@ enum ReferenceLinks { remaining = remaining.dropFirst(verbatim) continue } + // `tryParseFootnoteRef` runs before emphasis and links. + if remaining.hasPrefix("[^"), let (numbered, rest) = footnote(remaining, sources: sources) { + out += numbered + remaining = rest + continue + } if remaining.hasPrefix("![") { if let inline = InlineSyntaxScanner.image(atStartOf: remaining, references: [:], pairs: pairs) { out += remaining[.. (String, Substring)? { + sources: Sources) -> (String, Substring)? { + let definitions = sources.definitions guard let closeBracket = InlineSyntaxScanner.closingBracket(in: remaining, pairs: pairs) else { return nil } let afterBracket = remaining.index(after: closeBracket) let linkText = String(remaining[remaining.index(after: remaining.startIndex).. (String, Substring)? { + let afterBracket = remaining.dropFirst(2) + guard let close = afterBracket.firstIndex(of: "]") else { return nil } + let id = String(afterBracket[.. Quoted[^a] and [^missing]. + + > [!TIP] + > Tip[^b]. + + | A | + |---| + | cell[^a] `[^a]` | + + [^a]: First. + [^b]: Second. + """ + let blocks = parse(md) + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) // no definitions + guard case .text(let body) = blocks[0].content else { return XCTFail("paragraph") } + XCTAssertEqual(String(body.characters), "Body\u{00B2}.", "the paragraph's numbering (definition order)") + guard case .blockquote(let quote, _) = blocks[1].content, + case .alert(_, let alert) = blocks[2].content, + case .table(_, let rows, _, .grid) = blocks[3].content else { return XCTFail("blocks") } + XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "Quoted\u{00B9} and [^missing].") + XCTAssertEqual(String(renderer.renderQuotedBody(alert).characters), "Tip\u{00B2}.") + XCTAssertEqual(String(renderer.renderInline(rows[0][0]).characters), "cell\u{00B9} [^a]", "code span kept") + } + /// The rewrite must draw exactly what the paragraph renderer (which HAS the /// definitions) draws for the same line: characters and links. func testReferenceInliningMatchesTheDefinitionAwareRenderer() { let theme = MarkdownTheme.cached(for: .light) let definitions = ["docs": "https://example.com/docs_(v2)", "img": "img/My Pic.png", "a b": "https://example.com/ab", "bad": "https://example.com/a\\\\c", "paren": "https://e.x/a)b(c"] - let withDefinitions = MarkdownRenderer(theme: theme, referenceDefinitions: definitions) + let footnotes = [(id: "n1", content: "x"), (id: "docs", content: "y")] + let withDefinitions = MarkdownRenderer(theme: theme, referenceDefinitions: definitions, + footnoteDefinitions: footnotes) let plain = MarkdownRenderer(theme: theme) let lines = [ "[docs] and [Docs][] and [text][DOCS] and [A B]", @@ -473,9 +505,10 @@ final class ParserTests: XCTestCase { "[unknown][nope] [unknown] [docs][nope]", "[bad] and [paren] escape what the <…> form needs", "a ` lone backtick [docs] then ``` three ``` [docs]", + "note[^n1] and [^docs] then [^none] `[^n1]` **b[^n1]** [![x][img]][docs][^n1]", ] for line in lines { - let inlined = ReferenceLinks.inlined(line, definitions: definitions) + let inlined = ReferenceLinks.inlined(line, definitions: definitions, footnoteIds: footnotes.map(\.id)) let expected = withDefinitions.renderInline(line) let actual = plain.renderInline(inlined) XCTAssertEqual(String(actual.characters), String(expected.characters), line) From 46afa5e5b90ba5f9bbf4f81adcfed43d2736dbae Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:17:19 +0100 Subject: [PATCH 046/105] feat(reading): the reading column scales with the zoom Reading Mode capped the column at 720 pt at every zoom: ~90 characters a line at 100 %, ~45 at 200 %, ~180 at 50 %. The cap is now 720 pt x the zoom (the body text is 16 pt x the zoom; there is no other body size setting), in whole points, and the window still caps it: 1440 pt at 200 % where the window has room, 360 pt at 50 %, exactly 720 at 100 %. - One definition: BlockLayout.Document.readingColumnCap(fontScale:) and columnWidth(inner:cap:), used by the list (cells and the width it publishes) and by SourceEditorController.columnGeometry, which now takes the zoom (parity tested at 0.5, 1, 1.5, 2 and 3). - LayoutStyle.reading(fontScale:) is derived from renderedFontScale, so the cap changes in the transaction that installs the zoomed parse. - A parse at a new zoom in reading mode is measured at the column the new cap gives (BlockLayout.Document.parseWidth, from the list's column room asked through DocumentColumn), and the parent adopts that width with it; the list takes it as reported (and reports the real column if the window moved meanwhile). One measure per zoom, the anchor restored as in any zoom round trip. Print and PDF do not use the reading column and are unchanged. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 41 ++++++++++-- QuickMD/QuickMD/MarkdownView.swift | 25 ++++++-- QuickMD/QuickMD/SourceEditorController.swift | 15 +++-- QuickMD/QuickMD/Views/VirtualBlockList.swift | 58 +++++++++++++++-- .../SourceEditorControllerTests.swift | 49 ++++++++++----- .../QuickMDTests/VerticalRhythmTests.swift | 62 +++++++++++++++++++ 6 files changed, 214 insertions(+), 36 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 2f74390..eb686f9 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -82,11 +82,44 @@ enum BlockLayout { // so `BlockHeightMeasurer` needs no reading-mode branch at all — it is // handed the clamped `contentWidth` like any other width. - /// Upper bound on the text column in reading mode; wider windows centre - /// the column instead of stretching the lines. 720 pt is ~90 characters - /// at the 16 pt default body size — past that the eye loses the line it - /// came from on the way back to the left margin. + /// Upper bound on the text column in reading mode at 100 %; wider + /// windows centre the column instead of stretching the lines. 720 pt is + /// ~90 characters at the 16 pt default body size — past that the eye + /// loses the line it came from on the way back to the left margin. + /// The cap at other zooms is `readingColumnCap(fontScale:)`. static let readingMaxContentWidth: CGFloat = 720 + + /// Reading mode's column cap at `fontScale`: `readingMaxContentWidth` + /// scaled with the body text (16 pt × the zoom; there is no other body + /// size setting — Settings' default zoom IS the starting zoom), in + /// whole points. The line keeps its ~90 characters at every zoom: + /// until 1.14.4 the cap stayed 720 pt, so 200 % left ~45 characters a + /// line and 50 % ~180. Still capped by the window (`columnWidth`). + static func readingColumnCap(fontScale: CGFloat) -> CGFloat { + (readingMaxContentWidth * max(fontScale, 0)).rounded() + } + + /// The width a block view gets — the width the heights are measured + /// at: `inner` (the clip's whole points − 2 × `contentHorizontalPadding`), + /// no wider than `cap` in reading mode. THE one definition, used by + /// `VirtualBlockList` (cells and published width), by `MarkdownView` + /// (the width a zoom in reading mode measures its parse at) and by + /// `SourceEditorController.columnGeometry`. + static func columnWidth(inner: CGFloat, cap: CGFloat?) -> CGFloat { + guard let cap else { return max(0, inner) } + return max(0, floor(min(inner, cap))) + } + + /// The width a new parse is measured at: the list's `contentWidth` — + /// except at a new zoom in reading mode (`readingFontScale` = the + /// parse's zoom), whose cap follows the zoom: the column the list will have once the + /// parse is installed (`inner` capped at that zoom), so a zoom in + /// reading mode measures the document once, not at the old column + /// and then again. 0 (no layout yet) stays 0. + static func parseWidth(contentWidth: CGFloat, inner: CGFloat, readingFontScale: CGFloat?) -> CGFloat { + guard contentWidth > 0, inner > 0, let readingFontScale else { return contentWidth } + return columnWidth(inner: inner, cap: readingColumnCap(fontScale: readingFontScale)) + } /// `contentVerticalPadding`'s reading-mode counterpart: the top and /// bottom breathing room, doubled, because in reading mode nothing else /// occupies the top of the window. diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index d32322c..0063f4b 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -115,7 +115,8 @@ struct MarkdownView: View { @State private var appliedDefaultZoom: Double /// ⌘⇧R — distraction-free reading: both sidebars, the top-right chrome pills /// and the Support/Tip Jar button step out of the way, and the text column - /// stops at `Metrics.readingMaxContentWidth` and centres itself. + /// stops at `Metrics.readingColumnCap(fontScale:)` (720 pt at 100 %, scaled + /// with the zoom) and centres itself. /// /// @State, not @AppStorage, for the same reason as `fontScale`: it belongs to /// this window and this reading session. A viewer that reopens with its @@ -139,6 +140,8 @@ struct MarkdownView: View { /// on demand. A reference in @State on purpose: the coordinator fills it, /// nothing observes it, so scrolling never re-evaluates this body (E-D1). @State private var readingPosition = DocumentReadingPosition() + /// The list's column room, for a parse at a new zoom in reading mode. + @State private var documentColumn = DocumentColumn() /// Source Edit (v1.12): the raw-Markdown editor over the rendered list, its /// buffer, saving and the close guard's answers. One per window, lives with /// the tab. Publishes only rare events (enter / leave, dirty, banner) — the @@ -286,7 +289,7 @@ struct MarkdownView: View { focusedBlockId: focusedBlockId, focusedOccInBlock: focusedOccInBlock, scrollRequest: scrollRequest, - layoutStyle: isReadingMode ? .reading : .standard, + layoutStyle: isReadingMode ? .reading(fontScale: CGFloat(renderedFontScale)) : .standard, contentWidth: $contentWidth, onHeightReport: { blockId, row, height in applyHeightReport(blockId: blockId, row: row, height: height) @@ -305,7 +308,8 @@ struct MarkdownView: View { // the focus back while covered, and leaves the // accessibility tree meanwhile (set on the AppKit view). isCovered: graphicPreview != nil || editSession.isActive, - readingPosition: readingPosition + readingPosition: readingPosition, + column: documentColumn ) .onChange(of: scrollTrigger) { _ in scrollFocusedMatchIntoView() @@ -649,8 +653,15 @@ struct MarkdownView: View { let scale = CGFloat(fontScale) // The width the list will lay the blocks out at. 0 before the host's // first layout — then the heights task below picks it up as soon as - // the coordinator reports one. - let width = contentWidth + // the coordinator reports one. A new zoom in reading mode changes + // the column's cap with these blocks (the style is derived from + // `renderedFontScale`): measure at THAT column. Only a zoom: any + // other parse keeps the list's width, which the list keeps in step + // with the window itself. + let listWidth = contentWidth + let width = BlockLayout.Document.parseWidth( + contentWidth: listWidth, inner: documentColumn.innerWidth?() ?? 0, + readingFontScale: isReadingMode && Double(scale) != renderedFontScale ? scale : nil) // Keyed by the link, so valid across this re-parse. let imageSizes = heightCache.imageSizesSnapshot let out: ParsedAndMeasured = await Task.detached(priority: .userInitiated) { @@ -705,6 +716,10 @@ struct MarkdownView: View { cachedBlocks = blocks renderedFontScale = Double(scale) contentVersion += 1 + // The column this parse was measured at, in the same transaction + // as the cap that gives it — unless the list published another + // width meanwhile (then the heights task re-measures at that one). + if width != listWidth, contentWidth == listWidth { contentWidth = width } textBlockMeta = parsed.textMeta installedText = text // Inline `$…$` paragraphs and display-math rows were deferred by the diff --git a/QuickMD/QuickMD/SourceEditorController.swift b/QuickMD/QuickMD/SourceEditorController.swift index 37e494b..e5f1b2b 100644 --- a/QuickMD/QuickMD/SourceEditorController.swift +++ b/QuickMD/QuickMD/SourceEditorController.swift @@ -437,17 +437,19 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD /// The rendered document's column, reproduced so the text does not jump /// sideways when the mode toggles. `VirtualBlockList.currentContentWidth()` /// computes `inner = floor(clip) − 2 × contentHorizontalPadding` (32 pt) - /// and, in reading mode, `floor(min(inner, 720))`; its cells centre that + /// and, in reading mode, `floor(min(inner, cap))` with the cap at the + /// zoom (`BlockLayout.Document.columnWidth`, `readingColumnCap`, 720 pt at + /// 100 %); its cells centre that /// column in the `floor(clip)`-wide table column, i.e. the column starts /// at `(floor(clip) − column) / 2` — 32 pt exactly in the standard layout, /// never less in reading mode. The vertical gap is the list's visible gap /// above the first block (`LayoutStyle.verticalPadding`: 24 / 48 pt). - static func columnGeometry(clipWidth: CGFloat, isReadingLayout: Bool) -> ColumnGeometry { + static func columnGeometry(clipWidth: CGFloat, isReadingLayout: Bool, fontScale: CGFloat) -> ColumnGeometry { let available = floor(max(clipWidth, 0)) let inner = available - 2 * BlockLayout.Document.contentHorizontalPadding - let column = isReadingLayout - ? max(0, floor(min(inner, BlockLayout.Document.readingMaxContentWidth))) - : max(0, inner) + let column = BlockLayout.Document.columnWidth( + inner: inner, + cap: isReadingLayout ? BlockLayout.Document.readingColumnCap(fontScale: fontScale) : nil) let vertical = isReadingLayout ? BlockLayout.Document.readingContentVerticalPadding : BlockLayout.Document.contentVerticalPadding @@ -464,7 +466,8 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD isUpdatingGeometry = true defer { isUpdatingGeometry = false } let clip = scrollView.contentView.bounds.size - let geometry = Self.columnGeometry(clipWidth: clip.width, isReadingLayout: style.isReadingLayout) + let geometry = Self.columnGeometry(clipWidth: clip.width, isReadingLayout: style.isReadingLayout, + fontScale: CGFloat(style.fontScale)) let inset = NSSize(width: geometry.horizontalInset, height: geometry.verticalInset) // At least as tall as the clip, so a click below a short text still // lands in the text view (and places the caret at the end). diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 40f70aa..d8a471a 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -47,6 +47,18 @@ private let listLog = Logger(subsystem: "pl.falami.studio.QuickMD", category: "V private let listSignpost = OSSignposter(subsystem: "pl.falami.studio.QuickMD", category: "VirtualBlockList") #endif +/// The room the document column has, asked for on demand — the same bridge +/// as `DocumentReadingPosition`: `MarkdownView` owns one per tab (`@State`, +/// reference identity, so nothing re-renders), the list's coordinator installs +/// `innerWidth` on every update. A parse at a new zoom in reading mode reads +/// it to measure at the column the zoom's cap will give +/// (`BlockLayout.Document.parseWidth`). Main-thread by convention. +final class DocumentColumn { + /// The clip's whole points − 2 × the horizontal padding; 0 before the + /// list's first layout or while it is not wired. + var innerWidth: (() -> CGFloat)? +} + struct VirtualBlockList: NSViewRepresentable { /// Where a programmatic scroll parks the target row. @@ -86,8 +98,14 @@ struct VirtualBlockList: NSViewRepresentable { static let standard = LayoutStyle(maxContentWidth: nil, verticalPadding: Metrics.contentVerticalPadding) - static let reading = LayoutStyle(maxContentWidth: Metrics.readingMaxContentWidth, - verticalPadding: Metrics.readingContentVerticalPadding) + /// Reading mode at `fontScale`: the column cap follows the zoom + /// (`BlockLayout.Document.readingColumnCap`), so a zoom in reading mode + /// is a style change too — together with the parse that installs the + /// zoomed blocks (`MarkdownView` derives it from `renderedFontScale`). + static func reading(fontScale: CGFloat) -> LayoutStyle { + LayoutStyle(maxContentWidth: BlockLayout.Document.readingColumnCap(fontScale: fontScale), + verticalPadding: Metrics.readingContentVerticalPadding) + } /// The scroll view's top/bottom content inset for this style: exactly /// `verticalPadding`. The table has no intercell spacing (the space @@ -152,6 +170,8 @@ struct VirtualBlockList: NSViewRepresentable { /// ⌘E's reading-position handle (v1.11 E-D1). The coordinator installs a /// provider that is evaluated only when asked — no per-scroll publishing. var readingPosition: DocumentReadingPosition? + /// Answers "how wide is the column's room" on demand (`DocumentColumn`). + var column: DocumentColumn? typealias Metrics = BlockLayout.Document @@ -261,6 +281,7 @@ struct VirtualBlockList: NSViewRepresentable { // has the property set at all. if nsView.isAccessibilityHidden() != isCovered { nsView.setAccessibilityHidden(isCovered) } readingPosition?.provider = { [weak coordinator] in coordinator?.readingEditorLine() } + column?.innerWidth = { [weak coordinator] in coordinator?.columnInnerWidth() ?? 0 } readingPosition?.selectionProvider = { [weak coordinator] in guard let coordinator, let hint = coordinator.selection.sourceEditHint( @@ -418,6 +439,9 @@ struct VirtualBlockList: NSViewRepresentable { /// Last width handed to the parent. 0 = never reported, which is the one /// case that skips the debounce (the height table is blocked on it). private var reportedContentWidth: CGFloat = 0 + /// Published widths the parent's binding has not received yet + /// (`publish` hands each over one run-loop turn later). + private var pendingPublishes = 0 private var widthWork: DispatchWorkItem? private var frameObserver: NSObjectProtocol? /// `updateNSView` has run at least once, so `setContentWidth` is the @@ -1093,11 +1117,18 @@ struct VirtualBlockList: NSViewRepresentable { /// and `rootView(for:)` expresses the same `min` as a SwiftUI frame — so /// layout width == measurement width by construction, at any clip width. private func currentContentWidth() -> CGFloat { + let inner = columnInnerWidth() + guard inner != 0 else { return 0 } + return BlockLayout.Document.columnWidth(inner: inner, cap: layoutStyle.maxContentWidth) + } + + /// The room the column has before any cap: the clip's whole points + /// minus the horizontal padding on both sides; 0 before the first + /// layout. + func columnInnerWidth() -> CGFloat { let available = quantizedClipWidth() guard available > 0 else { return 0 } - let inner = available - 2 * Metrics.contentHorizontalPadding - guard let cap = layoutStyle.maxContentWidth else { return max(0, inner) } - return max(0, floor(min(inner, cap))) + return available - 2 * Metrics.contentHorizontalPadding } // MARK: - Layout style (reading mode) @@ -1208,6 +1239,17 @@ struct VirtualBlockList: NSViewRepresentable { func syncWidthIfNeeded(parentValue: CGFloat) { isWired = true syncColumnWidth() + if reportedContentWidth > 0, parentValue > 0, pendingPublishes == 0, + abs(parentValue - reportedContentWidth) >= 1 { + // The parent measured a parse at a width it worked out itself + // (a zoom in reading mode, `BlockLayout.Document.parseWidth`): + // that is the width its table has now. Take it as reported, and + // report the real column if the window moved meanwhile. + reportedContentWidth = parentValue + let width = currentContentWidth() + if width > 0, abs(width - parentValue) >= 1 { scheduleWidthReport() } + return + } guard reportedContentWidth == 0 else { return } let width = currentContentWidth() guard width > 0, !isLiveResizing else { return } @@ -1252,7 +1294,11 @@ struct VirtualBlockList: NSViewRepresentable { listLog.debug("contentWidth: \(width, format: .fixed(precision: 1))") #endif // Never inside an AppKit layout pass — SwiftUI state, one turn later. - DispatchQueue.main.async { [weak self] in self?.setContentWidth(width) } + pendingPublishes += 1 + DispatchQueue.main.async { [weak self] in + self?.setContentWidth(width) + self?.pendingPublishes -= 1 + } } // MARK: - Cell content diff --git a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift index 2edf212..99b0fbe 100644 --- a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift +++ b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift @@ -573,37 +573,50 @@ final class SourceEditorControllerTests: XCTestCase { } /// The list's arithmetic (`VirtualBlockList.currentContentWidth()` and its - /// centred cell column), spelled out independently. - private func listColumn(clip: CGFloat, reading: Bool) -> (left: CGFloat, width: CGFloat) { + /// centred cell column), spelled out independently: reading mode caps + /// the column at 720 pt × the zoom, in whole points. + private func listColumn(clip: CGFloat, reading: Bool, scale: CGFloat) -> (left: CGFloat, width: CGFloat) { let available = floor(clip) let inner = available - 2 * 32 - let width = reading ? max(0, floor(min(inner, 720))) : max(0, inner) + let width = reading ? max(0, floor(min(inner, (720 * scale).rounded()))) : max(0, inner) return (32 + (inner - width) / 2, width) } func testColumnGeometryMatchesTheRenderedList() { - for clip in [300, 640, 783.5, 800, 1000.6, 1600] as [CGFloat] { + for clip in [300, 640, 783.5, 800, 1000.6, 1600, 2400] as [CGFloat] { for reading in [false, true] { - let geometry = SourceEditorController.columnGeometry(clipWidth: clip, isReadingLayout: reading) - let list = listColumn(clip: clip, reading: reading) - XCTAssertEqual(geometry.columnWidth, list.width, "clip \(clip) reading \(reading)") - XCTAssertEqual(geometry.horizontalInset, list.left, "clip \(clip) reading \(reading)") - XCTAssertGreaterThanOrEqual(geometry.horizontalInset, 32) - XCTAssertEqual(geometry.verticalInset, reading ? 48 : 24) + for scale: CGFloat in [0.5, 1, 1.5, 2, 3] { + let geometry = SourceEditorController.columnGeometry(clipWidth: clip, isReadingLayout: reading, + fontScale: scale) + let list = listColumn(clip: clip, reading: reading, scale: scale) + let label = "clip \(clip) reading \(reading) × \(scale)" + XCTAssertEqual(geometry.columnWidth, list.width, label) + XCTAssertEqual(geometry.horizontalInset, list.left, label) + XCTAssertGreaterThanOrEqual(geometry.horizontalInset, 32) + XCTAssertEqual(geometry.verticalInset, reading ? 48 : 24) + // The ONE definition the list uses. + XCTAssertEqual(geometry.columnWidth, BlockLayout.Document.columnWidth( + inner: floor(clip) - 64, + cap: reading ? BlockLayout.Document.readingColumnCap(fontScale: scale) : nil), label) + } } } - let standard = SourceEditorController.columnGeometry(clipWidth: 1000, isReadingLayout: false) + let standard = SourceEditorController.columnGeometry(clipWidth: 1000, isReadingLayout: false, fontScale: 1) XCTAssertEqual(standard, .init(horizontalInset: 32, columnWidth: 936, verticalInset: 24)) - let reading = SourceEditorController.columnGeometry(clipWidth: 1000, isReadingLayout: true) + let reading = SourceEditorController.columnGeometry(clipWidth: 1000, isReadingLayout: true, fontScale: 1) XCTAssertEqual(reading, .init(horizontalInset: 140, columnWidth: 720, verticalInset: 48)) + let zoomed = SourceEditorController.columnGeometry(clipWidth: 1600, isReadingLayout: true, fontScale: 2) + XCTAssertEqual(zoomed, .init(horizontalInset: 80, columnWidth: 1440, verticalInset: 48)) + let narrow = SourceEditorController.columnGeometry(clipWidth: 1000, isReadingLayout: true, fontScale: 0.5) + XCTAssertEqual(narrow, .init(horizontalInset: 320, columnWidth: 360, verticalInset: 48)) } func testTextContainerFollowsTheViewWidthAndTheLayoutStyle() { let (controller, scrollView, window) = makeEditor("text", width: 1000) let view = controller.textView - func check(reading: Bool, line: UInt = #line) { + func check(reading: Bool, scale: CGFloat = 1, line: UInt = #line) { let clip = scrollView.contentView.bounds.width - let list = listColumn(clip: clip, reading: reading) + let list = listColumn(clip: clip, reading: reading, scale: scale) XCTAssertEqual(view.textContainer!.size.width, list.width, line: line) XCTAssertEqual(view.textContainerInset.width, list.left, line: line) XCTAssertEqual(view.textContainerInset.height, reading ? 48 : 24, line: line) @@ -615,6 +628,12 @@ final class SourceEditorControllerTests: XCTestCase { window.setContentSize(NSSize(width: 1400, height: 600)) window.contentView!.layoutSubtreeIfNeeded() check(reading: true) + // A zoom in reading mode widens the column with the text (1.5 × 720 + // = 1080 in a 1400 pt window), as the rendered list does. + controller.apply(style: style(scale: 1.5, reading: true)) + check(reading: true, scale: 1.5) + controller.apply(style: style(scale: 0.5, reading: true)) + check(reading: true, scale: 0.5) controller.apply(style: style(reading: false)) check(reading: false) window.setContentSize(NSSize(width: 501, height: 600)) @@ -665,7 +684,7 @@ final class SourceEditorControllerTests: XCTestCase { func testReadingModeInsetChangeDoesNotShiftTheText() { let (controller, scrollView, _) = makeEditor(wrappingLines(2_000), width: 700) XCTAssertEqual(SourceEditorController.columnGeometry(clipWidth: scrollView.contentView.bounds.width, - isReadingLayout: true).columnWidth, + isReadingLayout: true, fontScale: 1).columnWidth, controller.textView.textContainer!.size.width, "same column in both layouts") controller.scroll(toLine: 1_200) let storage = controller.textView.textStorage!.mutableString diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 9c88de5..6b9e069 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -1224,6 +1224,68 @@ final class VerticalRhythmTests: XCTestCase { } } + // MARK: - Reading column follows the zoom + + /// Reading mode's column is 720 pt at 100 % and scales with the text, in + /// whole points; the window still caps it. 1.14.4 kept 720 at every zoom: + /// ~45 characters a line at 200 %. + func testTheReadingColumnScalesWithTheZoom() { + typealias Document = BlockLayout.Document + XCTAssertEqual(Document.readingColumnCap(fontScale: 1), 720) + XCTAssertEqual(Document.readingColumnCap(fontScale: 2), 1440) + XCTAssertEqual(Document.readingColumnCap(fontScale: 0.5), 360) + XCTAssertEqual(Document.readingColumnCap(fontScale: 3), 2160) + XCTAssertEqual(Document.readingColumnCap(fontScale: 1.1), 792) // 792.0000000000001 + // 200 %: the full 1440 where the window has room, the window otherwise. + XCTAssertEqual(Document.columnWidth(inner: 1600 - 64, cap: Document.readingColumnCap(fontScale: 2)), 1440) + XCTAssertEqual(Document.columnWidth(inner: 1000 - 64, cap: Document.readingColumnCap(fontScale: 2)), 936) + XCTAssertEqual(Document.columnWidth(inner: 1000 - 64, cap: Document.readingColumnCap(fontScale: 1)), 720) + XCTAssertEqual(Document.columnWidth(inner: 935.5, cap: nil), 935.5) + XCTAssertEqual(Document.columnWidth(inner: -10, cap: 720), 0) + } + + /// A zoom in reading mode re-parses; the parse is measured at the column + /// the zoom's cap gives — the width the list publishes once the parse is + /// installed with that cap — so the zoomed text is laid out at the width + /// its heights were measured at, measured once per zoom. + func testAZoomInReadingModeMeasuresItsParseAtTheNewColumn() { + typealias Document = BlockLayout.Document + let blocks = parse(String(repeating: "A line of prose that wraps in every column it is given. ", count: 12)) + for inner: CGFloat in [1600 - 64, 1000 - 64] { + for scale: CGFloat in [0.5, 1, 1.5, 2, 3] { + let column = Document.columnWidth(inner: inner, cap: Document.readingColumnCap(fontScale: scale)) + let width = Document.parseWidth(contentWidth: 720, inner: inner, readingFontScale: scale) + XCTAssertEqual(width, column, "inner \(inner) × \(scale)") + XCTAssertEqual(measure(blocks, fontScale: scale, width: width).table.contentWidth, column) + } + } + // The standard layout: the list's width, whatever the zoom. + XCTAssertEqual(Document.parseWidth(contentWidth: 936, inner: 936, readingFontScale: nil), 936) + // No layout yet, or the list not wired: nothing to derive from. + XCTAssertEqual(Document.parseWidth(contentWidth: 0, inner: 1536, readingFontScale: 2), 0) + XCTAssertEqual(Document.parseWidth(contentWidth: 720, inner: 0, readingFontScale: 2), 720) + } + + /// ⌘+ ⌘+ ⌘− ⌘− in reading mode with the viewport top inside the image: + /// every zoom is a style change (the cap) AND a new table, in one + /// update — the reader comes back to the very same point, as in the + /// standard layout. + func testAZoomRoundTripInReadingModeKeepsAReaderInsideAnImage() { + var rows = fixtureRows(wrap: 2, imageReal: 576) + for index in rows.indices { rows[index].height = rows[index].real } // every view reported + var list = ListModel(rows: rows, contentTop: 0) + list.inset = 48 + list.contentTop = list.blockTop(image) + 520 // its bottom 56 pt on screen + let start = list.contentTop + XCTAssertEqual(list.rowUnderContentTop(), image) + for scale: CGFloat in [1.1, 1.25, 1.1, 1] { + list.toggleStyle(inset: 48) + list.install(fixtureRows(scale: scale, wrap: 2, imageReal: min(655, 576 * scale))) + XCTAssertEqual(list.rowUnderContentTop(), image, "× \(scale)") + } + XCTAssertEqual(list.contentTop, start, accuracy: 0.001) + } + /// Reading Mode on and off with the viewport top inside the image (the /// debug log: `row 21 + 579.5` on the way in, `row 29 + 0.0` on the way /// out). At 720 pt the image is smaller and the short paragraphs wrap. From c3186c2ae8519e3fef8eb1168f63af3508a74151 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:20:30 +0100 Subject: [PATCH 047/105] fix(text): decode HTML character references in all inline text, as GitHub does AT&T, ©,   and → showed as written outside headings (B5 covered headings with a 7-entity decoder). HTMLEntities decodes the CommonMark way: every HTML5 named reference ending in ';' (HTMLEntityTable, generated from Python's html.entities.html5; the command is in its header) and decimal/hex numeric references, 0 / surrogates / > U+10FFFF becoming U+FFFD. The renderer applies it after inline parsing to plain runs, emphasis, strikethrough, link text and image alt (so * is a literal *), never to code spans or code blocks; block image captions are decoded in the parser. Screen, measurer, search, copy and print all use these strings. Front-matter values keep references as written (GitHub renders front matter as plain text). The heading path and the Contents sidebar switch to the full decoder. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 12 + QuickMD/QuickMD/HTMLEntities.swift | 96 + QuickMD/QuickMD/HTMLEntityTable.swift | 2144 +++++++++++++++++ QuickMD/QuickMD/MarkdownBlockParser.swift | 5 +- QuickMD/QuickMD/MarkdownRenderer.swift | 46 +- QuickMD/QuickMD/TableTextBuilder.swift | 6 +- .../QuickMD/Views/TableOfContentsView.swift | 2 +- QuickMD/QuickMDTests/RendererTests.swift | 80 + QuickMD/QuickMDTests/SearchTests.swift | 7 +- 9 files changed, 2371 insertions(+), 27 deletions(-) create mode 100644 QuickMD/QuickMD/HTMLEntities.swift create mode 100644 QuickMD/QuickMD/HTMLEntityTable.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 4c4ad5f..35433e7 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -48,6 +48,10 @@ A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000303 /* CrossDocumentAnchors.swift */; }; A1B2C3D401000305 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000305 /* SettingsTab.swift */; }; A1B2C3D401000306 /* SettingsTab.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000305 /* SettingsTab.swift */; }; + A1B2C3D401000309 /* HTMLEntityTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000309 /* HTMLEntityTable.swift */; }; + A1B2C3D40100030A /* HTMLEntityTable.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000309 /* HTMLEntityTable.swift */; }; + A1B2C3D401000307 /* HTMLEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000307 /* HTMLEntities.swift */; }; + A1B2C3D401000308 /* HTMLEntities.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000307 /* HTMLEntities.swift */; }; A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C1 /* TableSelectionTests.swift */; }; A1B2C3D40100000E /* ImageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000012 /* ImageBlockView.swift */; }; A1B2C3D40100000F /* BlockquoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000013 /* BlockquoteView.swift */; }; @@ -244,6 +248,8 @@ A1B2C3D400000301 /* ReferenceLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceLinks.swift; sourceTree = ""; }; A1B2C3D400000303 /* CrossDocumentAnchors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDocumentAnchors.swift; sourceTree = ""; }; A1B2C3D400000305 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; + A1B2C3D400000309 /* HTMLEntityTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLEntityTable.swift; sourceTree = ""; }; + A1B2C3D400000307 /* HTMLEntities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLEntities.swift; sourceTree = ""; }; A1B2C3D4000001C1 /* TableSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSelectionTests.swift; sourceTree = ""; }; A1B2C3D400000012 /* ImageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBlockView.swift; sourceTree = ""; }; A1B2C3D400000013 /* BlockquoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockquoteView.swift; sourceTree = ""; }; @@ -399,6 +405,8 @@ A1B2C3D400000301 /* ReferenceLinks.swift */, A1B2C3D400000303 /* CrossDocumentAnchors.swift */, A1B2C3D400000305 /* SettingsTab.swift */, + A1B2C3D400000309 /* HTMLEntityTable.swift */, + A1B2C3D400000307 /* HTMLEntities.swift */, A1B2C3D400000110 /* DocumentReadingPosition.swift */, A1B2C3D400000120 /* DocumentFileFormat.swift */, A1B2C3D400000121 /* DocumentFileWriter.swift */, @@ -700,6 +708,8 @@ A1B2C3D401000301 /* ReferenceLinks.swift in Sources */, A1B2C3D401000303 /* CrossDocumentAnchors.swift in Sources */, A1B2C3D401000305 /* SettingsTab.swift in Sources */, + A1B2C3D401000309 /* HTMLEntityTable.swift in Sources */, + A1B2C3D401000307 /* HTMLEntities.swift in Sources */, A1B2C3D401000110 /* DocumentReadingPosition.swift in Sources */, A1B2C3D401000120 /* DocumentFileFormat.swift in Sources */, A1B2C3D401000122 /* DocumentFileWriter.swift in Sources */, @@ -757,6 +767,8 @@ A1B2C3D401000302 /* ReferenceLinks.swift in Sources (Tests) */, A1B2C3D401000304 /* CrossDocumentAnchors.swift in Sources (Tests) */, A1B2C3D401000306 /* SettingsTab.swift in Sources (Tests) */, + A1B2C3D40100030A /* HTMLEntityTable.swift in Sources (Tests) */, + A1B2C3D401000308 /* HTMLEntities.swift in Sources (Tests) */, A1B2C3D4010000C2 /* DocumentSelectionTests.swift in Sources */, A1B2C3D401000111 /* DocumentReadingPosition.swift in Sources (Tests) */, A1B2C3D401000112 /* ExternalEditorLineTests.swift in Sources */, diff --git a/QuickMD/QuickMD/HTMLEntities.swift b/QuickMD/QuickMD/HTMLEntities.swift new file mode 100644 index 0000000..b3085d4 --- /dev/null +++ b/QuickMD/QuickMD/HTMLEntities.swift @@ -0,0 +1,96 @@ +import Foundation + +// MARK: - HTML character references in Markdown text + +/// Decodes HTML character references the way CommonMark (and so GitHub) does +/// in inline text: `AT&T` → "AT&T", `©` → "©", `→` → "→". +/// +/// - Named: `&name;` for every HTML5 entity (`HTMLEntityTable`, generated); +/// the semicolon is required (`&` stays as written), unknown names stay. +/// - Decimal `&#N;` (1–7 digits) and hex `&#xH;` / `&#XH;` (1–6 digits); a +/// code point of 0, a surrogate or one above U+10FFFF becomes U+FFFD. +/// +/// The renderer applies it to text AFTER inline parsing (`*` is a literal +/// `*`, never emphasis) and never to code spans or code blocks. Pure and +/// thread-safe: the parse task, the measurer and the main thread all call it. +enum HTMLEntities { + + /// Name (without `&` and `;`) → replacement, built once from the table. + private static let named: [String: String] = { + var table: [String: String] = [:] + table.reserveCapacity(2_200) + for line in HTMLEntityTable.source.split(separator: "\n") { + guard let colon = line.firstIndex(of: ":") else { continue } + var value = String.UnicodeScalarView() + for hex in line[line.index(after: colon)...].split(separator: ",") { + if let code = UInt32(hex, radix: 16), let scalar = Unicode.Scalar(code) { value.append(scalar) } + } + table[String(line[.. String { + guard text.utf8.contains(UInt8(ascii: "&")) else { return text } + let scalars = text.unicodeScalars + var out = String.UnicodeScalarView() + var index = scalars.startIndex + while index < scalars.endIndex { + if scalars[index] == "&", let (replacement, next) = reference(in: scalars, at: index) { + out.append(contentsOf: replacement.unicodeScalars) + index = next + } else { + out.append(scalars[index]) + index = scalars.index(after: index) + } + } + return String(out) + } + + /// The reference at `start` (an `&`): its replacement and the index past + /// its `;`, or nil when the text there is not a valid reference. + private static func reference(in scalars: String.UnicodeScalarView, + at start: String.Index) -> (String, String.Index)? { + var index = scalars.index(after: start) + guard index < scalars.endIndex else { return nil } + + if scalars[index] == "#" { + index = scalars.index(after: index) + guard index < scalars.endIndex else { return nil } + let hex = scalars[index] == "x" || scalars[index] == "X" + if hex { index = scalars.index(after: index) } + var digits = "" + while index < scalars.endIndex, digits.unicodeScalars.count <= (hex ? 6 : 7) { + let scalar = scalars[index] + let isDigit = hex ? scalar.properties.isASCIIHexDigit : ("0"..."9").contains(scalar) + guard isDigit else { break } + digits.unicodeScalars.append(scalar) + index = scalars.index(after: index) + } + guard !digits.isEmpty, digits.unicodeScalars.count <= (hex ? 6 : 7), + index < scalars.endIndex, scalars[index] == ";", + let value = UInt32(digits, radix: hex ? 16 : 10) else { return nil } + let scalar = value == 0 ? nil : Unicode.Scalar(value) // nil for surrogates and > U+10FFFF + return (String(scalar ?? "\u{FFFD}"), scalars.index(after: index)) + } + + var name = "" + while index < scalars.endIndex, name.unicodeScalars.count <= maxNameLength { + let scalar = scalars[index] + if scalar == ";" { + guard let value = named[name] else { return nil } + return (value, scalars.index(after: index)) + } + guard scalar.isASCII, scalar.properties.isAlphabetic || ("0"..."9").contains(scalar) else { return nil } + name.unicodeScalars.append(scalar) + index = scalars.index(after: index) + } + return nil + } +} diff --git a/QuickMD/QuickMD/HTMLEntityTable.swift b/QuickMD/QuickMD/HTMLEntityTable.swift new file mode 100644 index 0000000..c5a480c --- /dev/null +++ b/QuickMD/QuickMD/HTMLEntityTable.swift @@ -0,0 +1,2144 @@ +// +// HTMLEntityTable.swift +// QuickMD +// +// GENERATED — do not edit by hand. Every HTML5 named character reference +// that ends in ";" (the only form CommonMark recognises), one per line as +// "name:HEX[,HEX]" (the code points it stands for). The lines between the +// quotes are exactly the output of (Python 3.12.10, stdlib only): +// +// python3 -I -c 'import html.entities as h; [print(k[:-1] + ":" + ",".join("%X" % ord(c) for c in v)) for k, v in sorted(h.html5.items()) if k.endswith(";")]' +// +// A single string parsed once on first use (`HTMLEntities`) compiles in a +// fraction of the time a 2,000-entry dictionary literal takes. +// + +enum HTMLEntityTable { + static let source = """ +AElig:C6 +AMP:26 +Aacute:C1 +Abreve:102 +Acirc:C2 +Acy:410 +Afr:1D504 +Agrave:C0 +Alpha:391 +Amacr:100 +And:2A53 +Aogon:104 +Aopf:1D538 +ApplyFunction:2061 +Aring:C5 +Ascr:1D49C +Assign:2254 +Atilde:C3 +Auml:C4 +Backslash:2216 +Barv:2AE7 +Barwed:2306 +Bcy:411 +Because:2235 +Bernoullis:212C +Beta:392 +Bfr:1D505 +Bopf:1D539 +Breve:2D8 +Bscr:212C +Bumpeq:224E +CHcy:427 +COPY:A9 +Cacute:106 +Cap:22D2 +CapitalDifferentialD:2145 +Cayleys:212D +Ccaron:10C +Ccedil:C7 +Ccirc:108 +Cconint:2230 +Cdot:10A +Cedilla:B8 +CenterDot:B7 +Cfr:212D +Chi:3A7 +CircleDot:2299 +CircleMinus:2296 +CirclePlus:2295 +CircleTimes:2297 +ClockwiseContourIntegral:2232 +CloseCurlyDoubleQuote:201D +CloseCurlyQuote:2019 +Colon:2237 +Colone:2A74 +Congruent:2261 +Conint:222F +ContourIntegral:222E +Copf:2102 +Coproduct:2210 +CounterClockwiseContourIntegral:2233 +Cross:2A2F +Cscr:1D49E +Cup:22D3 +CupCap:224D +DD:2145 +DDotrahd:2911 +DJcy:402 +DScy:405 +DZcy:40F +Dagger:2021 +Darr:21A1 +Dashv:2AE4 +Dcaron:10E +Dcy:414 +Del:2207 +Delta:394 +Dfr:1D507 +DiacriticalAcute:B4 +DiacriticalDot:2D9 +DiacriticalDoubleAcute:2DD +DiacriticalGrave:60 +DiacriticalTilde:2DC +Diamond:22C4 +DifferentialD:2146 +Dopf:1D53B +Dot:A8 +DotDot:20DC +DotEqual:2250 +DoubleContourIntegral:222F +DoubleDot:A8 +DoubleDownArrow:21D3 +DoubleLeftArrow:21D0 +DoubleLeftRightArrow:21D4 +DoubleLeftTee:2AE4 +DoubleLongLeftArrow:27F8 +DoubleLongLeftRightArrow:27FA +DoubleLongRightArrow:27F9 +DoubleRightArrow:21D2 +DoubleRightTee:22A8 +DoubleUpArrow:21D1 +DoubleUpDownArrow:21D5 +DoubleVerticalBar:2225 +DownArrow:2193 +DownArrowBar:2913 +DownArrowUpArrow:21F5 +DownBreve:311 +DownLeftRightVector:2950 +DownLeftTeeVector:295E +DownLeftVector:21BD +DownLeftVectorBar:2956 +DownRightTeeVector:295F +DownRightVector:21C1 +DownRightVectorBar:2957 +DownTee:22A4 +DownTeeArrow:21A7 +Downarrow:21D3 +Dscr:1D49F +Dstrok:110 +ENG:14A +ETH:D0 +Eacute:C9 +Ecaron:11A +Ecirc:CA +Ecy:42D +Edot:116 +Efr:1D508 +Egrave:C8 +Element:2208 +Emacr:112 +EmptySmallSquare:25FB +EmptyVerySmallSquare:25AB +Eogon:118 +Eopf:1D53C +Epsilon:395 +Equal:2A75 +EqualTilde:2242 +Equilibrium:21CC +Escr:2130 +Esim:2A73 +Eta:397 +Euml:CB +Exists:2203 +ExponentialE:2147 +Fcy:424 +Ffr:1D509 +FilledSmallSquare:25FC +FilledVerySmallSquare:25AA +Fopf:1D53D +ForAll:2200 +Fouriertrf:2131 +Fscr:2131 +GJcy:403 +GT:3E +Gamma:393 +Gammad:3DC +Gbreve:11E +Gcedil:122 +Gcirc:11C +Gcy:413 +Gdot:120 +Gfr:1D50A +Gg:22D9 +Gopf:1D53E +GreaterEqual:2265 +GreaterEqualLess:22DB +GreaterFullEqual:2267 +GreaterGreater:2AA2 +GreaterLess:2277 +GreaterSlantEqual:2A7E +GreaterTilde:2273 +Gscr:1D4A2 +Gt:226B +HARDcy:42A +Hacek:2C7 +Hat:5E +Hcirc:124 +Hfr:210C +HilbertSpace:210B +Hopf:210D +HorizontalLine:2500 +Hscr:210B +Hstrok:126 +HumpDownHump:224E +HumpEqual:224F +IEcy:415 +IJlig:132 +IOcy:401 +Iacute:CD +Icirc:CE +Icy:418 +Idot:130 +Ifr:2111 +Igrave:CC +Im:2111 +Imacr:12A +ImaginaryI:2148 +Implies:21D2 +Int:222C +Integral:222B +Intersection:22C2 +InvisibleComma:2063 +InvisibleTimes:2062 +Iogon:12E +Iopf:1D540 +Iota:399 +Iscr:2110 +Itilde:128 +Iukcy:406 +Iuml:CF +Jcirc:134 +Jcy:419 +Jfr:1D50D +Jopf:1D541 +Jscr:1D4A5 +Jsercy:408 +Jukcy:404 +KHcy:425 +KJcy:40C +Kappa:39A +Kcedil:136 +Kcy:41A +Kfr:1D50E +Kopf:1D542 +Kscr:1D4A6 +LJcy:409 +LT:3C +Lacute:139 +Lambda:39B +Lang:27EA +Laplacetrf:2112 +Larr:219E +Lcaron:13D +Lcedil:13B +Lcy:41B +LeftAngleBracket:27E8 +LeftArrow:2190 +LeftArrowBar:21E4 +LeftArrowRightArrow:21C6 +LeftCeiling:2308 +LeftDoubleBracket:27E6 +LeftDownTeeVector:2961 +LeftDownVector:21C3 +LeftDownVectorBar:2959 +LeftFloor:230A +LeftRightArrow:2194 +LeftRightVector:294E +LeftTee:22A3 +LeftTeeArrow:21A4 +LeftTeeVector:295A +LeftTriangle:22B2 +LeftTriangleBar:29CF +LeftTriangleEqual:22B4 +LeftUpDownVector:2951 +LeftUpTeeVector:2960 +LeftUpVector:21BF +LeftUpVectorBar:2958 +LeftVector:21BC +LeftVectorBar:2952 +Leftarrow:21D0 +Leftrightarrow:21D4 +LessEqualGreater:22DA +LessFullEqual:2266 +LessGreater:2276 +LessLess:2AA1 +LessSlantEqual:2A7D +LessTilde:2272 +Lfr:1D50F +Ll:22D8 +Lleftarrow:21DA +Lmidot:13F +LongLeftArrow:27F5 +LongLeftRightArrow:27F7 +LongRightArrow:27F6 +Longleftarrow:27F8 +Longleftrightarrow:27FA +Longrightarrow:27F9 +Lopf:1D543 +LowerLeftArrow:2199 +LowerRightArrow:2198 +Lscr:2112 +Lsh:21B0 +Lstrok:141 +Lt:226A +Map:2905 +Mcy:41C +MediumSpace:205F +Mellintrf:2133 +Mfr:1D510 +MinusPlus:2213 +Mopf:1D544 +Mscr:2133 +Mu:39C +NJcy:40A +Nacute:143 +Ncaron:147 +Ncedil:145 +Ncy:41D +NegativeMediumSpace:200B +NegativeThickSpace:200B +NegativeThinSpace:200B +NegativeVeryThinSpace:200B +NestedGreaterGreater:226B +NestedLessLess:226A +NewLine:A +Nfr:1D511 +NoBreak:2060 +NonBreakingSpace:A0 +Nopf:2115 +Not:2AEC +NotCongruent:2262 +NotCupCap:226D +NotDoubleVerticalBar:2226 +NotElement:2209 +NotEqual:2260 +NotEqualTilde:2242,338 +NotExists:2204 +NotGreater:226F +NotGreaterEqual:2271 +NotGreaterFullEqual:2267,338 +NotGreaterGreater:226B,338 +NotGreaterLess:2279 +NotGreaterSlantEqual:2A7E,338 +NotGreaterTilde:2275 +NotHumpDownHump:224E,338 +NotHumpEqual:224F,338 +NotLeftTriangle:22EA +NotLeftTriangleBar:29CF,338 +NotLeftTriangleEqual:22EC +NotLess:226E +NotLessEqual:2270 +NotLessGreater:2278 +NotLessLess:226A,338 +NotLessSlantEqual:2A7D,338 +NotLessTilde:2274 +NotNestedGreaterGreater:2AA2,338 +NotNestedLessLess:2AA1,338 +NotPrecedes:2280 +NotPrecedesEqual:2AAF,338 +NotPrecedesSlantEqual:22E0 +NotReverseElement:220C +NotRightTriangle:22EB +NotRightTriangleBar:29D0,338 +NotRightTriangleEqual:22ED +NotSquareSubset:228F,338 +NotSquareSubsetEqual:22E2 +NotSquareSuperset:2290,338 +NotSquareSupersetEqual:22E3 +NotSubset:2282,20D2 +NotSubsetEqual:2288 +NotSucceeds:2281 +NotSucceedsEqual:2AB0,338 +NotSucceedsSlantEqual:22E1 +NotSucceedsTilde:227F,338 +NotSuperset:2283,20D2 +NotSupersetEqual:2289 +NotTilde:2241 +NotTildeEqual:2244 +NotTildeFullEqual:2247 +NotTildeTilde:2249 +NotVerticalBar:2224 +Nscr:1D4A9 +Ntilde:D1 +Nu:39D +OElig:152 +Oacute:D3 +Ocirc:D4 +Ocy:41E +Odblac:150 +Ofr:1D512 +Ograve:D2 +Omacr:14C +Omega:3A9 +Omicron:39F +Oopf:1D546 +OpenCurlyDoubleQuote:201C +OpenCurlyQuote:2018 +Or:2A54 +Oscr:1D4AA +Oslash:D8 +Otilde:D5 +Otimes:2A37 +Ouml:D6 +OverBar:203E +OverBrace:23DE +OverBracket:23B4 +OverParenthesis:23DC +PartialD:2202 +Pcy:41F +Pfr:1D513 +Phi:3A6 +Pi:3A0 +PlusMinus:B1 +Poincareplane:210C +Popf:2119 +Pr:2ABB +Precedes:227A +PrecedesEqual:2AAF +PrecedesSlantEqual:227C +PrecedesTilde:227E +Prime:2033 +Product:220F +Proportion:2237 +Proportional:221D +Pscr:1D4AB +Psi:3A8 +QUOT:22 +Qfr:1D514 +Qopf:211A +Qscr:1D4AC +RBarr:2910 +REG:AE +Racute:154 +Rang:27EB +Rarr:21A0 +Rarrtl:2916 +Rcaron:158 +Rcedil:156 +Rcy:420 +Re:211C +ReverseElement:220B +ReverseEquilibrium:21CB +ReverseUpEquilibrium:296F +Rfr:211C +Rho:3A1 +RightAngleBracket:27E9 +RightArrow:2192 +RightArrowBar:21E5 +RightArrowLeftArrow:21C4 +RightCeiling:2309 +RightDoubleBracket:27E7 +RightDownTeeVector:295D +RightDownVector:21C2 +RightDownVectorBar:2955 +RightFloor:230B +RightTee:22A2 +RightTeeArrow:21A6 +RightTeeVector:295B +RightTriangle:22B3 +RightTriangleBar:29D0 +RightTriangleEqual:22B5 +RightUpDownVector:294F +RightUpTeeVector:295C +RightUpVector:21BE +RightUpVectorBar:2954 +RightVector:21C0 +RightVectorBar:2953 +Rightarrow:21D2 +Ropf:211D +RoundImplies:2970 +Rrightarrow:21DB +Rscr:211B +Rsh:21B1 +RuleDelayed:29F4 +SHCHcy:429 +SHcy:428 +SOFTcy:42C +Sacute:15A +Sc:2ABC +Scaron:160 +Scedil:15E +Scirc:15C +Scy:421 +Sfr:1D516 +ShortDownArrow:2193 +ShortLeftArrow:2190 +ShortRightArrow:2192 +ShortUpArrow:2191 +Sigma:3A3 +SmallCircle:2218 +Sopf:1D54A +Sqrt:221A +Square:25A1 +SquareIntersection:2293 +SquareSubset:228F +SquareSubsetEqual:2291 +SquareSuperset:2290 +SquareSupersetEqual:2292 +SquareUnion:2294 +Sscr:1D4AE +Star:22C6 +Sub:22D0 +Subset:22D0 +SubsetEqual:2286 +Succeeds:227B +SucceedsEqual:2AB0 +SucceedsSlantEqual:227D +SucceedsTilde:227F +SuchThat:220B +Sum:2211 +Sup:22D1 +Superset:2283 +SupersetEqual:2287 +Supset:22D1 +THORN:DE +TRADE:2122 +TSHcy:40B +TScy:426 +Tab:9 +Tau:3A4 +Tcaron:164 +Tcedil:162 +Tcy:422 +Tfr:1D517 +Therefore:2234 +Theta:398 +ThickSpace:205F,200A +ThinSpace:2009 +Tilde:223C +TildeEqual:2243 +TildeFullEqual:2245 +TildeTilde:2248 +Topf:1D54B +TripleDot:20DB +Tscr:1D4AF +Tstrok:166 +Uacute:DA +Uarr:219F +Uarrocir:2949 +Ubrcy:40E +Ubreve:16C +Ucirc:DB +Ucy:423 +Udblac:170 +Ufr:1D518 +Ugrave:D9 +Umacr:16A +UnderBar:5F +UnderBrace:23DF +UnderBracket:23B5 +UnderParenthesis:23DD +Union:22C3 +UnionPlus:228E +Uogon:172 +Uopf:1D54C +UpArrow:2191 +UpArrowBar:2912 +UpArrowDownArrow:21C5 +UpDownArrow:2195 +UpEquilibrium:296E +UpTee:22A5 +UpTeeArrow:21A5 +Uparrow:21D1 +Updownarrow:21D5 +UpperLeftArrow:2196 +UpperRightArrow:2197 +Upsi:3D2 +Upsilon:3A5 +Uring:16E +Uscr:1D4B0 +Utilde:168 +Uuml:DC +VDash:22AB +Vbar:2AEB +Vcy:412 +Vdash:22A9 +Vdashl:2AE6 +Vee:22C1 +Verbar:2016 +Vert:2016 +VerticalBar:2223 +VerticalLine:7C +VerticalSeparator:2758 +VerticalTilde:2240 +VeryThinSpace:200A +Vfr:1D519 +Vopf:1D54D +Vscr:1D4B1 +Vvdash:22AA +Wcirc:174 +Wedge:22C0 +Wfr:1D51A +Wopf:1D54E +Wscr:1D4B2 +Xfr:1D51B +Xi:39E +Xopf:1D54F +Xscr:1D4B3 +YAcy:42F +YIcy:407 +YUcy:42E +Yacute:DD +Ycirc:176 +Ycy:42B +Yfr:1D51C +Yopf:1D550 +Yscr:1D4B4 +Yuml:178 +ZHcy:416 +Zacute:179 +Zcaron:17D +Zcy:417 +Zdot:17B +ZeroWidthSpace:200B +Zeta:396 +Zfr:2128 +Zopf:2124 +Zscr:1D4B5 +aacute:E1 +abreve:103 +ac:223E +acE:223E,333 +acd:223F +acirc:E2 +acute:B4 +acy:430 +aelig:E6 +af:2061 +afr:1D51E +agrave:E0 +alefsym:2135 +aleph:2135 +alpha:3B1 +amacr:101 +amalg:2A3F +amp:26 +and:2227 +andand:2A55 +andd:2A5C +andslope:2A58 +andv:2A5A +ang:2220 +ange:29A4 +angle:2220 +angmsd:2221 +angmsdaa:29A8 +angmsdab:29A9 +angmsdac:29AA +angmsdad:29AB +angmsdae:29AC +angmsdaf:29AD +angmsdag:29AE +angmsdah:29AF +angrt:221F +angrtvb:22BE +angrtvbd:299D +angsph:2222 +angst:C5 +angzarr:237C +aogon:105 +aopf:1D552 +ap:2248 +apE:2A70 +apacir:2A6F +ape:224A +apid:224B +apos:27 +approx:2248 +approxeq:224A +aring:E5 +ascr:1D4B6 +ast:2A +asymp:2248 +asympeq:224D +atilde:E3 +auml:E4 +awconint:2233 +awint:2A11 +bNot:2AED +backcong:224C +backepsilon:3F6 +backprime:2035 +backsim:223D +backsimeq:22CD +barvee:22BD +barwed:2305 +barwedge:2305 +bbrk:23B5 +bbrktbrk:23B6 +bcong:224C +bcy:431 +bdquo:201E +becaus:2235 +because:2235 +bemptyv:29B0 +bepsi:3F6 +bernou:212C +beta:3B2 +beth:2136 +between:226C +bfr:1D51F +bigcap:22C2 +bigcirc:25EF +bigcup:22C3 +bigodot:2A00 +bigoplus:2A01 +bigotimes:2A02 +bigsqcup:2A06 +bigstar:2605 +bigtriangledown:25BD +bigtriangleup:25B3 +biguplus:2A04 +bigvee:22C1 +bigwedge:22C0 +bkarow:290D +blacklozenge:29EB +blacksquare:25AA +blacktriangle:25B4 +blacktriangledown:25BE +blacktriangleleft:25C2 +blacktriangleright:25B8 +blank:2423 +blk12:2592 +blk14:2591 +blk34:2593 +block:2588 +bne:3D,20E5 +bnequiv:2261,20E5 +bnot:2310 +bopf:1D553 +bot:22A5 +bottom:22A5 +bowtie:22C8 +boxDL:2557 +boxDR:2554 +boxDl:2556 +boxDr:2553 +boxH:2550 +boxHD:2566 +boxHU:2569 +boxHd:2564 +boxHu:2567 +boxUL:255D +boxUR:255A +boxUl:255C +boxUr:2559 +boxV:2551 +boxVH:256C +boxVL:2563 +boxVR:2560 +boxVh:256B +boxVl:2562 +boxVr:255F +boxbox:29C9 +boxdL:2555 +boxdR:2552 +boxdl:2510 +boxdr:250C +boxh:2500 +boxhD:2565 +boxhU:2568 +boxhd:252C +boxhu:2534 +boxminus:229F +boxplus:229E +boxtimes:22A0 +boxuL:255B +boxuR:2558 +boxul:2518 +boxur:2514 +boxv:2502 +boxvH:256A +boxvL:2561 +boxvR:255E +boxvh:253C +boxvl:2524 +boxvr:251C +bprime:2035 +breve:2D8 +brvbar:A6 +bscr:1D4B7 +bsemi:204F +bsim:223D +bsime:22CD +bsol:5C +bsolb:29C5 +bsolhsub:27C8 +bull:2022 +bullet:2022 +bump:224E +bumpE:2AAE +bumpe:224F +bumpeq:224F +cacute:107 +cap:2229 +capand:2A44 +capbrcup:2A49 +capcap:2A4B +capcup:2A47 +capdot:2A40 +caps:2229,FE00 +caret:2041 +caron:2C7 +ccaps:2A4D +ccaron:10D +ccedil:E7 +ccirc:109 +ccups:2A4C +ccupssm:2A50 +cdot:10B +cedil:B8 +cemptyv:29B2 +cent:A2 +centerdot:B7 +cfr:1D520 +chcy:447 +check:2713 +checkmark:2713 +chi:3C7 +cir:25CB +cirE:29C3 +circ:2C6 +circeq:2257 +circlearrowleft:21BA +circlearrowright:21BB +circledR:AE +circledS:24C8 +circledast:229B +circledcirc:229A +circleddash:229D +cire:2257 +cirfnint:2A10 +cirmid:2AEF +cirscir:29C2 +clubs:2663 +clubsuit:2663 +colon:3A +colone:2254 +coloneq:2254 +comma:2C +commat:40 +comp:2201 +compfn:2218 +complement:2201 +complexes:2102 +cong:2245 +congdot:2A6D +conint:222E +copf:1D554 +coprod:2210 +copy:A9 +copysr:2117 +crarr:21B5 +cross:2717 +cscr:1D4B8 +csub:2ACF +csube:2AD1 +csup:2AD0 +csupe:2AD2 +ctdot:22EF +cudarrl:2938 +cudarrr:2935 +cuepr:22DE +cuesc:22DF +cularr:21B6 +cularrp:293D +cup:222A +cupbrcap:2A48 +cupcap:2A46 +cupcup:2A4A +cupdot:228D +cupor:2A45 +cups:222A,FE00 +curarr:21B7 +curarrm:293C +curlyeqprec:22DE +curlyeqsucc:22DF +curlyvee:22CE +curlywedge:22CF +curren:A4 +curvearrowleft:21B6 +curvearrowright:21B7 +cuvee:22CE +cuwed:22CF +cwconint:2232 +cwint:2231 +cylcty:232D +dArr:21D3 +dHar:2965 +dagger:2020 +daleth:2138 +darr:2193 +dash:2010 +dashv:22A3 +dbkarow:290F +dblac:2DD +dcaron:10F +dcy:434 +dd:2146 +ddagger:2021 +ddarr:21CA +ddotseq:2A77 +deg:B0 +delta:3B4 +demptyv:29B1 +dfisht:297F +dfr:1D521 +dharl:21C3 +dharr:21C2 +diam:22C4 +diamond:22C4 +diamondsuit:2666 +diams:2666 +die:A8 +digamma:3DD +disin:22F2 +div:F7 +divide:F7 +divideontimes:22C7 +divonx:22C7 +djcy:452 +dlcorn:231E +dlcrop:230D +dollar:24 +dopf:1D555 +dot:2D9 +doteq:2250 +doteqdot:2251 +dotminus:2238 +dotplus:2214 +dotsquare:22A1 +doublebarwedge:2306 +downarrow:2193 +downdownarrows:21CA +downharpoonleft:21C3 +downharpoonright:21C2 +drbkarow:2910 +drcorn:231F +drcrop:230C +dscr:1D4B9 +dscy:455 +dsol:29F6 +dstrok:111 +dtdot:22F1 +dtri:25BF +dtrif:25BE +duarr:21F5 +duhar:296F +dwangle:29A6 +dzcy:45F +dzigrarr:27FF +eDDot:2A77 +eDot:2251 +eacute:E9 +easter:2A6E +ecaron:11B +ecir:2256 +ecirc:EA +ecolon:2255 +ecy:44D +edot:117 +ee:2147 +efDot:2252 +efr:1D522 +eg:2A9A +egrave:E8 +egs:2A96 +egsdot:2A98 +el:2A99 +elinters:23E7 +ell:2113 +els:2A95 +elsdot:2A97 +emacr:113 +empty:2205 +emptyset:2205 +emptyv:2205 +emsp13:2004 +emsp14:2005 +emsp:2003 +eng:14B +ensp:2002 +eogon:119 +eopf:1D556 +epar:22D5 +eparsl:29E3 +eplus:2A71 +epsi:3B5 +epsilon:3B5 +epsiv:3F5 +eqcirc:2256 +eqcolon:2255 +eqsim:2242 +eqslantgtr:2A96 +eqslantless:2A95 +equals:3D +equest:225F +equiv:2261 +equivDD:2A78 +eqvparsl:29E5 +erDot:2253 +erarr:2971 +escr:212F +esdot:2250 +esim:2242 +eta:3B7 +eth:F0 +euml:EB +euro:20AC +excl:21 +exist:2203 +expectation:2130 +exponentiale:2147 +fallingdotseq:2252 +fcy:444 +female:2640 +ffilig:FB03 +fflig:FB00 +ffllig:FB04 +ffr:1D523 +filig:FB01 +fjlig:66,6A +flat:266D +fllig:FB02 +fltns:25B1 +fnof:192 +fopf:1D557 +forall:2200 +fork:22D4 +forkv:2AD9 +fpartint:2A0D +frac12:BD +frac13:2153 +frac14:BC +frac15:2155 +frac16:2159 +frac18:215B +frac23:2154 +frac25:2156 +frac34:BE +frac35:2157 +frac38:215C +frac45:2158 +frac56:215A +frac58:215D +frac78:215E +frasl:2044 +frown:2322 +fscr:1D4BB +gE:2267 +gEl:2A8C +gacute:1F5 +gamma:3B3 +gammad:3DD +gap:2A86 +gbreve:11F +gcirc:11D +gcy:433 +gdot:121 +ge:2265 +gel:22DB +geq:2265 +geqq:2267 +geqslant:2A7E +ges:2A7E +gescc:2AA9 +gesdot:2A80 +gesdoto:2A82 +gesdotol:2A84 +gesl:22DB,FE00 +gesles:2A94 +gfr:1D524 +gg:226B +ggg:22D9 +gimel:2137 +gjcy:453 +gl:2277 +glE:2A92 +gla:2AA5 +glj:2AA4 +gnE:2269 +gnap:2A8A +gnapprox:2A8A +gne:2A88 +gneq:2A88 +gneqq:2269 +gnsim:22E7 +gopf:1D558 +grave:60 +gscr:210A +gsim:2273 +gsime:2A8E +gsiml:2A90 +gt:3E +gtcc:2AA7 +gtcir:2A7A +gtdot:22D7 +gtlPar:2995 +gtquest:2A7C +gtrapprox:2A86 +gtrarr:2978 +gtrdot:22D7 +gtreqless:22DB +gtreqqless:2A8C +gtrless:2277 +gtrsim:2273 +gvertneqq:2269,FE00 +gvnE:2269,FE00 +hArr:21D4 +hairsp:200A +half:BD +hamilt:210B +hardcy:44A +harr:2194 +harrcir:2948 +harrw:21AD +hbar:210F +hcirc:125 +hearts:2665 +heartsuit:2665 +hellip:2026 +hercon:22B9 +hfr:1D525 +hksearow:2925 +hkswarow:2926 +hoarr:21FF +homtht:223B +hookleftarrow:21A9 +hookrightarrow:21AA +hopf:1D559 +horbar:2015 +hscr:1D4BD +hslash:210F +hstrok:127 +hybull:2043 +hyphen:2010 +iacute:ED +ic:2063 +icirc:EE +icy:438 +iecy:435 +iexcl:A1 +iff:21D4 +ifr:1D526 +igrave:EC +ii:2148 +iiiint:2A0C +iiint:222D +iinfin:29DC +iiota:2129 +ijlig:133 +imacr:12B +image:2111 +imagline:2110 +imagpart:2111 +imath:131 +imof:22B7 +imped:1B5 +in:2208 +incare:2105 +infin:221E +infintie:29DD +inodot:131 +int:222B +intcal:22BA +integers:2124 +intercal:22BA +intlarhk:2A17 +intprod:2A3C +iocy:451 +iogon:12F +iopf:1D55A +iota:3B9 +iprod:2A3C +iquest:BF +iscr:1D4BE +isin:2208 +isinE:22F9 +isindot:22F5 +isins:22F4 +isinsv:22F3 +isinv:2208 +it:2062 +itilde:129 +iukcy:456 +iuml:EF +jcirc:135 +jcy:439 +jfr:1D527 +jmath:237 +jopf:1D55B +jscr:1D4BF +jsercy:458 +jukcy:454 +kappa:3BA +kappav:3F0 +kcedil:137 +kcy:43A +kfr:1D528 +kgreen:138 +khcy:445 +kjcy:45C +kopf:1D55C +kscr:1D4C0 +lAarr:21DA +lArr:21D0 +lAtail:291B +lBarr:290E +lE:2266 +lEg:2A8B +lHar:2962 +lacute:13A +laemptyv:29B4 +lagran:2112 +lambda:3BB +lang:27E8 +langd:2991 +langle:27E8 +lap:2A85 +laquo:AB +larr:2190 +larrb:21E4 +larrbfs:291F +larrfs:291D +larrhk:21A9 +larrlp:21AB +larrpl:2939 +larrsim:2973 +larrtl:21A2 +lat:2AAB +latail:2919 +late:2AAD +lates:2AAD,FE00 +lbarr:290C +lbbrk:2772 +lbrace:7B +lbrack:5B +lbrke:298B +lbrksld:298F +lbrkslu:298D +lcaron:13E +lcedil:13C +lceil:2308 +lcub:7B +lcy:43B +ldca:2936 +ldquo:201C +ldquor:201E +ldrdhar:2967 +ldrushar:294B +ldsh:21B2 +le:2264 +leftarrow:2190 +leftarrowtail:21A2 +leftharpoondown:21BD +leftharpoonup:21BC +leftleftarrows:21C7 +leftrightarrow:2194 +leftrightarrows:21C6 +leftrightharpoons:21CB +leftrightsquigarrow:21AD +leftthreetimes:22CB +leg:22DA +leq:2264 +leqq:2266 +leqslant:2A7D +les:2A7D +lescc:2AA8 +lesdot:2A7F +lesdoto:2A81 +lesdotor:2A83 +lesg:22DA,FE00 +lesges:2A93 +lessapprox:2A85 +lessdot:22D6 +lesseqgtr:22DA +lesseqqgtr:2A8B +lessgtr:2276 +lesssim:2272 +lfisht:297C +lfloor:230A +lfr:1D529 +lg:2276 +lgE:2A91 +lhard:21BD +lharu:21BC +lharul:296A +lhblk:2584 +ljcy:459 +ll:226A +llarr:21C7 +llcorner:231E +llhard:296B +lltri:25FA +lmidot:140 +lmoust:23B0 +lmoustache:23B0 +lnE:2268 +lnap:2A89 +lnapprox:2A89 +lne:2A87 +lneq:2A87 +lneqq:2268 +lnsim:22E6 +loang:27EC +loarr:21FD +lobrk:27E6 +longleftarrow:27F5 +longleftrightarrow:27F7 +longmapsto:27FC +longrightarrow:27F6 +looparrowleft:21AB +looparrowright:21AC +lopar:2985 +lopf:1D55D +loplus:2A2D +lotimes:2A34 +lowast:2217 +lowbar:5F +loz:25CA +lozenge:25CA +lozf:29EB +lpar:28 +lparlt:2993 +lrarr:21C6 +lrcorner:231F +lrhar:21CB +lrhard:296D +lrm:200E +lrtri:22BF +lsaquo:2039 +lscr:1D4C1 +lsh:21B0 +lsim:2272 +lsime:2A8D +lsimg:2A8F +lsqb:5B +lsquo:2018 +lsquor:201A +lstrok:142 +lt:3C +ltcc:2AA6 +ltcir:2A79 +ltdot:22D6 +lthree:22CB +ltimes:22C9 +ltlarr:2976 +ltquest:2A7B +ltrPar:2996 +ltri:25C3 +ltrie:22B4 +ltrif:25C2 +lurdshar:294A +luruhar:2966 +lvertneqq:2268,FE00 +lvnE:2268,FE00 +mDDot:223A +macr:AF +male:2642 +malt:2720 +maltese:2720 +map:21A6 +mapsto:21A6 +mapstodown:21A7 +mapstoleft:21A4 +mapstoup:21A5 +marker:25AE +mcomma:2A29 +mcy:43C +mdash:2014 +measuredangle:2221 +mfr:1D52A +mho:2127 +micro:B5 +mid:2223 +midast:2A +midcir:2AF0 +middot:B7 +minus:2212 +minusb:229F +minusd:2238 +minusdu:2A2A +mlcp:2ADB +mldr:2026 +mnplus:2213 +models:22A7 +mopf:1D55E +mp:2213 +mscr:1D4C2 +mstpos:223E +mu:3BC +multimap:22B8 +mumap:22B8 +nGg:22D9,338 +nGt:226B,20D2 +nGtv:226B,338 +nLeftarrow:21CD +nLeftrightarrow:21CE +nLl:22D8,338 +nLt:226A,20D2 +nLtv:226A,338 +nRightarrow:21CF +nVDash:22AF +nVdash:22AE +nabla:2207 +nacute:144 +nang:2220,20D2 +nap:2249 +napE:2A70,338 +napid:224B,338 +napos:149 +napprox:2249 +natur:266E +natural:266E +naturals:2115 +nbsp:A0 +nbump:224E,338 +nbumpe:224F,338 +ncap:2A43 +ncaron:148 +ncedil:146 +ncong:2247 +ncongdot:2A6D,338 +ncup:2A42 +ncy:43D +ndash:2013 +ne:2260 +neArr:21D7 +nearhk:2924 +nearr:2197 +nearrow:2197 +nedot:2250,338 +nequiv:2262 +nesear:2928 +nesim:2242,338 +nexist:2204 +nexists:2204 +nfr:1D52B +ngE:2267,338 +nge:2271 +ngeq:2271 +ngeqq:2267,338 +ngeqslant:2A7E,338 +nges:2A7E,338 +ngsim:2275 +ngt:226F +ngtr:226F +nhArr:21CE +nharr:21AE +nhpar:2AF2 +ni:220B +nis:22FC +nisd:22FA +niv:220B +njcy:45A +nlArr:21CD +nlE:2266,338 +nlarr:219A +nldr:2025 +nle:2270 +nleftarrow:219A +nleftrightarrow:21AE +nleq:2270 +nleqq:2266,338 +nleqslant:2A7D,338 +nles:2A7D,338 +nless:226E +nlsim:2274 +nlt:226E +nltri:22EA +nltrie:22EC +nmid:2224 +nopf:1D55F +not:AC +notin:2209 +notinE:22F9,338 +notindot:22F5,338 +notinva:2209 +notinvb:22F7 +notinvc:22F6 +notni:220C +notniva:220C +notnivb:22FE +notnivc:22FD +npar:2226 +nparallel:2226 +nparsl:2AFD,20E5 +npart:2202,338 +npolint:2A14 +npr:2280 +nprcue:22E0 +npre:2AAF,338 +nprec:2280 +npreceq:2AAF,338 +nrArr:21CF +nrarr:219B +nrarrc:2933,338 +nrarrw:219D,338 +nrightarrow:219B +nrtri:22EB +nrtrie:22ED +nsc:2281 +nsccue:22E1 +nsce:2AB0,338 +nscr:1D4C3 +nshortmid:2224 +nshortparallel:2226 +nsim:2241 +nsime:2244 +nsimeq:2244 +nsmid:2224 +nspar:2226 +nsqsube:22E2 +nsqsupe:22E3 +nsub:2284 +nsubE:2AC5,338 +nsube:2288 +nsubset:2282,20D2 +nsubseteq:2288 +nsubseteqq:2AC5,338 +nsucc:2281 +nsucceq:2AB0,338 +nsup:2285 +nsupE:2AC6,338 +nsupe:2289 +nsupset:2283,20D2 +nsupseteq:2289 +nsupseteqq:2AC6,338 +ntgl:2279 +ntilde:F1 +ntlg:2278 +ntriangleleft:22EA +ntrianglelefteq:22EC +ntriangleright:22EB +ntrianglerighteq:22ED +nu:3BD +num:23 +numero:2116 +numsp:2007 +nvDash:22AD +nvHarr:2904 +nvap:224D,20D2 +nvdash:22AC +nvge:2265,20D2 +nvgt:3E,20D2 +nvinfin:29DE +nvlArr:2902 +nvle:2264,20D2 +nvlt:3C,20D2 +nvltrie:22B4,20D2 +nvrArr:2903 +nvrtrie:22B5,20D2 +nvsim:223C,20D2 +nwArr:21D6 +nwarhk:2923 +nwarr:2196 +nwarrow:2196 +nwnear:2927 +oS:24C8 +oacute:F3 +oast:229B +ocir:229A +ocirc:F4 +ocy:43E +odash:229D +odblac:151 +odiv:2A38 +odot:2299 +odsold:29BC +oelig:153 +ofcir:29BF +ofr:1D52C +ogon:2DB +ograve:F2 +ogt:29C1 +ohbar:29B5 +ohm:3A9 +oint:222E +olarr:21BA +olcir:29BE +olcross:29BB +oline:203E +olt:29C0 +omacr:14D +omega:3C9 +omicron:3BF +omid:29B6 +ominus:2296 +oopf:1D560 +opar:29B7 +operp:29B9 +oplus:2295 +or:2228 +orarr:21BB +ord:2A5D +order:2134 +orderof:2134 +ordf:AA +ordm:BA +origof:22B6 +oror:2A56 +orslope:2A57 +orv:2A5B +oscr:2134 +oslash:F8 +osol:2298 +otilde:F5 +otimes:2297 +otimesas:2A36 +ouml:F6 +ovbar:233D +par:2225 +para:B6 +parallel:2225 +parsim:2AF3 +parsl:2AFD +part:2202 +pcy:43F +percnt:25 +period:2E +permil:2030 +perp:22A5 +pertenk:2031 +pfr:1D52D +phi:3C6 +phiv:3D5 +phmmat:2133 +phone:260E +pi:3C0 +pitchfork:22D4 +piv:3D6 +planck:210F +planckh:210E +plankv:210F +plus:2B +plusacir:2A23 +plusb:229E +pluscir:2A22 +plusdo:2214 +plusdu:2A25 +pluse:2A72 +plusmn:B1 +plussim:2A26 +plustwo:2A27 +pm:B1 +pointint:2A15 +popf:1D561 +pound:A3 +pr:227A +prE:2AB3 +prap:2AB7 +prcue:227C +pre:2AAF +prec:227A +precapprox:2AB7 +preccurlyeq:227C +preceq:2AAF +precnapprox:2AB9 +precneqq:2AB5 +precnsim:22E8 +precsim:227E +prime:2032 +primes:2119 +prnE:2AB5 +prnap:2AB9 +prnsim:22E8 +prod:220F +profalar:232E +profline:2312 +profsurf:2313 +prop:221D +propto:221D +prsim:227E +prurel:22B0 +pscr:1D4C5 +psi:3C8 +puncsp:2008 +qfr:1D52E +qint:2A0C +qopf:1D562 +qprime:2057 +qscr:1D4C6 +quaternions:210D +quatint:2A16 +quest:3F +questeq:225F +quot:22 +rAarr:21DB +rArr:21D2 +rAtail:291C +rBarr:290F +rHar:2964 +race:223D,331 +racute:155 +radic:221A +raemptyv:29B3 +rang:27E9 +rangd:2992 +range:29A5 +rangle:27E9 +raquo:BB +rarr:2192 +rarrap:2975 +rarrb:21E5 +rarrbfs:2920 +rarrc:2933 +rarrfs:291E +rarrhk:21AA +rarrlp:21AC +rarrpl:2945 +rarrsim:2974 +rarrtl:21A3 +rarrw:219D +ratail:291A +ratio:2236 +rationals:211A +rbarr:290D +rbbrk:2773 +rbrace:7D +rbrack:5D +rbrke:298C +rbrksld:298E +rbrkslu:2990 +rcaron:159 +rcedil:157 +rceil:2309 +rcub:7D +rcy:440 +rdca:2937 +rdldhar:2969 +rdquo:201D +rdquor:201D +rdsh:21B3 +real:211C +realine:211B +realpart:211C +reals:211D +rect:25AD +reg:AE +rfisht:297D +rfloor:230B +rfr:1D52F +rhard:21C1 +rharu:21C0 +rharul:296C +rho:3C1 +rhov:3F1 +rightarrow:2192 +rightarrowtail:21A3 +rightharpoondown:21C1 +rightharpoonup:21C0 +rightleftarrows:21C4 +rightleftharpoons:21CC +rightrightarrows:21C9 +rightsquigarrow:219D +rightthreetimes:22CC +ring:2DA +risingdotseq:2253 +rlarr:21C4 +rlhar:21CC +rlm:200F +rmoust:23B1 +rmoustache:23B1 +rnmid:2AEE +roang:27ED +roarr:21FE +robrk:27E7 +ropar:2986 +ropf:1D563 +roplus:2A2E +rotimes:2A35 +rpar:29 +rpargt:2994 +rppolint:2A12 +rrarr:21C9 +rsaquo:203A +rscr:1D4C7 +rsh:21B1 +rsqb:5D +rsquo:2019 +rsquor:2019 +rthree:22CC +rtimes:22CA +rtri:25B9 +rtrie:22B5 +rtrif:25B8 +rtriltri:29CE +ruluhar:2968 +rx:211E +sacute:15B +sbquo:201A +sc:227B +scE:2AB4 +scap:2AB8 +scaron:161 +sccue:227D +sce:2AB0 +scedil:15F +scirc:15D +scnE:2AB6 +scnap:2ABA +scnsim:22E9 +scpolint:2A13 +scsim:227F +scy:441 +sdot:22C5 +sdotb:22A1 +sdote:2A66 +seArr:21D8 +searhk:2925 +searr:2198 +searrow:2198 +sect:A7 +semi:3B +seswar:2929 +setminus:2216 +setmn:2216 +sext:2736 +sfr:1D530 +sfrown:2322 +sharp:266F +shchcy:449 +shcy:448 +shortmid:2223 +shortparallel:2225 +shy:AD +sigma:3C3 +sigmaf:3C2 +sigmav:3C2 +sim:223C +simdot:2A6A +sime:2243 +simeq:2243 +simg:2A9E +simgE:2AA0 +siml:2A9D +simlE:2A9F +simne:2246 +simplus:2A24 +simrarr:2972 +slarr:2190 +smallsetminus:2216 +smashp:2A33 +smeparsl:29E4 +smid:2223 +smile:2323 +smt:2AAA +smte:2AAC +smtes:2AAC,FE00 +softcy:44C +sol:2F +solb:29C4 +solbar:233F +sopf:1D564 +spades:2660 +spadesuit:2660 +spar:2225 +sqcap:2293 +sqcaps:2293,FE00 +sqcup:2294 +sqcups:2294,FE00 +sqsub:228F +sqsube:2291 +sqsubset:228F +sqsubseteq:2291 +sqsup:2290 +sqsupe:2292 +sqsupset:2290 +sqsupseteq:2292 +squ:25A1 +square:25A1 +squarf:25AA +squf:25AA +srarr:2192 +sscr:1D4C8 +ssetmn:2216 +ssmile:2323 +sstarf:22C6 +star:2606 +starf:2605 +straightepsilon:3F5 +straightphi:3D5 +strns:AF +sub:2282 +subE:2AC5 +subdot:2ABD +sube:2286 +subedot:2AC3 +submult:2AC1 +subnE:2ACB +subne:228A +subplus:2ABF +subrarr:2979 +subset:2282 +subseteq:2286 +subseteqq:2AC5 +subsetneq:228A +subsetneqq:2ACB +subsim:2AC7 +subsub:2AD5 +subsup:2AD3 +succ:227B +succapprox:2AB8 +succcurlyeq:227D +succeq:2AB0 +succnapprox:2ABA +succneqq:2AB6 +succnsim:22E9 +succsim:227F +sum:2211 +sung:266A +sup1:B9 +sup2:B2 +sup3:B3 +sup:2283 +supE:2AC6 +supdot:2ABE +supdsub:2AD8 +supe:2287 +supedot:2AC4 +suphsol:27C9 +suphsub:2AD7 +suplarr:297B +supmult:2AC2 +supnE:2ACC +supne:228B +supplus:2AC0 +supset:2283 +supseteq:2287 +supseteqq:2AC6 +supsetneq:228B +supsetneqq:2ACC +supsim:2AC8 +supsub:2AD4 +supsup:2AD6 +swArr:21D9 +swarhk:2926 +swarr:2199 +swarrow:2199 +swnwar:292A +szlig:DF +target:2316 +tau:3C4 +tbrk:23B4 +tcaron:165 +tcedil:163 +tcy:442 +tdot:20DB +telrec:2315 +tfr:1D531 +there4:2234 +therefore:2234 +theta:3B8 +thetasym:3D1 +thetav:3D1 +thickapprox:2248 +thicksim:223C +thinsp:2009 +thkap:2248 +thksim:223C +thorn:FE +tilde:2DC +times:D7 +timesb:22A0 +timesbar:2A31 +timesd:2A30 +tint:222D +toea:2928 +top:22A4 +topbot:2336 +topcir:2AF1 +topf:1D565 +topfork:2ADA +tosa:2929 +tprime:2034 +trade:2122 +triangle:25B5 +triangledown:25BF +triangleleft:25C3 +trianglelefteq:22B4 +triangleq:225C +triangleright:25B9 +trianglerighteq:22B5 +tridot:25EC +trie:225C +triminus:2A3A +triplus:2A39 +trisb:29CD +tritime:2A3B +trpezium:23E2 +tscr:1D4C9 +tscy:446 +tshcy:45B +tstrok:167 +twixt:226C +twoheadleftarrow:219E +twoheadrightarrow:21A0 +uArr:21D1 +uHar:2963 +uacute:FA +uarr:2191 +ubrcy:45E +ubreve:16D +ucirc:FB +ucy:443 +udarr:21C5 +udblac:171 +udhar:296E +ufisht:297E +ufr:1D532 +ugrave:F9 +uharl:21BF +uharr:21BE +uhblk:2580 +ulcorn:231C +ulcorner:231C +ulcrop:230F +ultri:25F8 +umacr:16B +uml:A8 +uogon:173 +uopf:1D566 +uparrow:2191 +updownarrow:2195 +upharpoonleft:21BF +upharpoonright:21BE +uplus:228E +upsi:3C5 +upsih:3D2 +upsilon:3C5 +upuparrows:21C8 +urcorn:231D +urcorner:231D +urcrop:230E +uring:16F +urtri:25F9 +uscr:1D4CA +utdot:22F0 +utilde:169 +utri:25B5 +utrif:25B4 +uuarr:21C8 +uuml:FC +uwangle:29A7 +vArr:21D5 +vBar:2AE8 +vBarv:2AE9 +vDash:22A8 +vangrt:299C +varepsilon:3F5 +varkappa:3F0 +varnothing:2205 +varphi:3D5 +varpi:3D6 +varpropto:221D +varr:2195 +varrho:3F1 +varsigma:3C2 +varsubsetneq:228A,FE00 +varsubsetneqq:2ACB,FE00 +varsupsetneq:228B,FE00 +varsupsetneqq:2ACC,FE00 +vartheta:3D1 +vartriangleleft:22B2 +vartriangleright:22B3 +vcy:432 +vdash:22A2 +vee:2228 +veebar:22BB +veeeq:225A +vellip:22EE +verbar:7C +vert:7C +vfr:1D533 +vltri:22B2 +vnsub:2282,20D2 +vnsup:2283,20D2 +vopf:1D567 +vprop:221D +vrtri:22B3 +vscr:1D4CB +vsubnE:2ACB,FE00 +vsubne:228A,FE00 +vsupnE:2ACC,FE00 +vsupne:228B,FE00 +vzigzag:299A +wcirc:175 +wedbar:2A5F +wedge:2227 +wedgeq:2259 +weierp:2118 +wfr:1D534 +wopf:1D568 +wp:2118 +wr:2240 +wreath:2240 +wscr:1D4CC +xcap:22C2 +xcirc:25EF +xcup:22C3 +xdtri:25BD +xfr:1D535 +xhArr:27FA +xharr:27F7 +xi:3BE +xlArr:27F8 +xlarr:27F5 +xmap:27FC +xnis:22FB +xodot:2A00 +xopf:1D569 +xoplus:2A01 +xotime:2A02 +xrArr:27F9 +xrarr:27F6 +xscr:1D4CD +xsqcup:2A06 +xuplus:2A04 +xutri:25B3 +xvee:22C1 +xwedge:22C0 +yacute:FD +yacy:44F +ycirc:177 +ycy:44B +yen:A5 +yfr:1D536 +yicy:457 +yopf:1D56A +yscr:1D4CE +yucy:44E +yuml:FF +zacute:17A +zcaron:17E +zcy:437 +zdot:17C +zeetrf:2128 +zeta:3B6 +zfr:1D537 +zhcy:436 +zigrarr:21DD +zopf:1D56B +zscr:1D4CF +zwj:200D +zwnj:200C +""" +} diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 08feff8..dd02259 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -228,7 +228,10 @@ struct MarkdownBlockParser: Sendable { if let images = InlineSyntaxScanner.standaloneImages(in: line[...], references: referenceDefinitions) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) for image in images { - blocks.append(.image(index: blockIndex, url: image.url, alt: image.alt, width: nil, sourceLine: sourceLine)) + // The alt is caption text: references decoded like any inline + // text (an `` is decoded by `HTMLImageSyntax`). + blocks.append(.image(index: blockIndex, url: image.url, alt: HTMLEntities.decode(image.alt), + width: nil, sourceLine: sourceLine)) blockIndex += 1 } i += 1 diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index fc1a14b..d1f92f8 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -146,13 +146,20 @@ struct MarkdownRenderer: Sendable { /// written; every other inline form still renders. Copied into the link /// text renderer with the rest of `self`. private var escapesDisabled = false - /// True while rendering a HEADING (`renderHeader`): character references - /// in its plain text and link text (`&`, `<`, `©` — the set - /// `HTMLImageSyntax.decodeEntities` knows) show as the character, as on - /// GitHub, and so the heading's id slugs `Q&A`, not `Q&A`. Decoded - /// AFTER inline parsing, so `*` is a literal `*`, never emphasis; - /// code spans keep the reference as written. - private var decodesEntities = false + /// Character references (`AT&T`, `©`, `©`) are decoded in + /// every piece of inline TEXT this renderer draws — plain runs, emphasis, + /// link text, image alt — as on GitHub (`HTMLEntities`, full HTML5 table), + /// so a heading's id slugs `Q&A`, not `Q&A`. AFTER inline parsing: + /// `*` is a literal `*`, never emphasis; code spans keep the reference + /// as written. True only for a front-matter value + /// (`renderInline(_:literalBackslashes:literalEntities:)`): GitHub shows + /// front matter as plain text, references included. + private var entitiesLiteral = false + + /// `text` with its character references decoded, unless `entitiesLiteral`. + private func decodedText(_ text: String) -> String { + entitiesLiteral ? text : HTMLEntities.decode(text) + } // Static precompiled regex for parsing (avoid recompilation per line) private static let taskListRegex = try! NSRegularExpression(pattern: MarkdownTheme.taskListPattern) @@ -415,9 +422,7 @@ struct MarkdownRenderer: Sendable { // MARK: - Block Renderers func renderHeader(_ text: String, level: Int) -> AttributedString { - var heading = self - heading.decodesEntities = true - var attr = heading.renderInlineFormatting(text) + var attr = renderInlineFormatting(text) attr.setDualFont(size: headerFontSize(level: level), bold: true, fonts: theme.fonts) attr.setDualForeground(theme.textColor) return attr @@ -701,11 +706,13 @@ struct MarkdownRenderer: Sendable { } /// `renderInline` with backslash escapes off when `literalBackslashes` - /// (see `escapesDisabled`). - func renderInline(_ text: String, literalBackslashes: Bool) -> AttributedString { - guard literalBackslashes else { return renderInlineFormatting(text) } + /// (see `escapesDisabled`) and character references shown as written when + /// `literalEntities` (see `entitiesLiteral`). + func renderInline(_ text: String, literalBackslashes: Bool, literalEntities: Bool = false) -> AttributedString { + guard literalBackslashes || literalEntities else { return renderInlineFormatting(text) } var literal = self - literal.escapesDisabled = true + literal.escapesDisabled = literalBackslashes + literal.entitiesLiteral = literalEntities return literal.renderInlineFormatting(text) } @@ -726,8 +733,7 @@ struct MarkdownRenderer: Sendable { // Helper to flush buffered plain text func flushPlainText() { guard !plainTextBuffer.isEmpty else { return } - var attr = AttributedString(decodesEntities ? HTMLImageSyntax.decodeEntities(plainTextBuffer) - : plainTextBuffer) + var attr = AttributedString(decodedText(plainTextBuffer)) attr.setDualFont(size: scaled(14), fonts: theme.fonts) attr.setDualForeground(theme.textColor) result.append(attr) @@ -815,7 +821,7 @@ struct MarkdownRenderer: Sendable { guard let endRange = afterMarker.range(of: marker) else { return nil } let text = String(afterMarker[.. AttributedString { - renderer.renderInline(value, literalBackslashes: true) + renderer.renderInline(value, literalBackslashes: true, literalEntities: true) } /// Header cells are semibold. Only raises the weight: a `**bold**` run diff --git a/QuickMD/QuickMD/Views/TableOfContentsView.swift b/QuickMD/QuickMD/Views/TableOfContentsView.swift index 07e74be..8e7a82f 100644 --- a/QuickMD/QuickMD/Views/TableOfContentsView.swift +++ b/QuickMD/QuickMD/Views/TableOfContentsView.swift @@ -105,7 +105,7 @@ private struct ToCEntryRow: View { onSelect() } label: { // Character references as the heading shows them (`Q&A` → `Q&A`). - Text(HTMLImageSyntax.decodeEntities(entry.title)) + Text(HTMLEntities.decode(entry.title)) .font(.system(size: fontSize(for: entry.level), weight: fontWeight(for: entry.level))) .foregroundColor(theme.textColor) .lineLimit(2) diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 4bb7b0b..6bd9774 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -730,4 +730,84 @@ final class RendererTests: XCTestCase { XCTAssertEqual(MarkdownZoom.bigger.applied(to: 1.25, defaultScale: 1.25), 1.5) XCTAssertEqual(MarkdownZoom.smaller.applied(to: 1.25, defaultScale: 1.25), 1.1) } + + // MARK: - Character references (decoded like GitHub) + + /// Expected strings from `gh api markdown` (2026-11-04): named references + /// from the full HTML5 table (`≧̸` is two code points), numeric ones + /// per CommonMark (0, a surrogate or > U+10FFFF → U+FFFD), a reference + /// without its `;` or with an unknown name stays as written, and decoding + /// happens after inline parsing (`*x*` is "*x*", not emphasis). + func testCharacterReferencesDecodeLikeGitHub() { + XCTAssertEqual(HTMLEntities.decode("AT&T © a b → # � � � &bogus; & ≧̸ A"), + "AT&T © a\u{00A0}b → # \u{FFFD} \u{FFFD} \u{FFFD} &bogus; & \u{2267}\u{0338} A") + XCTAssertEqual(HTMLEntities.decode("∳ � � &#; &#x; & ;"), + "\u{2233} � � &#; &#x; & ;", "too many digits, no digits: literal") + XCTAssertEqual(HTMLEntities.decode(" "), "\n\t") + XCTAssertEqual(HTMLEntities.decode("no ampersand"), "no ampersand") + + let line = "AT&T `&` **<b>** ~~>~~ ***>*** [&](http://x) ![a&b](i.png) *x*" + XCTAssertEqual(rendered(line), "AT&T & > > & [Image: a&b] *x*") + let attributed = renderer.renderInline("*x*") + XCTAssertEqual(attributed.runs.count, 1, "a decoded asterisk opens no emphasis") + } + + /// Every kind that renders inline text decodes — paragraph, list, quote, + /// alert, table cell, heading, footnote — and never a code block; a + /// front-matter VALUE keeps its references, as GitHub shows front matter + /// as plain text. + func testCharacterReferencesDecodeInEveryTextKindButCodeAndFrontMatter() { + let theme = MarkdownTheme.cached(for: .light) + let md = """ + --- + title: A&B + --- + Para © 2026[^n] + + - item → next + + > quote & more + + > [!NOTE] + > alert … + + | h&h | + |---| + | cell x | + + ## Q&A + + ``` + code & stays + ``` + + ![a&b](pic.png) + + [^n]: Note & text + """ + let blocks = MarkdownBlockParser(theme: theme).parse(md) + let plain = MarkdownRenderer(theme: theme) + var shown: [String] = [] + for block in blocks { + switch block.content { + case .text(let attributed): shown.append(String(attributed.characters)) + case .blockquote(let content, _), .alert(_, let content): + shown.append(String(plain.renderQuotedBody(content).characters)) + case .table(let headers, let rows, let alignments, let style): + shown.append(TableTextBuilder.plainString(headers: headers, rows: rows, alignments: alignments, + style: style, theme: theme, fontScale: 1)) + case .heading(let level, let title, _): + shown.append(String(plain.renderHeader(title, level: level).characters)) + case .codeBlock(let code, _): shown.append(code) + case .image(_, let alt, _): shown.append("image:" + alt) + default: break + } + } + let all = shown.joined(separator: "|") + for expected in ["title\nA&B\n", "Para © 2026", "item → next", "quote & more", "alert …", + "h&h\ncell\u{00A0}x\n", "Q&A", "code & stays", "image:a&b", "Note & text"] { + XCTAssertTrue(all.contains(expected), "\(expected.debugDescription) in \(all.debugDescription)") + } + } + } diff --git a/QuickMD/QuickMDTests/SearchTests.swift b/QuickMD/QuickMDTests/SearchTests.swift index 4e565ee..92147fb 100644 --- a/QuickMD/QuickMDTests/SearchTests.swift +++ b/QuickMD/QuickMDTests/SearchTests.swift @@ -512,9 +512,10 @@ final class SearchTests: XCTestCase { XCTAssertEqual(count("hidden", in: quote), 0, "link URL") XCTAssertEqual(count("\\", in: quote), 0, "escape") XCTAssertEqual(count("bold code label a*b", in: quote), 1, "rendered text across styles") - // HTML entities are drawn as written (the renderer does not decode - // them), so the source IS what shows — counted and highlighted alike. - XCTAssertEqual(count("&", in: quote), 1) + // Character references are drawn decoded (`HTMLEntities`, as on + // GitHub): the drawn "&" is counted, the source "&" is not. + XCTAssertEqual(count("&", in: quote), 0) + XCTAssertEqual(count("a*b & done", in: quote), 1) let alert = "> [!WARNING]\n> **bold** [label](https://hidden.example)" XCTAssertEqual(count("**", in: alert), 0) From ac2f9947e8fe7ded53b7c1090e16a51292fc86f2 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:22:22 +0100 Subject: [PATCH 048/105] fix(copy): a front-matter list copies without U+2028 A list value shown one item per line (B4a) carries U+2028, which went to the pasteboard as is from a single cell and became a space in the TSV, running "Doe, Jane" and "Smith" back into one comma list. Copying one cell now gives real line breaks (as shown); the table's TSV, whole or across cells, keeps one line per property and joins those items with "; ". Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/DocumentSelection.swift | 8 ++++++-- QuickMD/QuickMD/SelectionRows.swift | 7 ++++++- .../QuickMDTests/TableSelectionTests.swift | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/QuickMD/QuickMD/DocumentSelection.swift b/QuickMD/QuickMD/DocumentSelection.swift index e80da23..7de22b9 100644 --- a/QuickMD/QuickMD/DocumentSelection.swift +++ b/QuickMD/QuickMD/DocumentSelection.swift @@ -405,9 +405,13 @@ enum DocumentCopyBuilder { } /// One TSV field: a tab or line break inside a cell would split it, so - /// those become spaces. + /// those become spaces — except U+2028, which only a front-matter list + /// puts in a cell (one item per line because an item holds a comma, + /// `FrontMatter.joinedItems`): a space would run "Doe, Jane" and "Smith" + /// together again, so those items are joined with "; ". private static func tsvField(_ cell: String) -> String { - cell.components(separatedBy: CharacterSet(charactersIn: "\t\n\r\u{2028}\u{2029}")) + cell.replacingOccurrences(of: FrontMatter.itemLineSeparator, with: "; ") + .components(separatedBy: CharacterSet(charactersIn: "\t\n\r\u{2029}")) .joined(separator: " ") } diff --git a/QuickMD/QuickMD/SelectionRows.swift b/QuickMD/QuickMD/SelectionRows.swift index 79c72e4..e7f7e56 100644 --- a/QuickMD/QuickMD/SelectionRows.swift +++ b/QuickMD/QuickMD/SelectionRows.swift @@ -154,9 +154,14 @@ enum SelectionRows { /// `text` with what belongs to the TABLE removed from its paragraph /// styles: the text blocks (the cell) and the alignment (the column's, - /// from the separator row — back to natural). The rest is kept. + /// from the separator row — back to natural). The rest is kept. A + /// front-matter list's U+2028 (one item per line) becomes a plain line + /// break: the copy reads as the cell shows, without a separator most + /// apps treat as invisible. private static func withoutCellLayout(_ text: NSAttributedString) -> NSAttributedString { let result = NSMutableAttributedString(attributedString: text) + result.mutableString.replaceOccurrences(of: FrontMatter.itemLineSeparator, with: "\n", options: [], + range: NSRange(location: 0, length: result.length)) result.enumerateAttribute(.paragraphStyle, in: NSRange(location: 0, length: result.length)) { value, range, _ in guard let style = value as? NSParagraphStyle, !style.textBlocks.isEmpty || style.alignment != .natural, diff --git a/QuickMD/QuickMDTests/TableSelectionTests.swift b/QuickMD/QuickMDTests/TableSelectionTests.swift index 2d16651..9fa2226 100644 --- a/QuickMD/QuickMDTests/TableSelectionTests.swift +++ b/QuickMD/QuickMDTests/TableSelectionTests.swift @@ -193,6 +193,26 @@ final class TableSelectionTests: XCTestCase { XCTAssertEqual(output?.plain, "a b\tc") } + /// A front-matter list shown one item per line (an item holds a comma): + /// no U+2028 reaches the pasteboard. One cell copies as lines, as shown; + /// the TSV of the table (and across cells) keeps one row per property and + /// joins those items with "; " — a space would merge "Doe, Jane" and + /// "Smith" into one comma list again. + func testFrontMatterListCopiesWithoutLineSeparators() { + let fixture = tableBlock("---\nauthors: [\"Doe, Jane\", Smith]\ntags: [a, b]\n---\n") + let layout = fixture.text.layout + let value = layout.cellRanges[0][1] + XCTAssertEqual(copy(value, of: fixture)?.plain, "Doe, Jane\nSmith", "one cell: as shown") + let whole = copy(NSRange(location: 0, length: fixture.text.string.length), of: fixture) + XCTAssertEqual(whole?.plain, "authors\tDoe, Jane; Smith\ntags\ta, b") + XCTAssertEqual(copy(span(layout, from: (0, 0, 0), to: (0, 1, value.length)), of: fixture)?.plain, + "authors\tDoe, Jane; Smith") + for output in [copy(value, of: fixture), whole] { + XCTAssertFalse(output?.plain.contains("\u{2028}") ?? true) + XCTAssertFalse(output?.rtf.string.contains("\u{2028}") ?? true) + } + } + func testWholeTableIsExactlyThe112TSV() { let fixture = tableBlock() let length = fixture.text.string.length From 4196d8adc768a003795f53b51817b0f8690438e7 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:27:07 +0100 Subject: [PATCH 049/105] fix(references): inline quote and alert references per soft-break group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review R1: quote/alert bodies were rewritten line by line, but the renderers join soft breaks first, so a code span or link spanning lines broke: `git log [d]⏎--oneline` showed rewritten Markdown inside the code span, and [the⏎guide][d] linked only "d". ReferenceLinks.inlined (quoteBody: true) scans each joined group as one line; an unchanged group keeps its lines, a changed one becomes its joined line (plus " " after a hard break), which joinSoftBreaks reproduces. Table cells stay line-wise (one line each). Review R8: the quote/alert branch inlined the whole content only for an isEmpty check and then the alert body again; each body is now inlined once (no separate test: no behaviour change). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 15 ++++++---- QuickMD/QuickMD/MarkdownRenderer.swift | 2 +- QuickMD/QuickMD/ReferenceLinks.swift | 36 ++++++++++++++++++----- QuickMD/QuickMDTests/ParserTests.swift | 35 ++++++++++++++++++++++ 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index dd02259..7fce9c1 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -389,10 +389,11 @@ struct MarkdownBlockParser: Sendable { } // Quote and alert bodies are rendered later without the - // definitions, so references are resolved here (`ReferenceLinks`). - let content = ReferenceLinks.inlined(groupLines.joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions, - footnoteIds: footnoteDefinitions.map(\.id)) + // definitions, so references are resolved here, once per + // body (`ReferenceLinks`, soft-break groups as one line). + let content = groupLines.joined(separator: "\n") + .trimmingCharacters(in: .whitespacesAndNewlines) + let footnoteIds = footnoteDefinitions.map(\.id) if !content.isEmpty { // GFM alert: level-1 quote whose first line is exactly // `[!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / @@ -400,11 +401,13 @@ struct MarkdownBlockParser: Sendable { if groupLevel == 1, let kind = AlertKind(markerLine: groupLines[0]) { let body = ReferenceLinks.inlined(groupLines.dropFirst().joined(separator: "\n") .trimmingCharacters(in: .whitespacesAndNewlines), definitions: referenceDefinitions, - footnoteIds: footnoteDefinitions.map(\.id)) + footnoteIds: footnoteIds, quoteBody: true) blocks.append(.alert(index: blockIndex, kind: kind, content: body, sourceLine: groupSourceLine)) previousGroupWasQuote = false } else { - blocks.append(.blockquote(index: blockIndex, content: content, level: groupLevel, + let body = ReferenceLinks.inlined(content, definitions: referenceDefinitions, + footnoteIds: footnoteIds, quoteBody: true) + blocks.append(.blockquote(index: blockIndex, content: body, level: groupLevel, sourceLine: groupSourceLine, joinsPrevious: previousGroupWasQuote)) previousGroupWasQuote = true diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index d1f92f8..7033557 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -364,7 +364,7 @@ struct MarkdownRenderer: Sendable { /// Two-plus trailing spaces, or an odd run of trailing backslashes (an /// even run is escaped literal backslashes, e.g. `foo\\`). - private static func endsWithHardBreak(_ line: String) -> Bool { + static func endsWithHardBreak(_ line: String) -> Bool { if line.hasSuffix(" ") { return true } return line.reversed().prefix(while: { $0 == "\\" }).count % 2 == 1 } diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index ea3f00e..8f801ba 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -35,20 +35,42 @@ enum ReferenceLinks { let footnoteIds: [String] } - /// `text` with every resolvable reference inlined, line by line (the - /// renderers take quote bodies line by line, so a reference never spans - /// lines). Unchanged when there is nothing defined or no `[`. + /// `text` with every resolvable reference inlined. Unchanged when there is + /// nothing defined or no `[`. + /// + /// `quoteBody`: `text` is a blockquote / alert body, which the renderers + /// read through `MarkdownRenderer.joinSoftBreaks` — a paragraph's lines + /// are ONE logical line there, so a code span or a link may span lines. + /// Each soft-break group is scanned as that joined line; a group the scan + /// leaves alone keeps its original lines, a changed one becomes its joined + /// line (plus " " when it ended in a hard break), which the renderer's + /// join reproduces exactly. Otherwise (a table cell: one line, no soft + /// breaks) every line is scanned as is. /// /// Footnote references (`[^note]`) become `[^N]` /// (`MarkdownRenderer.numberedFootnoteMarker` + the number): Markdown has /// no inline form for "footnote number N", and a definition-less renderer /// draws that form as the same superscript a paragraph's `[^note]` gets. - static func inlined(_ text: String, definitions: [String: String], footnoteIds: [String] = []) -> String { + static func inlined(_ text: String, definitions: [String: String], footnoteIds: [String] = [], + quoteBody: Bool = false) -> String { guard !definitions.isEmpty || !footnoteIds.isEmpty, text.contains("[") else { return text } let sources = Sources(definitions: definitions, footnoteIds: footnoteIds) - return text.split(separator: "\n", omittingEmptySubsequences: false) - .map { inlinedLine(String($0), sources: sources, linksDisabled: false) } - .joined(separator: "\n") + let lines = text.components(separatedBy: "\n") + guard quoteBody else { + return lines.map { inlinedLine($0, sources: sources, linksDisabled: false) }.joined(separator: "\n") + } + let groups = MarkdownRenderer.joinSoftBreaksIndexed(lines) + var out: [String] = [] + for (index, group) in groups.enumerated() { + let end = index + 1 < groups.count ? groups[index + 1].first : lines.count + let rewritten = inlinedLine(group.line, sources: sources, linksDisabled: false) + if rewritten == group.line { + out.append(contentsOf: lines[group.first.. String { diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 6d8c061..b706a36 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -486,6 +486,41 @@ final class ParserTests: XCTestCase { XCTAssertEqual(String(renderer.renderInline(rows[0][0]).characters), "cell\u{00B9} [^a]", "code span kept") } + /// Quote and alert bodies are rendered with soft breaks joined, so a code + /// span or a link can span source lines: the rewrite scans each joined + /// group (review R1). Code must never show rewritten Markdown, and the + /// whole link text must be linked. + func testQuoteBodyReferencesSpanningLinesMatchTheDefinitionAwareRenderer() { + let theme = MarkdownTheme.cached(for: .light) + let definitions = ["d": "https://e.x/a", "img": "img/logo.png"] + let footnotes = [(id: "n", content: "x")] + let aware = MarkdownRenderer(theme: theme, referenceDefinitions: definitions, footnoteDefinitions: footnotes) + let plain = MarkdownRenderer(theme: theme) + let bodies = [ + "Use `git log [d]\n--oneline` to list.\nRead [the\nguide][d] first.", + "line one [d] \nhard break [the\nguide][d]\n\nnew paragraph ![x][img][^n]", + "- item [d]\n continued [d]\n- second\n## heading [d]\nafter\\\nback [d]", + "no references at all\njust text", + ] + for body in bodies { + let inlined = ReferenceLinks.inlined(body, definitions: definitions, footnoteIds: ["n"], quoteBody: true) + let expected = aware.renderQuotedBody(body) + let actual = plain.renderQuotedBody(inlined) + XCTAssertEqual(String(actual.characters), String(expected.characters), body) + XCTAssertEqual(actual.runs.map { $0.link?.absoluteString }, expected.runs.map { $0.link?.absoluteString }, body) + } + XCTAssertEqual(ReferenceLinks.inlined(bodies[3], definitions: definitions, quoteBody: true), bodies[3], + "untouched groups keep their lines") + + // The parser path (the reported repro). + let blocks = parse("> Use `git log [d]\n> --oneline` to list.\n> Read [the\n> guide][d] first.\n\n[d]: https://e.x/a") + guard case .blockquote(let quote, _) = blocks[0].content else { return XCTFail("quote") } + let shown = plain.renderQuotedBody(quote) + XCTAssertEqual(String(shown.characters), "Use git log [d] --oneline to list. Read the guide first.") + let linked = shown.runs.filter { $0.link != nil }.map { String(shown[$0.range].characters) } + XCTAssertEqual(linked, ["the guide"]) + } + /// The rewrite must draw exactly what the paragraph renderer (which HAS the /// definitions) draws for the same line: characters and links. func testReferenceInliningMatchesTheDefinitionAwareRenderer() { From ddc0540991d9e26632aa496a06be731a2b3ae321 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:28:32 +0100 Subject: [PATCH 050/105] fix(references): resolve an image reference inside an inline link's text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review R2: inline links were copied whole, so the README badge [![build][badge]](https://ci…) — often in a table — showed a literal "![build][badge]" instead of "[Image: build]". When the link text holds an image it is rewritten (links disabled, as the renderer parses it); the destination is still copied as written. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/ReferenceLinks.swift | 11 +++++++++-- QuickMD/QuickMDTests/ParserTests.swift | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index 8f801ba..f13a2c4 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -146,11 +146,18 @@ enum ReferenceLinks { let afterBracket = remaining.index(after: closeBracket) let linkText = String(remaining[remaining.index(after: remaining.startIndex).. (String, Substring) { diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index b706a36..c7ad4d3 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -430,6 +430,7 @@ final class ParserTests: XCTestCase { | A | B | |---|---| | [x][docs] | ![i][img] | + | [![build][img]](https://ci.example) | | [docs]: https://example.com/docs_(v2) [img]: img/logo.png @@ -454,6 +455,10 @@ final class ParserTests: XCTestCase { guard case .table(_, let rows, _, .grid) = blocks[2].content else { return XCTFail("table") } XCTAssertEqual(rows[0], ["[x]()", "![i]()"]) XCTAssertEqual(links(renderer.renderInline(rows[0][0])), ["https://example.com/docs_(v2)"]) + // Review R2: the README badge — an image reference inside an INLINE link. + let badge = renderer.renderInline(rows[1][0]) + XCTAssertEqual(String(badge.characters), "[Image: build]") + XCTAssertEqual(links(badge), ["https://ci.example"]) } /// Footnote references in quotes, alerts and table cells drew `[^note]` @@ -537,6 +542,7 @@ final class ParserTests: XCTestCase { "***[docs]*** and ~~[docs]~~ stay plain, **[docs]** is a link", "[![badge][img]][docs] and ![img] and ![alt][]", "[inline](https://x.y/(a)) next to [docs]", + "[![build][img]](https://ci.example/b) and [![x](i.png)](https://ci.example/c) and [t ![i][img]]()", "[unknown][nope] [unknown] [docs][nope]", "[bad] and [paren] escape what the <…> form needs", "a ` lone backtick [docs] then ``` three ``` [docs]", From 40a29da9a6545d3c4f3c9cb569a3640073d9d1af Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:30:18 +0100 Subject: [PATCH 051/105] fix(links): URL(string:) first, our encoder only when it fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review R3: linkURL sent destinations made of URL characters straight to URL(string:) with no fallback, so on macOS 13 (strict parse) docs/a[1].md, https://x/?a[]=1 and a#b#c got no link at all, and on 14+ an IDN host (https://bücher.de/x) was percent-encoded instead of punycoded by the OS. URL(string:) now runs first exactly as before 1.14.4; only when it returns nil are the part before the first # and the fragment encoded separately ([, ], a later # and spaces included). A parse seam lets the tests emulate macOS 13's strict parser. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownRenderer.swift | 43 +++++++------------ .../QuickMDTests/SectionExtractorTests.swift | 28 ++++++++++++ 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 7033557..6af9198 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -962,28 +962,26 @@ struct MarkdownRenderer: Sendable { return (attr, remaining) } - /// The URL of a link destination as written, the SAME on every macOS. + /// The URL of a link destination as written. /// - /// `URL(string:)` rejects a space or non-ASCII character on macOS 13 but - /// percent-encodes it on 14+, and the old fallback for 13 encoded the - /// whole string with `.urlPathAllowed` — the `#` too, so - /// `README.md#Install Guide` became a file named `README.md#Install Guide` - /// and the jump failed. Here a destination made only of characters a URL - /// may contain goes to `URL(string:)` as is (nothing for the OS to - /// encode); anything else is encoded by us, the part before the first `#` - /// and the fragment separately, so the `#` stays the fragment delimiter. - /// A `%` followed by two hex digits is an escape and kept; any other `%` - /// becomes `%25`. - static func linkURL(for destination: String) -> URL? { + /// `URL(string:)` first, exactly as before 1.14.4: on macOS 14+ it encodes + /// what needs encoding itself (spaces, non-ASCII, IDN hosts to punycode), + /// and every destination it accepts keeps that behaviour. Only when it + /// returns nil — macOS 13 rejects spaces, non-ASCII, `[` `]` in a path, a + /// second `#` — do we encode, the part before the FIRST `#` and the + /// fragment separately, so the `#` stays the fragment delimiter (the old + /// `.urlPathAllowed` fallback encoded it too: `README.md#Install Guide` + /// became a file named "README.md#Install Guide" and the jump failed). + /// `parse` is `URL(string:)`; tests pass the strict (macOS 13) parser. + static func linkURL(for destination: String, + parse: (String) -> URL? = { URL(string: $0) }) -> URL? { + if let url = parse(destination) { return url } let scalars = destination.unicodeScalars[...] - if !needsEncoding(scalars) { - return URL(string: destination) - } guard let hash = scalars.firstIndex(of: "#") else { - return URL(string: percentEncoded(scalars)) + return parse(percentEncoded(scalars)) } - return URL(string: percentEncoded(scalars[.. Bool { - var index = scalars.startIndex - while index < scalars.endIndex { - if !urlCharacters.contains(scalars[index]), !isEscape(scalars, at: index) { return true } - index = scalars.index(after: index) - } - return false - } - /// One part of a destination (no `#` delimiter inside): legal characters /// and valid escapes kept, everything else — `#`, `[`, `]` too, which a /// URL allows only in specific places — as UTF-8 `%XX`. diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 4b7061c..65fe408 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -272,6 +272,34 @@ final class SectionExtractorTests: XCTestCase { XCTAssertNil(fragment("docs/install")) } + /// Review R3: `URL(string:)` comes first, so every destination macOS + /// accepts keeps its URL exactly (IDN hosts included on 14+); our encoder + /// only runs when the parse fails — on macOS 13 that includes `[` `]` in a + /// path and a second `#`, which must still become links. + func testLinkURLFallsBackOnlyWhenTheParseFails() throws { + guard #available(macOS 14, *) else { throw XCTSkip("needs the strict-parse API to emulate macOS 13") } + let strict: (String) -> URL? = { URL(string: $0, encodingInvalidCharacters: false) } + let cases: [(String, String)] = [ + ("docs/a[1].md", "docs/a%5B1%5D.md"), + ("https://x/?a[]=1", "https://x/?a%5B%5D=1"), + ("a#b#c", "a#b%23c"), + ("README.md#Install Guide", "README.md#Install%20Guide"), + ("https://example.com/docs_(v2)#top", "https://example.com/docs_(v2)#top"), + ] + for (destination, expected) in cases { + let url = try XCTUnwrap(MarkdownRenderer.linkURL(for: destination, parse: strict), destination) + XCTAssertEqual(url.absoluteString, expected, destination) + } + XCTAssertEqual(try XCTUnwrap(MarkdownRenderer.linkURL(for: "a#b#c", parse: strict)) + .fragment(percentEncoded: false), "b#c") + + // The default parse (this OS): whatever URL(string:) gives, unchanged. + for destination in ["https://bücher.de/x", "docs/a[1].md", "https://x/?a[]=1", "README.md#Install Guide", + "a%20b.md#x", "mailto:a@b.c"] { + XCTAssertEqual(MarkdownRenderer.linkURL(for: destination), URL(string: destination), destination) + } + } + /// `other.md#section`: the fragment is parked for the other document's /// window (which takes it once laid out, or at once when already open). func testCrossDocumentAnchorIsHandedToTheOtherDocument() throws { From c7a46ee4169f33ec2eb69c2351fa2143a650b892 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:31:39 +0100 Subject: [PATCH 052/105] fix(links): cross-document anchors key on the symlink-resolved path Review R4: the key used standardizedFileURL only, so a link through a symlinked folder (or /tmp vs /private/tmp) parked the fragment under a path the opened window never asked for. Park and take both resolve symlinks now. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/CrossDocumentAnchors.swift | 8 +++++--- .../QuickMDTests/SectionExtractorTests.swift | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/QuickMD/QuickMD/CrossDocumentAnchors.swift b/QuickMD/QuickMD/CrossDocumentAnchors.swift index 19e2edc..4a6de92 100644 --- a/QuickMD/QuickMD/CrossDocumentAnchors.swift +++ b/QuickMD/QuickMD/CrossDocumentAnchors.swift @@ -30,9 +30,11 @@ final class CrossDocumentAnchors: @unchecked Sendable { private let lock = NSLock() private var pending: [String: (fragment: String, date: Date)] = [:] - /// The identity of a document file: the path as `handleLinkActivation` - /// compares documents (`standardizedFileURL`). - static func key(for fileURL: URL) -> String { fileURL.standardizedFileURL.path } + /// The identity of a document file: its path with `.`/`..` and symlinks + /// resolved, so a link through a symlinked folder (or `/tmp` vs + /// `/private/tmp`) reaches the window that opened the real path. The same + /// function on both sides (park and take). + static func key(for fileURL: URL) -> String { fileURL.standardizedFileURL.resolvingSymlinksInPath().path } /// The fragment of a link to another document, percent-decoded (`#Install /// Guide` arrives as `Install%20Guide`); nil when the link has none. diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 65fe408..caf1df1 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -330,6 +330,26 @@ final class SectionExtractorTests: XCTestCase { XCTAssertNil(anchors.take(for: target, now: now), "and it is gone") } + /// Review R4: the link may name the file through a symlinked folder while + /// the window shows the real path (or the other way round). + func testCrossDocumentAnchorKeyResolvesSymlinks() throws { + let fm = FileManager.default + let root = fm.temporaryDirectory.appendingPathComponent("quickmd-anchors-\(UUID().uuidString)") + let real = root.appendingPathComponent("real") + try fm.createDirectory(at: real, withIntermediateDirectories: true) + defer { try? fm.removeItem(at: root) } + let file = real.appendingPathComponent("other.md") + try "# Intro".write(to: file, atomically: false, encoding: .utf8) + let alias = root.appendingPathComponent("alias") + try fm.createSymbolicLink(at: alias, withDestinationURL: real) + + let anchors = CrossDocumentAnchors() + anchors.request("intro", for: alias.appendingPathComponent("other.md")) + XCTAssertEqual(anchors.take(for: file), "intro", "parked via the symlink, taken by the real path") + anchors.request("intro", for: file) + XCTAssertEqual(anchors.take(for: alias.appendingPathComponent("./other.md")), "intro") + } + /// A link destination becomes the same URL on every macOS: macOS 13's /// `URL(string:)` rejects spaces / non-ASCII (14+ encodes them), and the /// old 13 fallback encoded the `#` too, so `README.md#Install Guide` was a From 8d3649adf12d14454ea8ff91e41157a1ce4e95da Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:32:40 +0100 Subject: [PATCH 053/105] docs(readme): the reading column grows with zoom Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2eaaec..1029b2e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Perfect for developers, writers, students, and anyone who works with Markdown da - Find in document (`⌘F`) with match count and per-word navigation - Word-level highlighting across all block types (text, code, tables, blockquotes) - Table of Contents sidebar (`⌘⇧T`) — auto-generated from headings; right-click it to list every level or stop at H2, H3 or H4 -- Reading mode (`⌘⇧R`) — hides both sidebars and the hover buttons and centres the text in a 720 pt column; `Esc` brings everything back +- Reading mode (`⌘⇧R`) — hides both sidebars and the hover buttons and centres the text in a comfortable column (720 pt at 100 %, growing with zoom); `Esc` brings everything back - **Select across the whole document** — drag through paragraphs, headings, lists, quotes, code and tables (a word, a cell or a range of cells), it scrolls for you, Shift-click to extend, `⌘A` / `⌘C` to copy clean text (table cells as tab-separated values, math as LaTeX). Every copy shows how many characters and words were copied; optional auto-copy of selections in Settings → General - Copy entire document (`⌘⇧C`) or individual sections (hover heading → copy icon) - Export to PDF (`⌘⇧E`) — **vector text** (selectable, searchable) with **rendered Mermaid diagrams** — and Print (`⌘P`) From a6334059e7748f3f0e6146871dc886263de22b96 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:33:40 +0100 Subject: [PATCH 054/105] fix(text): every ASCII punctuation escapes; the Contents sidebar shows the heading's text Review R5: the sidebar decoded the RAW title, so `<x>` in a code span showed there but <x> in the heading. ToCEntry.shownTitle is renderHeader's characters, the text the heading shows and its id is made from (markup is gone in the sidebar too). escapableChars was a subset of CommonMark's: \& showed "\&" where GitHub shows "&". It is now every ASCII punctuation character except $: inline math is found in the rendered text, so \$ keeps its backslash rather than becoming a math delimiter. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownTheme.swift | 11 +++++++++-- QuickMD/QuickMD/SectionExtractor.swift | 7 +++++++ QuickMD/QuickMD/Views/TableOfContentsView.swift | 4 ++-- QuickMD/QuickMDTests/RendererTests.swift | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownTheme.swift b/QuickMD/QuickMD/MarkdownTheme.swift index fd259a8..26e829f 100644 --- a/QuickMD/QuickMD/MarkdownTheme.swift +++ b/QuickMD/QuickMD/MarkdownTheme.swift @@ -307,8 +307,15 @@ struct MarkdownTheme: Sendable, Identifiable { // MARK: - Escape Characters /// Characters that can be escaped with backslash in markdown - /// Example: \* renders as literal asterisk instead of italic marker - static let escapableChars: Set = ["\\", "`", "*", "_", "{", "}", "[", "]", "(", ")", "#", "+", "-", ".", "!", "|"] + /// Example: \* renders as literal asterisk instead of italic marker. + /// CommonMark's full list — every ASCII punctuation character (`\&` + /// shows "&", `\<` shows "<", as on GitHub) — EXCEPT `$`: inline math + /// is found in the RENDERED text (`InlineMathSegmenter`), so a `\$` that + /// rendered as a bare `$` would open a formula; it keeps its backslash. + static let escapableChars: Set = [ + "!", "\"", "#", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", + "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", + ] // MARK: - Reference Link Definitions diff --git a/QuickMD/QuickMD/SectionExtractor.swift b/QuickMD/QuickMD/SectionExtractor.swift index 080bd6e..e465de5 100644 --- a/QuickMD/QuickMD/SectionExtractor.swift +++ b/QuickMD/QuickMD/SectionExtractor.swift @@ -11,6 +11,13 @@ struct ToCEntry: Identifiable, Sendable { let level: Int let title: String let sourceLine: Int + + /// What the Contents sidebar lists: exactly the text the heading shows + /// (`renderHeader`'s characters — markup gone, references decoded, a code + /// span's `<` kept), the same text the heading's id is made from. + func shownTitle(renderer: MarkdownRenderer) -> String { + String(renderer.renderHeader(title, level: level).characters) + } } // MARK: - Contents Depth diff --git a/QuickMD/QuickMD/Views/TableOfContentsView.swift b/QuickMD/QuickMD/Views/TableOfContentsView.swift index 8e7a82f..061e4c7 100644 --- a/QuickMD/QuickMD/Views/TableOfContentsView.swift +++ b/QuickMD/QuickMD/Views/TableOfContentsView.swift @@ -104,8 +104,8 @@ private struct ToCEntryRow: View { Button { onSelect() } label: { - // Character references as the heading shows them (`Q&A` → `Q&A`). - Text(HTMLEntities.decode(entry.title)) + // The text the heading shows (markup gone, `Q&A` → `Q&A`). + Text(entry.shownTitle(renderer: MarkdownRenderer(theme: theme))) .font(.system(size: fontSize(for: entry.level), weight: fontWeight(for: entry.level))) .foregroundColor(theme.textColor) .lineLimit(2) diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 6bd9774..1fdef30 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -810,4 +810,21 @@ final class RendererTests: XCTestCase { } } + + /// Review R5: CommonMark lets a backslash escape ANY ASCII punctuation + /// (`\&` is the text "&"); expected from `gh api markdown`. `\$` + /// is the one exception (keeps its backslash: inline math is found in the + /// rendered text). The Contents sidebar shows exactly the heading's text. + func testEveryASCIIPunctuationEscapesAndTheToCShowsTheHeadingText() { + XCTAssertEqual(rendered("a \\& \\< \\~x\\~ \\\" \\@ \\^ \\, b"), "a & < ~x~ \" @ ^ , b") + XCTAssertEqual(rendered("\\$5"), "\\$5", "no bare $ that inline math would take") + let heading = String(renderer.renderHeader("\\& `<x>`", level: 1).characters) + XCTAssertEqual(heading, "& <x>") + + let entry = ToCEntry(id: "heading-0", level: 1, title: "\\& `<x>` **Q&A**", sourceLine: 0) + XCTAssertEqual(entry.shownTitle(renderer: renderer), "& <x> Q&A", "the ToC row = the heading") + XCTAssertEqual(entry.shownTitle(renderer: renderer), + String(renderer.renderHeader(entry.title, level: 1).characters)) + } + } From 256689a1cc2aa9280b4e998a7b189eb54b65a720 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:34:29 +0100 Subject: [PATCH 055/105] test(watcher): permission tests wait for the event, skip as root Review R7: the unreadable-file tests waited a fixed 1.5 s for the watcher, and a mode-000 file is readable for root. They now wait for the onFileUnreadable expectation (10 s timeout, fulfilled as soon as it fires) and skip under root. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMDTests/FileWatcherTests.swift | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMDTests/FileWatcherTests.swift b/QuickMD/QuickMDTests/FileWatcherTests.swift index f5e6296..fdb83e0 100644 --- a/QuickMD/QuickMDTests/FileWatcherTests.swift +++ b/QuickMD/QuickMDTests/FileWatcherTests.swift @@ -172,9 +172,14 @@ final class FileWatcherTests: XCTestCase { /// stops instead of failing every second and reports it unreadable, once — /// restoring the permissions later is NOT noticed (pre-poll behaviour). func testPermissionDeniedAfterReturnStopsThePoll() throws { + try XCTSkipIf(getuid() == 0, "root reads a mode-000 file") var missingCount = 0 var unreadableCount = 0 - watcher.onFileUnreadable = { unreadableCount += 1 } + let denied = expectation(description: "a poll found it unreadable") + watcher.onFileUnreadable = { + unreadableCount += 1 + denied.fulfill() + } let missing = expectation(description: "onFileMissing after delete") watcher.onFileMissing = { missingCount += 1 @@ -189,10 +194,8 @@ final class FileWatcherTests: XCTestCase { XCTAssertEqual(chmod(fileURL.path, 0), 0) defer { chmod(fileURL.path, 0o644) } - // One poll sees it, fails to open it, stops. - let denied = expectation(description: "a poll ran") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { denied.fulfill() } - wait(for: [denied], timeout: 3) + // One poll sees it, fails to open it, reports it, stops. + wait(for: [denied], timeout: 10) XCTAssertEqual(chmod(fileURL.path, 0o644), 0) let settle = expectation(description: "no poll after the denial") @@ -205,10 +208,15 @@ final class FileWatcherTests: XCTestCase { /// `chmod 000` on the open document (or a sandbox denial): the file is /// there, so the window must not say it no longer exists. func testUnreadableFileIsNotReportedAsMissing() throws { + try XCTSkipIf(getuid() == 0, "root reads a mode-000 file") watcher.onFileMissing = { XCTFail("an unreadable file is not a missing one") } watcher.onChange = { XCTFail("nothing changed") } var unreadableCount = 0 - watcher.onFileUnreadable = { unreadableCount += 1 } + var replaced: XCTestExpectation? + watcher.onFileUnreadable = { + unreadableCount += 1 + replaced?.fulfill() + } XCTAssertEqual(chmod(fileURL.path, 0), 0) defer { chmod(fileURL.path, 0o644) } @@ -222,10 +230,9 @@ final class FileWatcherTests: XCTestCase { let replacement = tempDir.appendingPathComponent("replacement.md") try "locked".write(to: replacement, atomically: false, encoding: .utf8) XCTAssertEqual(chmod(replacement.path, 0), 0) + replaced = expectation(description: "the replace is reported unreadable") XCTAssertEqual(rename(replacement.path, fileURL.path), 0) - let settle = expectation(description: "rename handled, no poll") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { settle.fulfill() } - wait(for: [settle], timeout: 3) + wait(for: [replaced!], timeout: 10) XCTAssertEqual(unreadableCount, 2) XCTAssertEqual(FileWatcher.Problem.missing.bannerText(path: "/a/b.md"), "File no longer exists at /a/b.md") From b0d8e5337b12bd9d997d439f5ab09bee7f372acd Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:36:27 +0100 Subject: [PATCH 056/105] feat(images): a line that is only one linked image is an image block [![alt](img)](url) alone on a line (inline or reference forms, spaces around allowed) rendered as a paragraph with an italic "[Image: alt]" on screen and in PDF, while ![alt](img) and draw the picture. InlineSyntaxScanner.linkedImage recognises it by the renderer's link rules and the parser emits the same .image block as a plain image line (same sourceLine, alt for search and copy). The link target is NOT clickable on that image, exactly as for today: clicking opens the preview. Links on images (both forms) and badge rows (several linked images on one line, still a paragraph) are a 1.15 feature. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 40 +++++++++++++++++++ QuickMD/QuickMD/MarkdownBlockParser.swift | 13 ++++++ QuickMD/QuickMDTests/ParserTests.swift | 48 +++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index a9771e7..9e8cdb0 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -171,6 +171,46 @@ enum InlineSyntaxScanner { } } + /// A line that is nothing but ONE linked image — `[![alt](img)](url)`, + /// `[![alt][logo]][site]` or any link form `MarkdownRenderer.tryParseLink` + /// accepts around one image, whitespace around it allowed — as the image + /// it shows. The link target is NOT returned: the parser makes the line an + /// ordinary image block (click = preview), as `` is; + /// links on images are a later feature. Text beside the link, a second + /// image or an outer link that does not resolve → nil (a paragraph, as + /// before). + static func linkedImage(in line: Substring, references: [String: String]) -> ImageSyntax? { + let scalars = line.unicodeScalars + guard let first = scalars.firstIndex(where: { !$0.properties.isWhitespace }), + let last = scalars.lastIndex(where: { !$0.properties.isWhitespace }) else { return nil } + // Cheap reject before any copy: most lines are not this. + guard Substring(scalars[first...last]).utf8.starts(with: "[![".utf8) else { return nil } + let text = String(Substring(scalars[first...last])) + let pairs = PairTable(text) + guard let close = closingBracket(in: text[...], pairs: pairs) else { return nil } + let linkText = text[text.index(after: text.startIndex).. Bool { diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 7fce9c1..7b91cd6 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -238,6 +238,19 @@ struct MarkdownBlockParser: Sendable { continue } + // A line that is only ONE linked image (`[![alt](img)](url)`, the + // README logo/badge form): the same `.image` block as `![alt](img)` + // — drawn on screen and in PDF, click = preview. The link target is + // dropped, as it is for `` (links on images: 1.15). + if let image = InlineSyntaxScanner.linkedImage(in: line[...], references: referenceDefinitions) { + flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) + blocks.append(.image(index: blockIndex, url: image.url, alt: HTMLEntities.decode(image.alt), + width: nil, sourceLine: sourceLine)) + blockIndex += 1 + i += 1 + continue + } + // HTML image line (T-C): nothing but `` tags and layout // wrappers (`

`, `

`, ``, …), possibly an // `` is an diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index c7ad4d3..9645362 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -414,6 +414,54 @@ final class ParserTests: XCTestCase { XCTAssertEqual(rows[4], ["`p\\|q`", "c"]) } + // MARK: - A line that is only one linked image (B16) + + /// `[![alt](img)](url)` alone on a line is the picture, like `![alt](img)` + /// and `` — an `.image` block on that line (Section copy, + /// search of the alt and copy work as for a plain image line). The link + /// target is not kept (as for ``). Text beside it, or two linked + /// images (a badge row), stay a paragraph. + func testLineOfOneLinkedImageIsAnImageBlock() { + let md = """ + # Title + [![Logo](img/logo.png)](https://example.com) + [![Badge][shield]][site]\u{0020}\u{0020} + ![Plain](img/plain.png) + text before [![a](a.png)](https://a.example) + + [![a](a.png)](https://a.example) text after + + [![a](a.png)](https://a.example) [![b](b.png)](https://b.example) + + [![a](a.png)][undefined] + + [shield]: https://img.shields.io/x.svg + [site]: https://example.com/site + """ + let blocks = parse(md) + let images: [(url: String, alt: String, line: Int)] = blocks.compactMap { + guard case .image(let url, let alt, nil) = $0.content else { return nil } + return (url, alt, $0.sourceLine) + } + XCTAssertEqual(images.map(\.url), ["img/logo.png", "https://img.shields.io/x.svg", "img/plain.png"]) + XCTAssertEqual(images.map(\.alt), ["Logo", "Badge", "Plain"]) + XCTAssertEqual(images.map(\.line), [1, 2, 3], "each on its own source line") + + let paragraphs = blocks.compactMap(plainText) + XCTAssertEqual(paragraphs.count, 4, "\(paragraphs)") + XCTAssertTrue(paragraphs[0].hasPrefix("text before [Image: a]")) + XCTAssertEqual(paragraphs[1], "[Image: a] text after") + XCTAssertEqual(paragraphs[2], "[Image: a] [Image: b]", "a badge row stays a paragraph (1.15)") + XCTAssertTrue(paragraphs[3].contains("[undefined]"), "no link → not a linked image") + + // Exactly what a plain `![Logo](img/logo.png)` line parses to, id aside. + guard case .image(let url, let alt, let width) = parse("![Logo](img/logo.png)")[0].content, + case .image(let linkedURL, let linkedAlt, let linkedWidth) = + parse("[![Logo](img/logo.png)](https://example.com)")[0].content else { return XCTFail("images") } + XCTAssertEqual([url, alt], [linkedURL, linkedAlt]) + XCTAssertNil(width); XCTAssertNil(linkedWidth) + } + // MARK: - Reference links in quotes, alerts and table cells /// Quotes, alerts and table cells keep their markdown and are rendered From 63da3c4a81be3af37c7d84c44e6670b9a0eebca4 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:37:15 +0100 Subject: [PATCH 057/105] test(settings): SettingsTab migration test in its own SettingsTabTests file Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD.xcodeproj/project.pbxproj | 4 +++ .../QuickMDTests/SectionExtractorTests.swift | 23 ---------------- QuickMD/QuickMDTests/SettingsTabTests.swift | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 QuickMD/QuickMDTests/SettingsTabTests.swift diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 35433e7..7696463 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ A1B2C3D401000232 /* ReviewPromptTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000231 /* ReviewPromptTrigger.swift */; }; A1B2C3D401000233 /* ReviewPromptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000232 /* ReviewPromptTests.swift */; }; A1B2C3D401000300 /* TemporaryDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000300 /* TemporaryDefaults.swift */; }; + A1B2C3D40100030B /* SettingsTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000030B /* SettingsTabTests.swift */; }; A1B2C3D401000201 /* PrintableTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000200 /* PrintableTableTests.swift */; }; A1B2C3D4010001C0 /* SelectionRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; A1B2C3D4010001C1 /* SelectionRows.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D4000001C0 /* SelectionRows.swift */; }; @@ -242,6 +243,7 @@ A1B2C3D400000231 /* ReviewPromptTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptTrigger.swift; sourceTree = ""; }; A1B2C3D400000232 /* ReviewPromptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptTests.swift; sourceTree = ""; }; A1B2C3D400000300 /* TemporaryDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryDefaults.swift; sourceTree = ""; }; + A1B2C3D40000030B /* SettingsTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTabTests.swift; sourceTree = ""; }; A1B2C3D400000200 /* PrintableTableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintableTableTests.swift; sourceTree = ""; }; A1B2C3D4000001C0 /* SelectionRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRows.swift; sourceTree = ""; }; A1B2C3D4000002A0 /* VerticalRhythm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalRhythm.swift; sourceTree = ""; }; @@ -364,6 +366,7 @@ A1B2C3D400000221 /* FrontMatterTests.swift */, A1B2C3D400000232 /* ReviewPromptTests.swift */, A1B2C3D400000300 /* TemporaryDefaults.swift */, + A1B2C3D40000030B /* SettingsTabTests.swift */, A1B2C3D400000200 /* PrintableTableTests.swift */, A1B2C3D4000001C1 /* TableSelectionTests.swift */, A1B2C3D4000000C1 /* DocumentSelectionTests.swift */, @@ -795,6 +798,7 @@ A1B2C3D401000222 /* FrontMatterTests.swift in Sources */, A1B2C3D401000233 /* ReviewPromptTests.swift in Sources */, A1B2C3D401000300 /* TemporaryDefaults.swift in Sources */, + A1B2C3D40100030B /* SettingsTabTests.swift in Sources */, A1B2C3D401000201 /* PrintableTableTests.swift in Sources */, A1B2C3D4010001C2 /* TableSelectionTests.swift in Sources */, ); diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index caf1df1..622c7d0 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -125,29 +125,6 @@ final class SectionExtractorTests: XCTestCase { XCTAssertEqual([0, 1, 2, 3, 4, 5, 6, 7, 42].map(ToCDepth.normalized), [2, 2, 2, 3, 4, 6, 6, 6, 6]) } - // MARK: - Settings tab (remembered by identifier) - - /// The Settings tab was remembered by INDEX; the update reads that index - /// once (when the identifier key is absent) and maps it to the same tab. - func testSettingsTabMigratesTheLegacyIndex() throws { - XCTAssertEqual(SettingsTab.allCases, [.general, .themes, .fonts, .editor], "legacy index order") - XCTAssertEqual([0, 1, 2, 3].map(SettingsTab.fromLegacyIndex), [.general, .themes, .fonts, .editor]) - XCTAssertEqual([-1, 4, 99].map(SettingsTab.fromLegacyIndex), [.general, .general, .general]) - XCTAssertEqual(SettingsTab.fromLegacyIndex(nil), .general) - - let suite = try TemporaryDefaults("SettingsTab") - defer { suite.remove() } - let defaults = suite.defaults - XCTAssertEqual(SettingsTab.initial(in: defaults), .general, "fresh install") - defaults.set(2, forKey: SettingsTab.legacyIndexKey) - XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "the tab SwiftUI remembered") - defaults.set(SettingsTab.editor.rawValue, forKey: SettingsTab.defaultsKey) - XCTAssertEqual(SettingsTab.initial(in: defaults), .editor, "the identifier wins once written") - defaults.set("removed-tab", forKey: SettingsTab.defaultsKey) - XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "an unknown identifier falls back") - XCTAssertEqual(SettingsTab.legacyIndexKey, "com_apple_SwiftUI_Settings_selectedTabIndex") - } - // MARK: - Heading anchors (#43) private func anchorHeadings(of markdown: String) -> [(id: String, text: String)] { diff --git a/QuickMD/QuickMDTests/SettingsTabTests.swift b/QuickMD/QuickMDTests/SettingsTabTests.swift new file mode 100644 index 0000000..b0416b6 --- /dev/null +++ b/QuickMD/QuickMDTests/SettingsTabTests.swift @@ -0,0 +1,27 @@ +import XCTest + +/// Settings remembers its tab by identifier (`SettingsTab`), migrating the +/// index SwiftUI used to remember. +final class SettingsTabTests: XCTestCase { + + /// The Settings tab was remembered by INDEX; the update reads that index + /// once (when the identifier key is absent) and maps it to the same tab. + func testSettingsTabMigratesTheLegacyIndex() throws { + XCTAssertEqual(SettingsTab.allCases, [.general, .themes, .fonts, .editor], "legacy index order") + XCTAssertEqual([0, 1, 2, 3].map(SettingsTab.fromLegacyIndex), [.general, .themes, .fonts, .editor]) + XCTAssertEqual([-1, 4, 99].map(SettingsTab.fromLegacyIndex), [.general, .general, .general]) + XCTAssertEqual(SettingsTab.fromLegacyIndex(nil), .general) + + let suite = try TemporaryDefaults("SettingsTab") + defer { suite.remove() } + let defaults = suite.defaults + XCTAssertEqual(SettingsTab.initial(in: defaults), .general, "fresh install") + defaults.set(2, forKey: SettingsTab.legacyIndexKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "the tab SwiftUI remembered") + defaults.set(SettingsTab.editor.rawValue, forKey: SettingsTab.defaultsKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .editor, "the identifier wins once written") + defaults.set("removed-tab", forKey: SettingsTab.defaultsKey) + XCTAssertEqual(SettingsTab.initial(in: defaults), .fonts, "an unknown identifier falls back") + XCTAssertEqual(SettingsTab.legacyIndexKey, "com_apple_SwiftUI_Settings_selectedTabIndex") + } +} From a8512297be05f9c436cd2e9afb47d04956fc7412 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:37:47 +0100 Subject: [PATCH 058/105] fix(parser): block markers take spaces and tabs only; the block loop always advances A quote marker after a no-break space hung the parser. The block loop entered the quote branch on a line trimmed of every Unicode space, the quote's own loop (spaces and tabs only since 9bbf6ae) refused the same line without consuming it, and the branch re-entered it for ever: 100 % CPU, the document never shown, one more spinning task per re-parse. CommonMark indents with spaces and tabs only, and GitHub (gh api markdown) shows a line with a no-break, em or ideographic space before a block or list marker as paragraph text, as it does `#` + no-break space, `1.` + no-break space and a rule followed by one. Now: - MarkdownBlockParser.markerLine (spaces and tabs) is the one line every block marker is looked for in, at the branch that decides and in the loop that walks the block: fences (opening and closing), `$$` (opening and closing), quotes, setext title and underline, table separators, front-matter fences, link and footnote definitions, definition-list terms. - MarkdownRenderer.markerWhitespace and markerLine for list items, rules, task items and ordered-list hangs; MarkdownTheme's marker patterns say [ \t] where they said \s (header, rule, task, table separator, setext, link and footnote definitions), as does the ordered marker. - Every turn of the block loop consumes at least one line: DEBUG asserts, Release keeps the line as text and goes on. Tests parse on a worker thread with a 5 s timeout, so a hang fails instead of stalling the suite. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 56 ++++++--- QuickMD/QuickMD/MarkdownRenderer.swift | 31 +++-- QuickMD/QuickMD/MarkdownTheme.swift | 16 +-- .../QuickMDTests/VerticalRhythmTests.swift | 113 ++++++++++++++++++ 4 files changed, 183 insertions(+), 33 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 0f29947..51372d3 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -45,11 +45,11 @@ struct MarkdownBlockParser: Sendable { // this step the setext-H2 rule would promote the first frontmatter key // to a giant heading. var firstContentLine = 0 - if allLines.first?.trimmingCharacters(in: .whitespaces) == "---" { + if allLines.first.map(Self.markerLine) == "---" { var closingIndex: Int? var j = 1 while j < allLines.count { - let t = allLines[j].trimmingCharacters(in: .whitespaces) + let t = Self.markerLine(allLines[j]) if t == "---" || t == "..." { closingIndex = j; break } j += 1 } @@ -86,7 +86,7 @@ struct MarkdownBlockParser: Sendable { var lineMap: [Int] = [] for originalIndex in firstContentLine.. \u{00A0}`) is text, not an empty quote line. - let qLine = lines[i].trimmingCharacters(in: Self.spacesAndTabs) + // The predicate the branch was entered on (`markerLine`), + // so the first line is always taken. + let qLine = Self.markerLine(lines[i]) guard qLine.hasPrefix(">") else { break } // Count nesting level (number of leading > chars) @@ -367,7 +380,7 @@ struct MarkdownBlockParser: Sendable { i += 1 while i < lines.count { - let nextLine = lines[i].trimmingCharacters(in: Self.spacesAndTabs) + let nextLine = Self.markerLine(lines[i]) guard nextLine.hasPrefix(">") else { break } var nextLevel = 0 @@ -503,7 +516,7 @@ struct MarkdownBlockParser: Sendable { /// Check if line matches table separator pattern (e.g., |---|---|) private func isTableSeparator(_ line: String) -> Bool { - line.trimmingCharacters(in: .whitespaces) + Self.markerLine(line) .range(of: MarkdownTheme.tableSeparatorPattern, options: .regularExpression) != nil } @@ -615,7 +628,18 @@ struct MarkdownBlockParser: Sendable { /// CommonMark's white space around block markers: spaces and tabs, not /// every Unicode space separator (`isBlank`). - static let spacesAndTabs = CharacterSet(charactersIn: " \t") + static let spacesAndTabs = MarkdownRenderer.markerWhitespace + + /// `line` without the spaces and tabs around it: what EVERY block marker + /// (fence, `$$`, quote, setext underline, table separator, front-matter + /// fence, link definition) is looked for in, at the branch that decides + /// AND in the loop that then walks the block. CommonMark indents with + /// spaces and tabs only — a no-break, em or ideographic space before a + /// marker leaves the line paragraph text, as on GitHub. Two predicates + /// for one decision once hung the parser (a quote marker after U+00A0). + static func markerLine(_ line: String) -> String { + line.trimmingCharacters(in: spacesAndTabs) + } /// CommonMark's blank line (`MarkdownRenderer.isBlankLine`): a line of /// no-break spaces is text, not a separator. @@ -634,7 +658,7 @@ struct MarkdownBlockParser: Sendable { /// definitions: a shortcut-reference image line (`![logo]`) is an image /// block, so it must not be a term either. private func isTermCandidate(_ line: String, references: [String: String]) -> Bool { - let trimmed = line.trimmingCharacters(in: .whitespaces) + let trimmed = Self.markerLine(line) guard !Self.isBlank(line), Self.definitionText(of: line) == nil else { return false } // Fences, quotes, display math @@ -643,7 +667,7 @@ struct MarkdownBlockParser: Sendable { // List items — bullets (task items included, they start `- [ ]`) and ordered if ["- ", "* ", "+ "].contains(where: { trimmed.hasPrefix($0) }) { return false } - if trimmed.range(of: #"^(\d+)\.\s"#, options: .regularExpression) != nil { return false } + if trimmed.range(of: #"^(\d+)\.[ \t]"#, options: .regularExpression) != nil { return false } // Table rows (the table branch needs a separator to fire, but a pipe row // is still a table row, not a term) and separators diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 7024773..d6dc714 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -234,7 +234,7 @@ struct MarkdownRenderer: Sendable { /// `(prefix, level)` of an ordered list item, prefix exactly as /// `renderListItem` will emit it (indent spaces + normalised number). private static func orderedItem(_ line: String) -> (prefix: String, level: Int)? { - let trimmed = line.trimmingCharacters(in: .whitespaces) + let trimmed = markerLine(line) guard trimmed.range(of: Self.orderedMarkerPattern, options: .regularExpression) != nil else { return nil } let level = listLevel(for: line.prefix(while: { $0 == " " || $0 == "\t" })) let number = Int(trimmed.prefix(while: { $0.isNumber })) ?? 1 @@ -333,6 +333,19 @@ struct MarkdownRenderer: Sendable { line.allSatisfy { $0 == " " || $0 == "\t" } } + /// CommonMark's white space around block and list markers: spaces and + /// tabs only. A no-break, em or ideographic space before a marker (or + /// after `#`, a list marker or a rule) leaves the line paragraph text, as + /// on GitHub; the patterns in `MarkdownTheme` say `[ \t]` for the same + /// reason. `MarkdownBlockParser.markerLine` trims with it too. + static let markerWhitespace = CharacterSet(charactersIn: " \t") + + /// `line` without the spaces and tabs around it — what every list, + /// rule and heading marker is looked for in. + private static func markerLine(_ line: String) -> String { + line.trimmingCharacters(in: markerWhitespace) + } + /// A continuation line's text: CommonMark strips its leading spaces and /// tabs, and nothing else (a no-break space is text). private static func continuationText(_ text: String) -> String { @@ -342,13 +355,13 @@ struct MarkdownRenderer: Sendable { /// An ordered item's marker (CommonMark): one to nine digits, then `.` or /// `)`, then white space. Drawn as `n.` whichever delimiter it had, as /// GitHub draws both (an HTML list). - static let orderedMarkerPattern = #"^(\d{1,9})[.)]\s"# + static let orderedMarkerPattern = #"^(\d{1,9})[.)][ \t]"# /// `line` (an ordered item line that may not interrupt a paragraph) as /// paragraph text: its delimiter backslash-escaped, so `renderLine` draws /// `1984. A year` as the words it is and never as an item. static func asParagraphText(_ line: String) -> String { - line.replacingOccurrences(of: #"^(\s*\d{1,9})([.)])"#, with: "$1\\\\$2", options: .regularExpression) + line.replacingOccurrences(of: #"^([ \t]*\d{1,9})([.)])"#, with: "$1\\\\$2", options: .regularExpression) } /// Whether this item line may interrupt a paragraph (CommonMark): a @@ -358,17 +371,17 @@ struct MarkdownRenderer: Sendable { /// it paragraph text, so it is no item and no block boundary. static func canInterruptParagraph(_ line: String) -> Bool { guard isListItem(line) else { return false } - let trimmed = line.trimmingCharacters(in: .whitespaces) + let trimmed = markerLine(line) if let marker = trimmed.range(of: #"^\d{1,9}[.)]"#, options: .regularExpression) { guard Int(trimmed[marker].dropLast()) == 1 else { return false } - return !trimmed[marker.upperBound...].trimmingCharacters(in: .whitespaces).isEmpty + return !isBlankLine(trimmed[marker.upperBound...]) } - return !trimmed.dropFirst(2).trimmingCharacters(in: .whitespaces).isEmpty + return !isBlankLine(trimmed.dropFirst(2)) } /// A thematic break (`---`, `***`, `___`) — `renderLine` draws it as a rule. static func isThematicBreak(_ line: String) -> Bool { - line.trimmingCharacters(in: .whitespaces) + markerLine(line) .range(of: MarkdownTheme.horizontalRulePattern, options: .regularExpression) != nil } @@ -376,7 +389,7 @@ struct MarkdownRenderer: Sendable { /// get glued into the paragraph before it. private static func classify(_ line: String) -> LineKind { if isBlankLine(line) { return .structural } - let trimmed = line.trimmingCharacters(in: .whitespaces) + let trimmed = markerLine(line) let nsRange = NSRange(line.startIndex..., in: line) if headerRegex.firstMatch(in: line, range: nsRange) != nil { return .structural } if trimmed.range(of: MarkdownTheme.horizontalRulePattern, options: .regularExpression) != nil { return .structural } @@ -396,7 +409,7 @@ struct MarkdownRenderer: Sendable { // MARK: - Line Rendering private func renderLine(_ line: String, hangingPrefix: String? = nil) -> AttributedString { - let trimmed = line.trimmingCharacters(in: .whitespaces) + let trimmed = Self.markerLine(line) // Header - extract hash count from regex group, not space position let nsRange = NSRange(line.startIndex..., in: line) diff --git a/QuickMD/QuickMD/MarkdownTheme.swift b/QuickMD/QuickMD/MarkdownTheme.swift index fd259a8..433d48c 100644 --- a/QuickMD/QuickMD/MarkdownTheme.swift +++ b/QuickMD/QuickMD/MarkdownTheme.swift @@ -291,18 +291,18 @@ struct MarkdownTheme: Sendable, Identifiable { /// Matches table separator row: |---|---|---| or variants with alignment colons /// Examples: |:---|:---:|---:|, |---|---| - static let tableSeparatorPattern = #"^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$"# + static let tableSeparatorPattern = #"^\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$"# /// Matches ATX-style headers: # H1, ## H2, ... ###### H6 /// Captures: (1) hash marks, (2) header text - static let headerPattern = #"^(#{1,6})\s+(.*)$"# + static let headerPattern = #"^(#{1,6})[ \t]+(.*)$"# /// Matches horizontal rules: ***, ---, ___ (3 or more) - static let horizontalRulePattern = #"^(\*{3,}|-{3,}|_{3,})\s*$"# + static let horizontalRulePattern = #"^(\*{3,}|-{3,}|_{3,})[ \t]*$"# /// Matches task list items: - [ ] or - [x] with optional indentation /// Captures: (1) indent whitespace, (2) check state (space or x), (3) content - static let taskListPattern = #"^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$"# + static let taskListPattern = #"^([ \t]*)[-*+][ \t]+\[([ xX])\][ \t]+(.*)$"# // MARK: - Escape Characters @@ -314,11 +314,11 @@ struct MarkdownTheme: Sendable, Identifiable { /// Matches reference link definitions: [id]: url or [id]: with optional title /// Captures: (1) reference ID, (2) URL - static let referenceLinkDefinitionPattern = #"^\s*\[([^\]]+)\]:\s+]+)>?"# + static let referenceLinkDefinitionPattern = #"^[ \t]*\[([^\]]+)\]:[ \t]+]+)>?"# /// Matches footnote definitions: [^id]: text /// Captures: (1) footnote ID, (2) content - static let footnoteDefinitionPattern = #"^\s*\[\^([^\]]+)\]:\s+(.*)"# + static let footnoteDefinitionPattern = #"^[ \t]*\[\^([^\]]+)\]:[ \t]+(.*)"# // MARK: - URL Autolinking @@ -330,12 +330,12 @@ struct MarkdownTheme: Sendable, Identifiable { /// Matches setext H1 underline: one or more = signs /// Example: Title\n===== - static let setextH1Pattern = #"^=+\s*$"# + static let setextH1Pattern = #"^=+[ \t]*$"# /// Matches setext H2 underline: three or more - signs /// Example: Subtitle\n------- /// Note: Must be 3+ to distinguish from horizontal rule in some contexts - static let setextH2Pattern = #"^-{3,}\s*$"# + static let setextH2Pattern = #"^-{3,}[ \t]*$"# } // MARK: - Color Hex Extension diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 6b9e069..2d09884 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -472,6 +472,119 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNotEqual(separate[1], separate[2]) } + // MARK: - Block markers: spaces and tabs only + + /// Parses on a worker thread: a parser that never returns fails the test + /// after `seconds` instead of stalling the suite (its thread keeps + /// spinning, so a failure here is worth stopping the run for). + private func parseReturning(_ markdown: String, within seconds: Double = 5, + file: StaticString = #filePath, line: UInt = #line) -> [MarkdownBlock]? { + final class Box: @unchecked Sendable { var blocks: [MarkdownBlock]? } + let box = Box() + let done = DispatchSemaphore(value: 0) + let theme = self.theme + Thread.detachNewThread { + box.blocks = MarkdownBlockParser(theme: theme).parse(markdown) + done.signal() + } + guard done.wait(timeout: .now() + seconds) == .success else { + XCTFail("the parser did not return within \(seconds) s", file: file, line: line) + return nil + } + return box.blocks + } + + private func plain(_ block: MarkdownBlock) -> String? { + if case .text(let attributed) = block.content { return String(attributed.characters) } + return nil + } + + /// A quote marker after a no-break, em or ideographic space: the block + /// loop entered the quote branch on a line trimmed of EVERY Unicode + /// space, the quote's own loop (spaces and tabs only, 9bbf6ae) refused + /// the same line without consuming it, and the branch re-entered it for + /// ever — 100 % CPU, the document never shown. CommonMark indents with + /// spaces and tabs only, and GitHub shows each such line as paragraph + /// text (`gh api markdown`: `

> quoted

`). + func testAQuoteMarkerAfterAUnicodeSpaceIsTextAndTheParserReturns() throws { + for space in ["\u{00A0}", "\u{2003}", "\u{3000}"] { + let name = String(format: "U+%04X", space.unicodeScalars.first!.value) + let blocks = try XCTUnwrap(parseReturning("Intro\n\n\(space)> quoted\n"), name) + XCTAssertEqual(blocks.map(plain), ["Intro", "\(space)> quoted"], name) + // Inside a quote run too: the run ends there, and the parser goes on. + let inside = try XCTUnwrap(parseReturning("> a\n\(space)> b\n\nAfter."), name) + if case .blockquote(let content, _) = inside.first?.content { + XCTAssertEqual(content, "a", name) + } else { + XCTFail("\(name): \(String(describing: inside.first?.content))") + } + XCTAssertEqual(inside.last.flatMap(plain), "After.", name) + } + // Spaces still indent a quote marker. + let indented = try XCTUnwrap(parseReturning("Intro\n\n > quoted\n")) + if case .blockquote(let content, _) = indented.last?.content { + XCTAssertEqual(content, "quoted") + } else { + XCTFail("\(String(describing: indented.last?.content))") + } + } + + /// Every other block marker the same way, as GitHub reads them + /// (`gh api markdown`, 2026-11-04): a no-break space before a fence, + /// `$$`, a setext underline, a table separator, a list marker, a rule or + /// a link definition — or after `#` — leaves the line paragraph text. + func testBlockMarkersAfterAUnicodeSpaceAreText() throws { + let nbsp = "\u{00A0}" + func kinds(_ markdown: String) throws -> [String] { + try XCTUnwrap(parseReturning(markdown)).map { block in + switch block.content { + case .text: return "text" + case .codeBlock: return "code" + case .mathBlock: return "math" + case .heading: return "heading" + case .table: return "table" + case .blockquote, .alert: return "quote" + default: return "other" + } + } + } + // A fence: `

``` code

` then the bare ``` opens an empty one. + XCTAssertEqual(try kinds("\(nbsp)```\ncode\n```\n"), ["text", "code"]) + // Not a closing fence: one before it, one with a no-break space after. + let closers = try XCTUnwrap(parseReturning("```\ncode\n\(nbsp)```\nmid\n```\(nbsp)\nend\n```\n")) + XCTAssertEqual(closers.count, 1) + if case .codeBlock(let code, _) = closers.first?.content { + XCTAssertEqual(code, "code\n\(nbsp)```\nmid\n```\(nbsp)\nend") + } else { + XCTFail("\(String(describing: closers.first?.content))") + } + XCTAssertFalse(try kinds("\(nbsp)$$\nx^2\n$$\n").contains("math")) + XCTAssertEqual(try kinds("Title\n\(nbsp)===\n"), ["text"]) + XCTAssertEqual(try kinds("#\(nbsp)Title\n"), ["text"]) + XCTAssertEqual(try kinds("| a | b |\n\(nbsp)|---|---|\n| 1 | 2 |\n"), ["text"]) + // Text, not an item or a rule: the characters are drawn as written. + let blocks = try XCTUnwrap(parseReturning("\(nbsp)- item\n\npara\n\n\(nbsp)---\n\n1.\(nbsp)one\n")) + XCTAssertEqual(blocks.map(plain), ["\(nbsp)- item", "para", "\(nbsp)---", "1.\(nbsp)one"]) + XCTAssertFalse(MarkdownRenderer.isListItem("\(nbsp)- item")) + XCTAssertFalse(MarkdownRenderer.isListItem("1.\(nbsp)one")) + XCTAssertFalse(MarkdownRenderer.isThematicBreak("\(nbsp)---")) + XCTAssertFalse(MarkdownRenderer.isThematicBreak("---\(nbsp)")) + // A link definition is not one: it stays in the text. + let definition = try XCTUnwrap(parseReturning("[x]\n\n\(nbsp)[x]: https://example.com\n")) + XCTAssertEqual(definition.count, 2) + XCTAssertEqual(definition.last.flatMap(plain), "\(nbsp)[x]: https://example.com") + + // Spaces and tabs still indent every one of them. + XCTAssertEqual(try kinds(" ```\ncode\n ```\n"), ["code"]) + XCTAssertEqual(try kinds("Title\n ===\n"), ["heading"]) + XCTAssertEqual(try kinds("# Title\n"), ["heading"]) + XCTAssertEqual(try kinds("#\tTitle\n"), ["heading"]) + XCTAssertEqual(try kinds("| a | b |\n |---|---|\n| 1 | 2 |\n"), ["table"]) + XCTAssertTrue(MarkdownRenderer.isListItem(" - item")) + XCTAssertTrue(MarkdownRenderer.isListItem("1.\tone")) + XCTAssertTrue(MarkdownRenderer.isThematicBreak(" ---\t")) + } + // MARK: - Anchor signatures /// The scroll anchor is found again after a reload by the block's content From 636de0e6774027f992467ab377eb400575271337 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:39:26 +0100 Subject: [PATCH 059/105] fix(source-edit): the editor's column follows the zoom the document is shown at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list's Reading Mode cap follows renderedFontScale (the zoom its blocks were parsed at), but Source Edit took the requested fontScale. Between ⌘+ and the parse that follows it the two differ: at 200 % requested over a 100 % document the editor's column was 1440 pt over a 720 pt list column, and leaving Source Edit before the parse landed moved the text sideways. SourceEditorController.Style.overDocument(theme:shownFontScale: isReadingMode:) is the editor's style over a rendered document, and MarkdownView hands it renderedFontScale. The editor's font follows the zoom when the parse lands, with the document's. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownView.swift | 4 ++-- QuickMD/QuickMD/SourceEditorController.swift | 12 ++++++++++ .../SourceEditorControllerTests.swift | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index 0063f4b..d8af58d 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -324,8 +324,8 @@ struct MarkdownView: View { Group { if editSession.isActive { SourceEditorView(controller: editSession.editor, - style: .init(theme: theme, fontScale: fontScale, - isReadingLayout: isReadingMode)) + style: .overDocument(theme: theme, shownFontScale: renderedFontScale, + isReadingMode: isReadingMode)) } } // An animated transaction from elsewhere (a toast, a sidebar) diff --git a/QuickMD/QuickMD/SourceEditorController.swift b/QuickMD/QuickMD/SourceEditorController.swift index e5f1b2b..23ac428 100644 --- a/QuickMD/QuickMD/SourceEditorController.swift +++ b/QuickMD/QuickMD/SourceEditorController.swift @@ -120,6 +120,18 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD && lhs.hasSameTintColors(as: rhs) } + /// The editor's style over a rendered document: at the zoom the + /// document is SHOWN at (`MarkdownView.renderedFontScale` — the scale + /// its blocks were parsed at and its Reading Mode column is capped + /// at), not the zoom just asked for. Between ⌘+ and the parse that + /// follows it the two differ; the editor's column (`columnGeometry`) + /// has to be the list's then too, or leaving Source Edit before the + /// parse lands moves the text sideways. The editor's font follows the + /// zoom when the parse lands, with the document's. + static func overDocument(theme: MarkdownTheme, shownFontScale: Double, isReadingMode: Bool) -> Style { + Style(theme: theme, fontScale: shownFontScale, isReadingLayout: isReadingMode) + } + /// The colours of the block tint (`paintTint`). func hasSameTintColors(as other: Style) -> Bool { theme.keywordColor == other.theme.keywordColor diff --git a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift index 99b0fbe..9c95bde 100644 --- a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift +++ b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift @@ -611,6 +611,30 @@ final class SourceEditorControllerTests: XCTestCase { XCTAssertEqual(narrow, .init(horizontalInset: 320, columnWidth: 360, verticalInset: 48)) } + /// ⌘+ in Reading Mode with the editor open: the list keeps showing the + /// blocks of the old zoom, with that zoom's column cap, until the zoom's + /// parse lands. The editor took the REQUESTED zoom (`fontScale`), so for + /// that while its column was 1440 pt over a 720 pt list column, and + /// leaving Source Edit then moved the text sideways. It follows the + /// zoom the document is shown at now. + func testTheEditorColumnFollowsTheZoomTheDocumentIsShownAt() { + let requested = 2.0, shown = 1.0 + let (controller, scrollView, _) = makeEditor( + "text", width: 1800, + style: .overDocument(theme: light, shownFontScale: shown, isReadingMode: true)) + let inner = floor(scrollView.contentView.bounds.width) - 64 + func listColumn(_ scale: Double) -> CGFloat { + BlockLayout.Document.columnWidth(inner: inner, + cap: BlockLayout.Document.readingColumnCap(fontScale: CGFloat(scale))) + } + XCTAssertEqual(listColumn(shown), 720) + XCTAssertEqual(listColumn(requested), 1440) // the column the editor used to take + XCTAssertEqual(controller.textView.textContainer!.size.width, listColumn(shown)) + // The zoom's parse lands: the list and the editor move together. + controller.apply(style: .overDocument(theme: light, shownFontScale: requested, isReadingMode: true)) + XCTAssertEqual(controller.textView.textContainer!.size.width, listColumn(requested)) + } + func testTextContainerFollowsTheViewWidthAndTheLayoutStyle() { let (controller, scrollView, window) = makeEditor("text", width: 1000) let view = controller.textView From b9f0cda2531b65474d04baeb80aedd0ad905c022 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:42:20 +0100 Subject: [PATCH 060/105] fix(search): the reader's own scroll cancels a jump's pending aim at its match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⌘G into a block taller than the window whose view is not on display jumps to the block and aims at the match when it lands. Only "is the anchor row still the one" was checked then, and a glide ends on it: a reader scrolling inside the block during the glide was yanked to the match when it ended. The aim is now a PendingMatchAim, and any scroll of the reader's after the request drops it: the wheel or trackpad (BlockScrollView.scrollWheel), a scroller drag (willStartLiveScroll), a scroll key or a drag-select at the edge (BlockTableView.scrollVertically). A new table drops it too. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/VerticalRhythm.swift | 29 ++++++++++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 38 ++++++++++++++----- .../QuickMDTests/VerticalRhythmTests.swift | 21 ++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index 27654d5..ca9d9a0 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -329,6 +329,35 @@ enum RowGeometry { } } +// MARK: - Search jump: the aim at the match + +/// A search jump into a block taller than the window whose match could not +/// be located yet (its view was not on display): the jump goes to the block, +/// and this aims it at the match once it has landed +/// (`VirtualBlockList.aimAtPendingMatch`, `RowGeometry.searchTarget`). +/// +/// Anything the reader does to the scroll position after the request — the +/// wheel or trackpad, a scroller drag, a scroll key, a drag-select at the +/// edge — takes the aim away: they have taken over, and a glide to the match +/// when the jump lands would yank the view from where they scrolled to. A +/// new table drops it too. +struct PendingMatchAim: Equatable { + /// The block the jump went to; nil when nothing is pending. + private(set) var blockId: String? + + /// A search jump was sent to `blockId`; `needsMatch`: its match is not + /// known yet (`RowGeometry.searchTarget`). + mutating func jumped(to blockId: String, needsMatch: Bool) { + self.blockId = needsMatch ? blockId : nil + } + + /// The reader moved the document themselves. + mutating func readerScrolled() { blockId = nil } + + /// Taken, given up, or a new table. + mutating func drop() { blockId = nil } +} + // MARK: - Print / PDF /// The rhythm for print and PDF (`PDFExportManager.generateMultiPagePDF`): diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index d8a471a..b96e247 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -385,8 +385,10 @@ struct VirtualBlockList: NSViewRepresentable { private var isSwitchingLayout = false private var layoutSwitchGeneration = 0 /// A search jump into a block taller than the content area whose match - /// could not be located yet (`aimAtPendingMatch`): that block's id. - private var pendingMatchRow: String? + /// could not be located yet (`aimAtPendingMatch`). + private var matchAim = PendingMatchAim() + /// Scroller drags and trackpad scrolls (`PendingMatchAim.readerScrolled`). + private var liveScrollObserver: NSObjectProtocol? #if DEBUG /// Where the last `setContentTop` left the content top, to tell a clip /// move this list made from one something else made (logged with the @@ -453,6 +455,7 @@ struct VirtualBlockList: NSViewRepresentable { deinit { widthWork?.cancel() if let frameObserver { NotificationCenter.default.removeObserver(frameObserver) } + if let liveScrollObserver { NotificationCenter.default.removeObserver(liveScrollObserver) } #if DEBUG debugObservers.forEach { NotificationCenter.default.removeObserver($0) } #endif @@ -468,6 +471,12 @@ struct VirtualBlockList: NSViewRepresentable { scrollView.onEndLiveResize = { [weak self] in self?.clipFrameChanged(afterLiveResize: true) } + scrollView.onReaderScroll = { [weak self] in self?.matchAim.readerScrolled() } + liveScrollObserver = NotificationCenter.default.addObserver( + forName: NSScrollView.willStartLiveScrollNotification, object: scrollView, queue: nil + ) { [weak self] _ in + self?.matchAim.readerScrolled() + } frameObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, object: scrollView.contentView, queue: nil @@ -566,7 +575,7 @@ struct VirtualBlockList: NSViewRepresentable { self.anchor = nil isJumping = false isSwitchingLayout = false - pendingMatchRow = nil + matchAim.drop() reportedBlocks = [] blocks = newBlocks @@ -951,7 +960,7 @@ struct VirtualBlockList: NSViewRepresentable { listLog.debug("scrollRequest: \(request.blockId, privacy: .public) row \(row) anchor=\(String(describing: request.anchor), privacy: .public) target=\(target, format: .fixed(precision: 1))") #endif place(row: row, offset: target - blockTopY(row), animated: request.animated) - pendingMatchRow = needsMatch ? request.blockId : nil + matchAim.jumped(to: request.blockId, needsMatch: needsMatch) if needsMatch, !isJumping { // No glide to wait for (not animated, or already there). DispatchQueue.main.async { [weak self] in self?.aimAtPendingMatch() } @@ -975,11 +984,12 @@ struct VirtualBlockList: NSViewRepresentable { /// the next layout pass; one is forced, and failing that it is asked /// again on the next turns of the run loop. private func aimAtPendingMatch(retries: Int = 2) { - guard let blockId = pendingMatchRow else { return } - // Not if the reader has scrolled on meanwhile. + guard let blockId = matchAim.blockId else { return } + // Not if the reader has scrolled on meanwhile (their scroll drops + // `matchAim`; the anchor check is the belt to those braces). guard let tableView, let scrollView, let row = rowForBlockId[blockId], anchoredRow(contentTop: contentTopY()) == row else { - pendingMatchRow = nil + matchAim.drop() return } var matchMidY = focusedMatchMidY(row: row) @@ -991,11 +1001,11 @@ struct VirtualBlockList: NSViewRepresentable { if retries > 0 { DispatchQueue.main.async { [weak self] in self?.aimAtPendingMatch(retries: retries - 1) } } else { - pendingMatchRow = nil + matchAim.drop() } return } - pendingMatchRow = nil + matchAim.drop() let insets = scrollView.contentInsets let contentHeight = scrollView.contentView.bounds.height - insets.top - insets.bottom let target = matchMidY - contentHeight / 2 @@ -1507,6 +1517,14 @@ private struct RowHeightReporter: ViewModifier { /// or two of the user letting go instead of waiting out the debounce. final class BlockScrollView: NSScrollView { var onEndLiveResize: (() -> Void)? + /// The reader scrolled: wheel or trackpad here, a scroll key or a + /// drag-select at the edge (`BlockTableView.scrollVertically`). + var onReaderScroll: (() -> Void)? + + override func scrollWheel(with event: NSEvent) { + onReaderScroll?() + super.scrollWheel(with: event) + } /// Clicks in the content insets above the first / below the last row land /// on the clip view, whose responder chain ends here, not at the table — /// they still belong to the document (a click there clears the selection, @@ -1719,8 +1737,10 @@ final class BlockTableView: NSTableView { enclosingScrollView?.verticalLineScroll ?? 10 } + /// The reader's own scroll (keys, a drag-select at the edge). func scrollVertically(by dy: CGFloat) { guard let scrollView = enclosingScrollView else { return } + (scrollView as? BlockScrollView)?.onReaderScroll?() let clipView = scrollView.contentView let proposed = NSRect(origin: NSPoint(x: clipView.bounds.origin.x, y: clipView.bounds.origin.y + dy), diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 2d09884..62a24e1 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -472,6 +472,27 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNotEqual(separate[1], separate[2]) } + /// ⌘G into a block taller than the window whose view is not on display: + /// the jump goes to the block and aims at the match when it lands. The + /// reader scrolling inside the block during the glide used to leave the + /// aim in place (only "is the anchor row still there" was checked, and + /// the glide ends on it), so the view was yanked to the match. Their + /// scroll takes the aim away now; nothing else does until it is taken. + func testASearchJumpsAimAtItsMatchIsDroppedWhenTheReaderScrolls() { + var aim = PendingMatchAim() + aim.jumped(to: "code-3", needsMatch: true) + XCTAssertEqual(aim.blockId, "code-3") // lands untouched: aims + aim.readerScrolled() + XCTAssertNil(aim.blockId) // a wheel during the glide: stays where they are + aim.jumped(to: "code-3", needsMatch: false) + XCTAssertNil(aim.blockId) // the match was known: centred at once + aim.jumped(to: "table-1", needsMatch: true) + aim.jumped(to: "code-9", needsMatch: true) + XCTAssertEqual(aim.blockId, "code-9") // the latest jump's + aim.drop() + XCTAssertNil(aim.blockId) // a new table + } + // MARK: - Block markers: spaces and tabs only /// Parses on a worker thread: a parser that never returns fails the test From 88bceb7e0edcbac55618fc20efc6634e6b12d8c0 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:43:19 +0100 Subject: [PATCH 061/105] test(parser): linked images,   lines and 1. under whole-document row rendering After the vertical-rhythm merge: a linked image indented under an ordered list stands in like a plain image and keeps the list (and its widest hang) going; a line of   is a paragraph, not a blank line; 1. x is paragraph text, never a list item. Expected behaviour from gh api markdown. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMDTests/ParserTests.swift | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 9645362..86949d3 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -462,6 +462,38 @@ final class ParserTests: XCTestCase { XCTAssertNil(width); XCTAssertNil(linkedWidth) } + /// After the vertical-rhythm merge (rows rendered at the end of the + /// parse, ordered hangs over the whole document), checked against GitHub + /// (`gh api markdown`): a linked image indented under a list stands in + /// like a plain image and keeps the list going (`10.` hangs the items + /// above it); a line of ` ` is a paragraph (`

 

`), not a + /// blank line; `1. x` is paragraph text "1. x", never a list item + /// (references are decoded after block parsing). + func testMergedRowRenderingWithLinkedImagesAndReferences() { + func hangs(_ md: String) -> [CGFloat] { + parse(md).compactMap { block -> CGFloat? in + guard case .text(let attributed) = block.content, + let style = attributed.runs.first?[AttributeScopes.AppKitAttributes.ParagraphStyleAttribute.self] + else { return nil } + return style.headIndent + } + } + let linked = "1. one\n2. two\n\n [![b](b.png)](https://x)\n\n10. ten" + let plain = "1. one\n2. two\n\n ![b](b.png)\n\n10. ten" + let linkedHangs = hangs(linked) + XCTAssertEqual(linkedHangs.count, 2) + XCTAssertEqual(linkedHangs, hangs(plain), "a linked image stands in like a plain image") + XCTAssertEqual(linkedHangs[0], linkedHangs[1], accuracy: 0.01, "one list: 1. and 10. share the widest hang") + let split = hangs("1. one\n2. two\n\n[![b](b.png)](https://x)\n\n10. ten") + XCTAssertNotEqual(split[0], split[1], "control: an unindented picture ends the list") + + let blocks = parse("a\n\n \n\n1. x") + XCTAssertEqual(blocks.compactMap(plainText), ["a", "\u{00A0}", "1. x"]) + guard case .text(let item) = blocks[2].content else { return XCTFail("text") } + XCTAssertNil(item.runs.first?[AttributeScopes.AppKitAttributes.ParagraphStyleAttribute.self]?.headIndent + .nonZero, "no list hang") + } + // MARK: - Reference links in quotes, alerts and table cells /// Quotes, alerts and table cells keep their markdown and are rendered @@ -1335,3 +1367,7 @@ final class ParserTests: XCTestCase { XCTAssertEqual(gapBlocks[1].sourceLine, 81) } } + +private extension CGFloat { + var nonZero: CGFloat? { self == 0 ? nil : self } +} From e71f11cad174fb3b46b0fd35d7390771f12fa98c Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:43:58 +0100 Subject: [PATCH 062/105] fix(source-edit): the tint reads titles and blank lines as the parser does SourceTint recognises a Setext heading by comparing its line, trimmed "the way the parser trims it", with the parser's title, and drops trailing blank lines by "the parser's notion of blank". Both still said every Unicode space; the parser says spaces and tabs (a851229, 9bbf6ae). A title starting with a no-break space lost its underline from the tint. Both now use the parser's rule (MarkdownBlockParser.markerLine, MarkdownRenderer.markerWhitespace). Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/SourceTint.swift | 10 ++++++---- QuickMD/QuickMDTests/SourceTintTests.swift | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/SourceTint.swift b/QuickMD/QuickMD/SourceTint.swift index e8c291e..1b9f2cd 100644 --- a/QuickMD/QuickMD/SourceTint.swift +++ b/QuickMD/QuickMD/SourceTint.swift @@ -78,7 +78,8 @@ enum SourceTint { func isBlank(_ line: Int) -> Bool { let start = lineStarts[line] let range = NSRange(location: start, length: max(0, contentEnd(line) - start)) - // The parser's notion of blank: nothing but `.whitespaces`. + // The parser's notion of blank (`MarkdownRenderer.isBlankLine`): + // nothing but spaces and tabs. return text.rangeOfCharacter(from: nonWhitespace, options: [], range: range).location == NSNotFound } @@ -112,8 +113,9 @@ enum SourceTint { return spans } - /// Non-blank characters, by the parser's `.whitespaces` trimming rule. - private static let nonWhitespace = CharacterSet.whitespaces.inverted + /// Non-blank characters, by the parser's blank-line rule: anything but a + /// space or a tab (a no-break space is text). + private static let nonWhitespace = MarkdownRenderer.markerWhitespace.inverted static func kind(of block: MarkdownBlock) -> Kind? { switch block.content { @@ -138,6 +140,6 @@ enum SourceTint { lineStarts: [Int]) -> Bool { let start = lineStarts[line] let content = text.substring(with: NSRange(location: start, length: max(0, contentEnd - start))) - return content.trimmingCharacters(in: .whitespaces) == title + return MarkdownBlockParser.markerLine(content) == title } } diff --git a/QuickMD/QuickMDTests/SourceTintTests.swift b/QuickMD/QuickMDTests/SourceTintTests.swift index dbdd0b8..a37e626 100644 --- a/QuickMD/QuickMDTests/SourceTintTests.swift +++ b/QuickMD/QuickMDTests/SourceTintTests.swift @@ -50,6 +50,14 @@ final class SourceTintTests: XCTestCase { [("Title\n=====", .heading), ("Sub\n---", .heading)]) } + /// The parser trims a Setext title of spaces and tabs only + /// (`MarkdownBlockParser.markerLine`); the tint compared it with the line + /// trimmed of every Unicode space, so a title starting with a no-break + /// space lost its underline from the tint. + func testASetextTitleStartingWithANoBreakSpaceTintsItsUnderline() { + assertTinted("\u{00A0}Title\n===\nBody\n", [("\u{00A0}Title\n===", .heading)]) + } + func testFencedCodeKeepsInnerBlankLinesAndDropsTrailingOnes() { assertTinted("```swift\nlet a = 1\n\nlet b = 2\n```\n\n \nAfter\n", [("```swift\nlet a = 1\n\nlet b = 2\n```", .verbatim)]) From f06ad86c1a97aae70fdf7efada3c10746c5bf9f8 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:49:01 +0100 Subject: [PATCH 063/105] fix(math): an escaped dollar is a dollar sign, never a formula delimiter Review R9: $ was not escapable, so "Price \$5 and \$10" kept its backslashes and InlineMathSegmenter paired the dollars into a formula "5 and \". GitHub shows "Price $5 and $10". $ is escapable now and the escape tags its run with LiteralDollarAttribute (not in the AppKit scope). BlockTextConverter and PrintableMath pass the tagged offsets to InlineMathSegmenter.split(_:literalDollars:), which never opens or closes a formula on them; a tagged dollar inside a formula goes back to \$ in the LaTeX ($a\$b$ still typesets). A dollar made by $ is not tagged and stays a delimiter, as on GitHub. Screen, measurer, search and print share the converter's split; quotes, alerts and table cells (no inline math) just show the dollar. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 8 ++-- QuickMD/QuickMD/InlineMathSegmenter.swift | 58 +++++++++++++++++++++-- QuickMD/QuickMD/MarkdownExport.swift | 8 ++-- QuickMD/QuickMD/MarkdownRenderer.swift | 2 + QuickMD/QuickMD/MarkdownTheme.swift | 8 ++-- QuickMD/QuickMDTests/RendererTests.swift | 54 ++++++++++++++++++++- 6 files changed, 123 insertions(+), 15 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index 7da6482..faea09f 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -738,7 +738,8 @@ enum BlockTextConverter { } let plain = String(attributed.characters) - let segments = InlineMathSegmenter.split(plain) + let segments = InlineMathSegmenter.split(plain, + literalDollars: InlineMathSegmenter.literalDollars(in: attributed)) let result = NSMutableAttributedString() var attrIndex = attributed.startIndex @@ -756,9 +757,10 @@ enum BlockTextConverter { attrIndex = end } - case .math(let latex): + case .math(let source): // Skip past the $latex$ in the source AttributedString - let skipCount = latex.count + 2 + let skipCount = source.count + 2 + let latex = InlineMathSegmenter.tex(source) if let end = attributed.characters.index(attrIndex, offsetBy: skipCount, limitedBy: attributed.endIndex) { attrIndex = end diff --git a/QuickMD/QuickMD/InlineMathSegmenter.swift b/QuickMD/QuickMD/InlineMathSegmenter.swift index 63a3e49..cda8675 100644 --- a/QuickMD/QuickMD/InlineMathSegmenter.swift +++ b/QuickMD/QuickMD/InlineMathSegmenter.swift @@ -1,30 +1,80 @@ import Foundation +/// Marks a `$` the author escaped (`\$5`): the renderer draws it as "$", +/// and `InlineMathSegmenter` never opens or closes a formula on it — the math +/// pass runs on the RENDERED text, where the backslash is gone. A dollar made +/// by a character reference (`$`) is NOT tagged: GitHub reads that one as +/// a math delimiter. Not part of the AppKit scope, so the NSAttributedString a +/// view installs never carries it. +enum LiteralDollarAttribute: AttributedStringKey { + typealias Value = Bool + static let name = "qmdLiteralDollar" +} + /// Splits paragraph text into alternating plain-text and inline-math segments. /// Math is delimited by single `$` (never `$$` — that's display math). /// Pure Foundation — shared by TextBlockView (app) and the unit tests. enum InlineMathSegmenter { + /// The indices, in `String(attributed.characters)`, of the dollars the + /// author escaped (`LiteralDollarAttribute`). Empty for almost every + /// paragraph: only tagged runs are walked. + static func literalDollars(in attributed: AttributedString) -> Set { + var offsets: Set = [] + let characters = attributed.characters + for run in attributed.runs where run[LiteralDollarAttribute.self] == true { + var offset = characters.distance(from: characters.startIndex, to: run.range.lowerBound) + for character in characters[run.range] { + if character == "$" { offsets.insert(offset) } + offset += 1 + } + } + return offsets + } + + /// The LaTeX of a math segment: a `$` inside one can only be an escaped + /// dollar (any other would have closed it), so it goes back to `\$` + /// (`$a\$b$` typesets "a$b"). The segment's SOURCE length is still + /// `latex.count + 2`. + static func tex(_ latex: String) -> String { + latex.contains("$") ? latex.replacingOccurrences(of: "$", with: "\\$") : latex + } + enum Segment: Equatable { case text(String) case math(String) } - static func split(_ text: String) -> [Segment] { + /// `literalDollars`: Character offsets of escaped dollars + /// (`literalDollars(in:)`) — never a delimiter, so they stay text or, inside + /// a formula, part of its LaTeX (`tex`). + static func split(_ text: String, literalDollars: Set = []) -> [Segment] { var segments: [Segment] = [] var remaining = text[...] var buffer = "" + let literal: Set = Set(literalDollars.compactMap { + text.index(text.startIndex, offsetBy: $0, limitedBy: text.endIndex) + }) + /// The next delimiter `$` at or after `from`. + func nextDollar(in slice: Substring) -> String.Index? { + var index = slice.startIndex + while let found = slice[index...].firstIndex(of: "$") { + if !literal.contains(found) { return found } + index = slice.index(after: found) + } + return nil + } while !remaining.isEmpty { // Look for opening $ - guard let dollarIndex = remaining.firstIndex(of: "$") else { + guard let dollarIndex = nextDollar(in: remaining) else { buffer.append(contentsOf: remaining) break } // Skip $$ (display math delimiter, not inline) let afterDollar = remaining.index(after: dollarIndex) - if afterDollar < remaining.endIndex && remaining[afterDollar] == "$" { + if afterDollar < remaining.endIndex && remaining[afterDollar] == "$" && !literal.contains(afterDollar) { buffer.append(contentsOf: remaining[remaining.startIndex...afterDollar]) remaining = remaining[remaining.index(after: afterDollar)...] continue @@ -33,7 +83,7 @@ enum InlineMathSegmenter { // Check for closing $ let searchStart = afterDollar guard searchStart < remaining.endIndex, - let closeIndex = remaining[searchStart...].firstIndex(of: "$") else { + let closeIndex = nextDollar(in: remaining[searchStart...]) else { buffer.append(contentsOf: remaining) break } diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index 4f34f1c..dbceaba 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -935,7 +935,8 @@ enum PrintableMath { var result = Text("") var index = attributed.startIndex - for segment in InlineMathSegmenter.split(plain) { + for segment in InlineMathSegmenter.split(plain, + literalDollars: InlineMathSegmenter.literalDollars(in: attributed)) { switch segment { case .text(let str): if let end = attributed.characters.index(index, offsetBy: str.count, @@ -943,11 +944,12 @@ enum PrintableMath { result = result + Text(AttributedString(attributed[index.. = [ - "!", "\"", "#", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", + "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", ] diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 1fdef30..0eb998e 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -817,7 +817,7 @@ final class RendererTests: XCTestCase { /// rendered text). The Contents sidebar shows exactly the heading's text. func testEveryASCIIPunctuationEscapesAndTheToCShowsTheHeadingText() { XCTAssertEqual(rendered("a \\& \\< \\~x\\~ \\\" \\@ \\^ \\, b"), "a & < ~x~ \" @ ^ , b") - XCTAssertEqual(rendered("\\$5"), "\\$5", "no bare $ that inline math would take") + XCTAssertEqual(rendered("\\$5"), "$5", "an escaped dollar (R9: tagged, never math)") let heading = String(renderer.renderHeader("\\& `<x>`", level: 1).characters) XCTAssertEqual(heading, "& <x>") @@ -827,4 +827,56 @@ final class RendererTests: XCTestCase { String(renderer.renderHeader(entry.title, level: 1).characters)) } + + // MARK: - Escaped dollars (review R9) + + /// The text a paragraph's row shows, with every formula as "⟨latex⟩" (a + /// stub engine records what it was asked to typeset). + private func shownParagraph(_ markdown: String) -> (text: String, formulas: [String]) { + let theme = MarkdownTheme.cached(for: .light) + var formulas: [String] = [] + let engine = MathRendering(inlineImage: { latex, _, _ in + formulas.append(latex) + return NSImage(size: NSSize(width: 1, height: 1)) + }, displayHeight: { _, _, _ in 0 }) + let blocks = MarkdownBlockParser(theme: theme).parse(markdown) + guard case .text(let attributed)? = blocks.first?.content else { return ("", []) } + let plain = String(attributed.characters) + let ns = BlockTextConverter.makeNSAttributedString( + from: attributed, hasInlineMath: BlockTextConverter.containsInlineMath(plain), + theme: theme, fontScale: 1, math: engine) + return (ns.string, formulas) + } + + /// `\$` is a dollar sign (GitHub: "Price $5 and $10"), never a delimiter; + /// inside a formula it stays `\$` in the LaTeX; a dollar made by `$` + /// IS a delimiter (GitHub renders `$x$` as math); a code span keeps + /// `\$` as written. Print (`PrintableMath.text`) and search use the same + /// split (`literalDollars`). + func testEscapedDollarsAreNeverMathDelimiters() { + let price = shownParagraph("Price \\$5 and \\$10") + XCTAssertEqual(price.text, "Price $5 and $10") + XCTAssertEqual(price.formulas, []) + + let inside = shownParagraph("Cost $a\\$b$ here") + XCTAssertEqual(inside.formulas, ["a\\$b"], "the escaped dollar goes back into the LaTeX") + XCTAssertEqual(inside.text, "Cost \u{FFFC} here") + + let entity = shownParagraph("Then $x^2$ is math") + XCTAssertEqual(entity.formulas, ["x^2"], "a reference-made dollar is a delimiter") + + let code = shownParagraph("Use `\\$HOME` and \\$PATH") + XCTAssertEqual(code.text, "Use \\$HOME and $PATH") + XCTAssertEqual(code.formulas, []) + + // Table cells, quotes and alerts have no inline math: the dollar shows. + let theme = MarkdownTheme.cached(for: .light) + let cell = TableTextBuilder.plainString(headers: ["a"], rows: [["\\$5 and \\$10"]], alignments: [.leading], + style: .grid, theme: theme, fontScale: 1) + XCTAssertTrue(cell.contains("$5 and $10\n")) + XCTAssertEqual(String(renderer.renderQuotedBody("Pay \\$5").characters), "Pay $5") + // The segmenter itself: offsets of escaped dollars never delimit. + XCTAssertEqual(InlineMathSegmenter.split("a $5 and $6", literalDollars: [2, 9]), [.text("a $5 and $6")]) + } + } From 1f9947c2a031a91a03bd4b7e63ac02ca2ee06de8 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:51:09 +0100 Subject: [PATCH 064/105] fix(links): character references in link destinations are decoded Review R10: [x](a?b=1&c=2) linked to a?b=1&c=2; CommonMark and GitHub decode references in a destination (href a?b=1&c=2). The scanner's destination text (inline links and images) now decodes them outside backslash escapes, and reference definitions are decoded when read. ReferenceLinks escapes & when it inlines a definition, so an already decoded destination is not decoded a second time. Code spans are untouched; titles are discarded as before. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 26 +++++++++++++----- QuickMD/QuickMD/MarkdownBlockParser.swift | 4 ++- QuickMD/QuickMD/ReferenceLinks.swift | 7 +++-- QuickMD/QuickMDTests/RendererTests.swift | 32 +++++++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index 9e8cdb0..6907ede 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -238,6 +238,7 @@ enum InlineSyntaxScanner { } private static let backslash = UInt8(ascii: "\\") + private static let ampersand = UInt8(ascii: "&") /// CommonMark whitespace for these purposes: space, tab, LF, VT, FF, CR. private static func isWhitespace(_ byte: UInt8) -> Bool { @@ -386,22 +387,35 @@ enum InlineSyntaxScanner { /// The bytes as a String with `\` → ``. Other /// backslashes stay literal (`C:\dir\x.png`). One copy when there is /// nothing to unescape — the common case, and the multi-MB `data:` case. + /// A destination's text: backslash escapes of ASCII punctuation removed + /// and character references decoded (`a?b=1&c=2` → `a?b=1&c=2`, as + /// CommonMark and GitHub read a link's href) — but not the `&` of an + /// escaped `\&`. One pass over the bytes when there is neither. private static func unescaped(_ bytes: Bytes, _ range: Range) -> String { let slice = Bytes(rebasing: bytes[range]) - guard slice.contains(backslash) else { return String(decoding: slice, as: UTF8.self) } - var out: [UInt8] = [] - out.reserveCapacity(slice.count) + guard slice.contains(where: { $0 == backslash || $0 == ampersand }) else { + return String(decoding: slice, as: UTF8.self) + } + var out = "" + var run: [UInt8] = [] + func flushRun() { + guard !run.isEmpty else { return } + out += HTMLEntities.decode(String(decoding: run, as: UTF8.self)) + run.removeAll(keepingCapacity: true) + } var i = 0 while i < slice.count { if isEscape(slice, at: i, before: slice.count) { - out.append(slice[i + 1]) + flushRun() + out += String(decoding: [slice[i + 1]], as: UTF8.self) i += 2 } else { - out.append(slice[i]) + run.append(slice[i]) i += 1 } } - return String(decoding: out, as: UTF8.self) + flushRun() + return out } /// An image starting at `start` (`![`), see `image(atStartOf:references:)`. diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 7754c6f..0e8a152 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -100,7 +100,9 @@ struct MarkdownBlockParser: Sendable { } else if let match = Self.refLinkDefRegex.firstMatch(in: trimmed, range: nsRange), let idRange = Range(match.range(at: 1), in: trimmed), let urlRange = Range(match.range(at: 2), in: trimmed) { - referenceDefinitions[String(trimmed[idRange]).lowercased()] = String(trimmed[urlRange]) + // Character references in the destination are decoded, as in + // an inline link's (`InlineSyntaxScanner.unescaped`). + referenceDefinitions[String(trimmed[idRange]).lowercased()] = HTMLEntities.decode(String(trimmed[urlRange])) } else { lines.append(line) lineMap.append(originalIndex) diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index f13a2c4..7b5e67b 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -228,11 +228,14 @@ enum ReferenceLinks { /// ``, which `InlineSyntaxScanner.linkDestination` reads back as /// exactly `url`: the characters that would end the pointy form or pair /// up with the link's own parentheses are backslash-escaped (the scanner - /// unescapes ASCII punctuation). A definition is one line, so no line break. + /// unescapes ASCII punctuation), and so is `&` — the definition's + /// references were decoded when it was read, a second decode must not + /// turn a literal `&` into `&`. A definition is one line, so no line + /// break. private static func inlineDestination(_ url: String) -> String { var escaped = "<" for character in url { - if "\\<>()".contains(character) { escaped.append("\\") } + if "\\<>()&".contains(character) { escaped.append("\\") } escaped.append(character) } return escaped + ">" diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 0eb998e..f32f27d 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -828,6 +828,38 @@ final class RendererTests: XCTestCase { } + // MARK: - References in link destinations (review R10) + + /// A destination's character references are decoded (hrefs from + /// `gh api markdown`: `a?b=1&c=2` → `a?b=1&c=2`, `
` → + /// `a%20b%C2%A9`), in inline links, images and reference definitions; a + /// definition inlined into a quote is decoded exactly once. + func testLinkDestinationsDecodeCharacterReferences() { + let theme = MarkdownTheme.cached(for: .light) + let md = """ + [x](a?b=1&c=2) [z]() [r] [twice] `[c](a&b)` + + ![i](p&q.png) + + > quoted [r] and [twice] + + [r]: /u?x=1&y=2 + [twice]: /v?k=&amp; + """ + let blocks = MarkdownBlockParser(theme: theme).parse(md) + func links(_ attributed: AttributedString) -> [String] { + attributed.runs.compactMap { $0.link?.absoluteString } + } + guard case .text(let paragraph) = blocks[0].content else { return XCTFail("paragraph") } + XCTAssertEqual(links(paragraph), ["a?b=1&c=2", "a%20b%C2%A9", "/u?x=1&y=2", "/v?k=&"]) + XCTAssertTrue(String(paragraph.characters).hasSuffix("[c](a&b)"), "code span untouched") + guard case .image(let url, _, _) = blocks[1].content else { return XCTFail("image") } + XCTAssertEqual(url, "p&q.png") + guard case .blockquote(let quote, _) = blocks[2].content else { return XCTFail("quote") } + XCTAssertEqual(links(renderer.renderQuotedBody(quote)), ["/u?x=1&y=2", "/v?k=&"], + "inlined definitions are not decoded a second time") + } + // MARK: - Escaped dollars (review R9) /// The text a paragraph's row shows, with every formula as "⟨latex⟩" (a From a32f42dfd1826c7da775dac8f96b558479236f93 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski Date: Wed, 4 Nov 2026 21:55:09 +0100 Subject: [PATCH 065/105] fix(parser): a quote's paragraph takes lazy continuation lines GitHub (CommonMark) reads "> a\nb" as one quote paragraph: a line that would continue a paragraph continues the paragraph the quote ends in, also inside a nested quote, a list item or an alert, and on a line with fewer markers than the quote ("> > a\n> b" is the inner quote's "a b"). Our parser ended the quote and made the line a paragraph of its own, so a hard-wrapped quote with a marker on its first line only lost the rest. - QuoteTip follows what a quote group ends in: an open paragraph (or a list item's text) takes lazy lines; a blank or empty quote line, a fenced code block (open or just closed), an ATX heading, a Setext underline, a rule or a table do not. - continuesQuoteLazily: not blank and no block start, checked as cmark does against the document: any list item, a fence, an ATX heading, a rule and an HTML block start (CommonMark kinds 1-7) end the quote; "===", an indented line, a pipe row or inline HTML continue it. Our own picture lines and `$$` end it too. - The absorbed lines are part of the quote's stored body: its source line, the next block's, the tint, search, copy and the quote-body renderers all see them. - A Setext title is paragraph text: "> a" or "- a" over "---" or "===" was a heading titled "> a" / "- a"; it is the quote or list now, then a rule (or the text goes on), as on GitHub. Every expected shape is GitHub's (gh api markdown); the tests parse with the hang guard. Co-Authored-By: Claude Opus 5.5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 157 +++++++++++++++++- .../QuickMDTests/VerticalRhythmTests.swift | 110 +++++++++++- 2 files changed, 259 insertions(+), 8 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 51372d3..7b93f8e 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -13,7 +13,7 @@ struct MarkdownBlockParser: Sendable { private let renderer: MarkdownRenderer // Static precompiled regex (avoid recompilation per parse call) - private static let headerRegex = try! NSRegularExpression(pattern: MarkdownTheme.headerPattern) + static let headerRegex = try! NSRegularExpression(pattern: MarkdownTheme.headerPattern) private static let refLinkDefRegex = try! NSRegularExpression(pattern: MarkdownTheme.referenceLinkDefinitionPattern) private static let footnoteDefRegex = try! NSRegularExpression(pattern: MarkdownTheme.footnoteDefinitionPattern) @@ -284,9 +284,13 @@ struct MarkdownBlockParser: Sendable { continue } - // Setext headers (underlined headers) + // Setext headers (underlined headers). The title is paragraph + // text: a quote line or a list item never is one (GitHub: `> a` + // or `- a` over `---` is the quote or list, then a rule; over + // `===`, the quote's or item's text goes on). let trimmed = trimmedLine - if i + 1 < lines.count && !trimmed.isEmpty && !trimmed.contains("|") { + if i + 1 < lines.count && !trimmed.isEmpty && !trimmed.contains("|") + && !trimmed.hasPrefix(">") && !MarkdownRenderer.isListItem(line) { let nextLine = Self.markerLine(lines[i + 1]) if nextLine.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil { @@ -377,11 +381,24 @@ struct MarkdownBlockParser: Sendable { let groupSourceLine = lineMap[i] let groupLevel = level var groupLines = [String(scanner)] + var tip = QuoteTip() + tip.take(groupLines[0]) i += 1 while i < lines.count { let nextLine = Self.markerLine(lines[i]) - guard nextLine.hasPrefix(">") else { break } + guard nextLine.hasPrefix(">") else { + // No marker: a lazy continuation of the paragraph + // the group ends in (CommonMark), or the quote's end. + guard tip.endsInParagraph, + continuesQuoteLazily(lines[i], htmlImageLine: htmlImageLine(lines, at: i) != nil, + references: referenceDefinitions) + else { break } + groupLines.append(lines[i]) + tip.take(lines[i]) + i += 1 + continue + } var nextLevel = 0 var nextScanner = nextLine[...] @@ -393,8 +410,23 @@ struct MarkdownBlockParser: Sendable { } } - guard nextLevel == groupLevel else { break } + if nextLevel != groupLevel { + // Fewer markers than the group: what follows them may + // continue the group's paragraph lazily (`> > a` then + // `> b` is one paragraph of the inner quote). + let content = String(nextScanner) + guard nextLevel < groupLevel, tip.endsInParagraph, + continuesQuoteLazily(content, + htmlImageLine: HTMLImageSyntax.scan(content[...]) != nil, + references: referenceDefinitions) + else { break } + groupLines.append(content) + tip.take(content) + i += 1 + continue + } groupLines.append(String(nextScanner)) + tip.take(groupLines[groupLines.count - 1]) i += 1 } @@ -959,6 +991,121 @@ struct MarkdownBlockParser: Sendable { flushRow() } + // MARK: - Quote laziness + + /// What a quote group's lines end in, as CommonMark reads them: an OPEN + /// paragraph (a paragraph's or a list item's text) is the one block a + /// lazy line may continue. A blank or empty quote line closes it, as do a + /// fenced code block (open or just closed), an ATX heading, a Setext + /// underline, a rule and a table. + struct QuoteTip { + private(set) var endsInParagraph = false + private var fence: (marker: Character, length: Int)? + private var inTable = false + private var previousHadPipe = false + + mutating func take(_ content: String) { + let marker = MarkdownBlockParser.markerLine(content) + if let open = fence { + // Inside the quote's fence: only its closing line matters. + if marker.hasPrefix(String(repeating: open.marker, count: open.length)), + MarkdownRenderer.isBlankLine(marker.drop(while: { $0 == open.marker })) { + fence = nil + } + endsInParagraph = false + return + } + defer { previousHadPipe = content.contains("|") } + if MarkdownRenderer.isBlankLine(content) { + endsInParagraph = false + inTable = false + return + } + if let first = marker.first, first == "`" || first == "~" { + let length = marker.prefix(while: { $0 == first }).count + if length >= 3 { + fence = (first, length) + endsInParagraph = false + inTable = false + return + } + } + if inTable, content.contains("|") { + endsInParagraph = false + return + } + inTable = false + if endsInParagraph, previousHadPipe, + marker.range(of: MarkdownTheme.tableSeparatorPattern, options: .regularExpression) != nil { + inTable = true + endsInParagraph = false + return + } + let nsRange = NSRange(content.startIndex..., in: content) + if MarkdownBlockParser.headerRegex.firstMatch(in: content, range: nsRange) != nil + || MarkdownRenderer.isThematicBreak(content) { + endsInParagraph = false + return + } + if endsInParagraph, + marker.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil + || marker.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { + endsInParagraph = false + return + } + endsInParagraph = true + } + } + + /// Whether `line` — a line without a quote marker, or what follows fewer + /// markers than the quote has — continues the paragraph a quote ends in + /// (CommonMark laziness): it is not blank and starts no block. Checked as + /// cmark does, against the DOCUMENT, not the paragraph: any list item ends + /// the quote (`2. b` too), and so does a lone HTML tag; `===`, an indented + /// line, a pipe row or `` text continue it (GitHub, gh api markdown). + /// Our own block starts end it as well: a picture line (Markdown or HTML) + /// and `$$` stay the picture and the formula they are everywhere else. + private func continuesQuoteLazily(_ line: String, htmlImageLine: Bool, + references: [String: String]) -> Bool { + guard !Self.isBlank(line), !htmlImageLine else { return false } + let marker = Self.markerLine(line) + if marker.hasPrefix("```") || marker.hasPrefix("~~~") || marker.hasPrefix("$$") { return false } + let nsRange = NSRange(line.startIndex..., in: line) + if Self.headerRegex.firstMatch(in: line, range: nsRange) != nil { return false } + if MarkdownRenderer.isThematicBreak(line) || MarkdownRenderer.isListItem(line) { return false } + if ["-", "*", "+"].contains(marker) { return false } // an empty item + if Self.startsHTMLBlock(marker) { return false } + if InlineSyntaxScanner.standaloneImages(in: line[...], references: references) != nil { return false } + return true + } + + /// CommonMark's HTML block starts (kinds 1-7), as far as a lazy line needs + /// them: ``, `text` is inline HTML. + static func startsHTMLBlock(_ marker: String) -> Bool { + guard marker.hasPrefix("<") else { return false } + if marker.hasPrefix("" || rest.hasPrefix("/>") + if ends, htmlBlockTags.contains(name) { return true } + return marker.range(of: #"^]*)?/?>[ \t]*$"#, + options: .regularExpression) != nil + } + + /// CommonMark HTML block kinds 1 and 6. + private static let htmlBlockTags: Set = [ + "pre", "script", "style", "textarea", + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", + "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", + "header", "hr", "html", "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", "summary", "table", + "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul", + ] + /// A text row waiting for `renderPendingRows`: its soft-break joined lines /// and where its block sits in the block list. struct PendingTextRow { diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 62a24e1..bb3e623 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -381,7 +381,9 @@ final class VerticalRhythmTests: XCTestCase { } func testNestedQuoteGroupsJoinTheirQuote() { - let blocks = parse("Text.\n\n> outer\n>> inner\n> outer again\n\n> separate quote") + // The empty `>` line closes the inner quote's paragraph: without it + // `> outer again` would continue that paragraph lazily (CommonMark). + let blocks = parse("Text.\n\n> outer\n>> inner\n>\n> outer again\n\n> separate quote") XCTAssertEqual(blocks.count, 5) XCTAssertEqual(blocks.map(\.joinsPrevious), [false, false, true, true, false]) let measured = measure(blocks) @@ -532,10 +534,12 @@ final class VerticalRhythmTests: XCTestCase { let name = String(format: "U+%04X", space.unicodeScalars.first!.value) let blocks = try XCTUnwrap(parseReturning("Intro\n\n\(space)> quoted\n"), name) XCTAssertEqual(blocks.map(plain), ["Intro", "\(space)> quoted"], name) - // Inside a quote run too: the run ends there, and the parser goes on. + // Inside a quote run too: no marker, so the line is the quote + // paragraph's lazy continuation, text and all (GitHub: + // `

a > b

`), and the parser goes on. let inside = try XCTUnwrap(parseReturning("> a\n\(space)> b\n\nAfter."), name) if case .blockquote(let content, _) = inside.first?.content { - XCTAssertEqual(content, "a", name) + XCTAssertEqual(content, "a\n\(space)> b", name) } else { XCTFail("\(name): \(String(describing: inside.first?.content))") } @@ -606,6 +610,106 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertTrue(MarkdownRenderer.isThematicBreak(" ---\t")) } + // MARK: - Quote laziness + + /// The parse in short: `q:` for a quote, `alert::`, + /// `t:`, `c:`, `h:`, `img`, `table`. + private func quoteShape(_ markdown: String, file: StaticString = #filePath, + line: UInt = #line) throws -> [String] { + try XCTUnwrap(parseReturning(markdown, file: file, line: line), file: file, line: line).map { block in + switch block.content { + case .blockquote(let content, let level): return "q\(level):\(content)" + case .alert(let kind, let content): return "alert:\(kind.rawValue):\(content)" + case .text(let attributed): return "t:" + String(attributed.characters) + case .codeBlock(let code, _): return "c:\(code)" + case .heading(_, let title, _): return "h:\(title)" + case .image: return "img" + case .table: return "table" + default: return "other" + } + } + } + + /// CommonMark laziness: a line without the quote marker that would be a + /// paragraph's continuation continues the paragraph the quote ends in — + /// in a nested quote, a list item or an alert too, and on a line with + /// fewer markers than the quote. Our parser ended the quote and made the + /// line a paragraph of its own. Every expected shape is GitHub's + /// (`gh api markdown`, scratchpad/audit/gfm/lazy_*.html). + func testALazyLineContinuesTheParagraphAQuoteEndsIn() throws { + // Continues: a plain line, a hard-wrapped quote, `===`, an indented + // line, an item that may not start a list here (none may: a lazy + // line is checked against the document, not the paragraph). + XCTAssertEqual(try quoteShape("> a\nb\n"), ["q1:a\nb"]) + XCTAssertEqual(try quoteShape("> line one\nline two\nline three\n\nAfter\n"), + ["q1:line one\nline two\nline three", "t:After"]) + XCTAssertEqual(try quoteShape("> a\n===\n"), ["q1:a\n==="]) + XCTAssertEqual(try quoteShape("> a\n b\n"), ["q1:a\n b"]) + XCTAssertEqual(try quoteShape("> a\n<span>x</span>\n").first, "q1:a\n<span>x</span>") + XCTAssertEqual(try quoteShape("> a\nb\n> c\n"), ["q1:a\nb\nc"]) + // In a list item, a nested quote, an alert; and with fewer markers. + XCTAssertEqual(try quoteShape("> - a\nb\n"), ["q1:- a\nb"]) + XCTAssertEqual(try quoteShape("> > a\nb\n"), ["q2:a\nb"]) + XCTAssertEqual(try quoteShape("> > a\n> b\n"), ["q2:a\nb"]) + XCTAssertEqual(try quoteShape("> > > a\n> b\n"), ["q3:a\nb"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> text\nlazy\n"), ["alert:note:text\nlazy"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\nlazy\n"), ["alert:note:lazy"]) + + // Ends the quote: a blank line, an empty quote line, a block start. + XCTAssertEqual(try quoteShape("> a\n\nb\n"), ["q1:a", "t:b"]) + XCTAssertEqual(try quoteShape("> a\n>\nb\n"), ["q1:a", "t:b"]) + XCTAssertEqual(try quoteShape("> a\n```\ncode\n```\n"), ["q1:a", "c:code"]) + XCTAssertEqual(try quoteShape("> a\n~~~\ncode\n~~~\n"), ["q1:a", "c:code"]) + XCTAssertEqual(try quoteShape("> a\n# H\n"), ["q1:a", "h:H"]) + XCTAssertEqual(try quoteShape("> a\n---\n").count, 2) + XCTAssertEqual(try quoteShape("> a\n***\n").count, 2) + XCTAssertEqual(try quoteShape("> a\n- b\n"), ["q1:a", "t:• b"]) + XCTAssertEqual(try quoteShape("> a\n2. b\n").first, "q1:a") + XCTAssertEqual(try quoteShape("> a\n2. b\n").count, 2) + XCTAssertEqual(try quoteShape("> a\n<div>x</div>\n").first, "q1:a") + XCTAssertEqual(try quoteShape("> a\n<!-- c -->\n").first, "q1:a") + XCTAssertEqual(try quoteShape("> > a\n> - b\n"), ["q2:a", "q1:- b"]) + // Our own block starts end it too: a picture line, `$$` (GitHub reads + // both as the paragraph's text; here they stay the picture and the + // formula they are everywhere else). + XCTAssertEqual(try quoteShape("> a\n![img](x.png)\n"), ["q1:a", "img"]) + XCTAssertEqual(try quoteShape("> a\n$$\nx\n$$\n").first, "q1:a") + + // Nothing to continue: the quote does not end in a paragraph. + XCTAssertEqual(try quoteShape("> ```\n> code\nb\n"), ["q1:```\ncode", "t:b"]) + XCTAssertEqual(try quoteShape("> # H\nb\n"), ["q1:# H", "t:b"]) + XCTAssertEqual(try quoteShape("> a\n> ===\nb\n"), ["q1:a\n===", "t:b"]) + XCTAssertEqual(try quoteShape("> | x | y |\n> |---|---|\n> | 1 | 2 |\nlazy\n"), + ["q1:| x | y |\n|---|---|\n| 1 | 2 |", "t:lazy"]) + } + + /// The lines a quote absorbed are the quote's: its source line is its + /// first, the next block starts after them, the tint spans them, search + /// finds and copy reads them in the quote — and a long hard-wrapped quote + /// with a marker on its first line only parses (the hang guard) into one + /// quote row. + func testTheLinesAQuoteAbsorbedAreTheQuotes() throws { + let markdown = "Intro\n\n> Quoted start\nlazy middle word\nlazy end\n\nAfter\n" + let blocks = try XCTUnwrap(parseReturning(markdown)) + XCTAssertEqual(blocks.count, 3) + XCTAssertEqual(blocks.map(\.sourceLine), [0, 2, 6]) + let ns = markdown as NSString + let spans = SourceTint.spans(for: blocks, lineStarts: SourceEditSupport.lineStarts(in: ns), in: ns) + XCTAssertEqual(spans.map { ns.substring(with: $0.range) }, ["> Quoted start\nlazy middle word\nlazy end"]) + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "middle").matchBlockIds, [blocks[1].id]) + let measured = measure(blocks) + XCTAssertEqual(measured.converted[blocks[1].id].map(\.string), "Quoted start lazy middle word lazy end") + + let long = "> first\n" + String(repeating: "a hard-wrapped line of the quote\n", count: 5_000) + "\nAfter\n" + let longBlocks = try XCTUnwrap(parseReturning(long, within: 10)) + XCTAssertEqual(longBlocks.count, 2) + if case .blockquote(let content, _) = longBlocks[0].content { + XCTAssertEqual(content.components(separatedBy: "\n").count, 5_001) + } else { + XCTFail("\(longBlocks[0].content)") + } + } + // MARK: - Anchor signatures /// The scroll anchor is found again after a reload by the block's content From c0e3378fe90b8d4a02aaf8d9442609c3e1325b05 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:00:10 +0100 Subject: [PATCH 066/105] fix(math): a formula keeps its LaTeX escapes Review R11: inline math is found in the rendered text, after every backslash escape ran, and a633405 made all ASCII punctuation escapable: $50\%$ reached SwiftMath as "50%" (a TeX comment, typeset "50"), \, \; \: thin spaces were lost, \{ \} \_ never survived. Generalises f06ad86: the escape tags EVERY escaped character (EscapedCharacterAttribute). InlineMathSegmenter.pieces(of:) splits with those offsets (an escaped $ never delimits) and builds each formula's LaTeX with a backslash put back before every escaped character, by offset. Text outside formulas is unchanged. BlockTextConverter (screen, measurer, search, the copy tag) and PrintableMath (print/PDF) both use pieces(of:). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 15 ++-- QuickMD/QuickMD/InlineMathSegmenter.swift | 93 ++++++++++++++++------- QuickMD/QuickMD/MarkdownExport.swift | 14 ++-- QuickMD/QuickMD/MarkdownRenderer.swift | 5 +- QuickMD/QuickMDTests/RendererTests.swift | 35 ++++++++- 5 files changed, 113 insertions(+), 49 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index faea09f..b818a9c 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -737,16 +737,13 @@ enum BlockTextConverter { ?? NSAttributedString(string: String(attributed.characters)) } - let plain = String(attributed.characters) - let segments = InlineMathSegmenter.split(plain, - literalDollars: InlineMathSegmenter.literalDollars(in: attributed)) let result = NSMutableAttributedString() var attrIndex = attributed.startIndex - for segment in segments { - switch segment { + for piece in InlineMathSegmenter.pieces(of: attributed) { + switch piece.segment { case .text(let str): - if let end = attributed.characters.index(attrIndex, offsetBy: str.count, + if let end = attributed.characters.index(attrIndex, offsetBy: piece.length, limitedBy: attributed.endIndex) { let slice = AttributedString(attributed[attrIndex..<end]) if let ns = try? NSAttributedString(slice, including: \.appKit) { @@ -757,11 +754,9 @@ enum BlockTextConverter { attrIndex = end } - case .math(let source): + case .math(let latex): // Skip past the $latex$ in the source AttributedString - let skipCount = source.count + 2 - let latex = InlineMathSegmenter.tex(source) - if let end = attributed.characters.index(attrIndex, offsetBy: skipCount, + if let end = attributed.characters.index(attrIndex, offsetBy: piece.length, limitedBy: attributed.endIndex) { attrIndex = end } diff --git a/QuickMD/QuickMD/InlineMathSegmenter.swift b/QuickMD/QuickMD/InlineMathSegmenter.swift index cda8675..d9801b0 100644 --- a/QuickMD/QuickMD/InlineMathSegmenter.swift +++ b/QuickMD/QuickMD/InlineMathSegmenter.swift @@ -1,14 +1,17 @@ import Foundation -/// Marks a `$` the author escaped (`\$5`): the renderer draws it as "$", -/// and `InlineMathSegmenter` never opens or closes a formula on it — the math -/// pass runs on the RENDERED text, where the backslash is gone. A dollar made -/// by a character reference (`$`) is NOT tagged: GitHub reads that one as -/// a math delimiter. Not part of the AppKit scope, so the NSAttributedString a -/// view installs never carries it. -enum LiteralDollarAttribute: AttributedStringKey { +/// Marks a character the author backslash-escaped (`\$`, `\%`, `\,`): the +/// renderer draws it without the backslash, but inline math is found in the +/// RENDERED text, so `InlineMathSegmenter` needs to know what was escaped: +/// - an escaped `$` never opens or closes a formula (`Price \$5 and \$10`); +/// - inside a formula every escaped character gets its backslash back, so +/// the LaTeX reaches SwiftMath intact (`$50\%$`, `$a\,b$`, `$\{x\}$`). +/// A dollar made by a character reference (`$`) is NOT tagged: GitHub +/// reads that one as a math delimiter. Not part of the AppKit scope, so the +/// NSAttributedString a view installs never carries it. +enum EscapedCharacterAttribute: AttributedStringKey { typealias Value = Bool - static let name = "qmdLiteralDollar" + static let name = "qmdEscapedCharacter" } /// Splits paragraph text into alternating plain-text and inline-math segments. @@ -16,43 +19,77 @@ enum LiteralDollarAttribute: AttributedStringKey { /// Pure Foundation — shared by TextBlockView (app) and the unit tests. enum InlineMathSegmenter { - /// The indices, in `String(attributed.characters)`, of the dollars the - /// author escaped (`LiteralDollarAttribute`). Empty for almost every - /// paragraph: only tagged runs are walked. - static func literalDollars(in attributed: AttributedString) -> Set<Int> { + /// The Character offsets, in `String(attributed.characters)`, of every + /// character the author escaped (`EscapedCharacterAttribute`). Empty for + /// almost every paragraph: only tagged runs are walked. + static func escapedCharacters(in attributed: AttributedString) -> Set<Int> { var offsets: Set<Int> = [] let characters = attributed.characters - for run in attributed.runs where run[LiteralDollarAttribute.self] == true { + for run in attributed.runs where run[EscapedCharacterAttribute.self] == true { var offset = characters.distance(from: characters.startIndex, to: run.range.lowerBound) - for character in characters[run.range] { - if character == "$" { offsets.insert(offset) } + for _ in characters[run.range] { + offsets.insert(offset) offset += 1 } } return offsets } - /// The LaTeX of a math segment: a `$` inside one can only be an escaped - /// dollar (any other would have closed it), so it goes back to `\$` - /// (`$a\$b$` typesets "a$b"). The segment's SOURCE length is still - /// `latex.count + 2`. - static func tex(_ latex: String) -> String { - latex.contains("$") ? latex.replacingOccurrences(of: "$", with: "\\$") : latex - } - enum Segment: Equatable { case text(String) case math(String) } - /// `literalDollars`: Character offsets of escaped dollars - /// (`literalDollars(in:)`) — never a delimiter, so they stay text or, inside - /// a formula, part of its LaTeX (`tex`). - static func split(_ text: String, literalDollars: Set<Int> = []) -> [Segment] { + /// A segment as the views draw it: text as is, a formula as the LaTeX to + /// typeset (escaped characters with their backslash back, `tex`), with + /// the number of Characters it spans in the rendered text (`$…$`). + struct Piece: Equatable { + let segment: Segment + let length: Int + } + + /// The paragraph's text and formulas, from the renderer's string: the one + /// split the screen, the measurer, search and print share. + static func pieces(of attributed: AttributedString) -> [Piece] { + let plain = String(attributed.characters) + let escaped = escapedCharacters(in: attributed) + var offset = 0 + return split(plain, escaped: escaped).map { segment in + switch segment { + case .text(let text): + offset += text.count + return Piece(segment: segment, length: text.count) + case .math(let source): + let piece = Piece(segment: .math(tex(source, escaped: escaped, contentStart: offset + 1)), + length: source.count + 2) + offset += piece.length + return piece + } + } + } + + /// A formula's LaTeX: `source` (as rendered) with a backslash put back + /// before every character that was escaped — by offset, never by + /// character, so a `$` or `%` the author did not escape stays as it is. + /// `contentStart` is the offset of `source`'s first character. + static func tex(_ source: String, escaped: Set<Int>, contentStart: Int) -> String { + guard !escaped.isEmpty else { return source } + var latex = "" + for (index, character) in source.enumerated() { + if escaped.contains(contentStart + index) { latex.append("\\") } + latex.append(character) + } + return latex + } + + /// `escaped`: Character offsets of escaped characters + /// (`escapedCharacters(in:)`). An escaped `$` is never a delimiter, so it + /// stays text or, inside a formula, part of its LaTeX. + static func split(_ text: String, escaped: Set<Int> = []) -> [Segment] { var segments: [Segment] = [] var remaining = text[...] var buffer = "" - let literal: Set<String.Index> = Set(literalDollars.compactMap { + let literal: Set<String.Index> = Set(escaped.compactMap { text.index(text.startIndex, offsetBy: $0, limitedBy: text.endIndex) }) /// The next delimiter `$` at or after `from`. diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index dbceaba..68f484b 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -935,21 +935,19 @@ enum PrintableMath { var result = Text("") var index = attributed.startIndex - for segment in InlineMathSegmenter.split(plain, - literalDollars: InlineMathSegmenter.literalDollars(in: attributed)) { - switch segment { - case .text(let str): - if let end = attributed.characters.index(index, offsetBy: str.count, + for piece in InlineMathSegmenter.pieces(of: attributed) { + switch piece.segment { + case .text: + if let end = attributed.characters.index(index, offsetBy: piece.length, limitedBy: attributed.endIndex) { result = result + Text(AttributedString(attributed[index..<end])) index = end } - case .math(let source): - if let end = attributed.characters.index(index, offsetBy: source.count + 2, + case .math(let latex): + if let end = attributed.characters.index(index, offsetBy: piece.length, limitedBy: attributed.endIndex) { index = end } - let latex = InlineMathSegmenter.tex(source) if let rendered = inlineImage(latex) { // Sit the image on the baseline: its own descent goes below. result = result + Text(Image(nsImage: rendered.image)).baselineOffset(-rendered.descent) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 84bb030..b3dd341 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1095,8 +1095,9 @@ struct MarkdownRenderer: Sendable { var attr = AttributedString(String(escaped)) attr.setDualFont(size: scaled(14), fonts: theme.fonts) attr.setDualForeground(theme.textColor) - // `\$` is a dollar sign, never a math delimiter (`InlineMathSegmenter`). - if escaped == "$" { attr[LiteralDollarAttribute.self] = true } + // Inline math is found in the rendered text: tagged so `\$` is never + // a delimiter and a formula gets its LaTeX escapes back. + attr[EscapedCharacterAttribute.self] = true return (attr, remaining.dropFirst(2)) } diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index f32f27d..b6143bd 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -828,6 +828,39 @@ final class RendererTests: XCTestCase { } + // MARK: - LaTeX escapes inside inline math (review R11) + + /// Inline math is found in the RENDERED text, after every `\<punct>` + /// escape ran; the formula must still reach the engine with its LaTeX + /// escapes (`$50\%$` typesets "50%": a bare `%` starts a TeX comment). + /// Text outside the formula keeps Markdown's escapes. The copy tag carries + /// the same LaTeX, and print uses the same `pieces(of:)`. + func testInlineMathKeepsItsLaTeXEscapes() throws { + for (source, latex) in [("$50\\%$", "50\\%"), ("$a\\,b$", "a\\,b"), ("$\\{x\\}$", "\\{x\\}"), + ("$a\\_b$", "a\\_b"), ("$a\\$b$", "a\\$b"), ("$a\\;b\\:c$", "a\\;b\\:c")] { + XCTAssertEqual(shownParagraph("x " + source + " y").formulas, [latex], source) + } + let mixed = shownParagraph("Price \\$5, \\* and $a\\,b + \\{c\\}$ then \\_") + XCTAssertEqual(mixed.formulas, ["a\\,b + \\{c\\}"]) + XCTAssertEqual(mixed.text, "Price $5, * and \u{FFFC} then _") + + // The copy tag (DocumentCopyBuilder writes `$<tag>$`) is the LaTeX too. + let theme = MarkdownTheme.cached(for: .light) + guard case .text(let attributed)? = MarkdownBlockParser(theme: theme).parse("t $50\\%$").first?.content else { + return XCTFail("paragraph") + } + let ns = BlockTextConverter.makeNSAttributedString( + from: attributed, hasInlineMath: true, theme: theme, fontScale: 1, + math: MathRendering(inlineImage: { _, _, _ in NSImage(size: NSSize(width: 1, height: 1)) }, + displayHeight: { _, _, _ in 0 })) + var tags: [String] = [] + ns.enumerateAttribute(.qmdInlineMathSource, in: NSRange(location: 0, length: ns.length)) { value, _, _ in + if let value = value as? String { tags.append(value) } + } + XCTAssertEqual(tags, ["50\\%"]) + XCTAssertEqual(InlineMathSegmenter.pieces(of: attributed).map(\.length), [2, 5], "spans in the rendered text: $50%$") + } + // MARK: - References in link destinations (review R10) /// A destination's character references are decoded (hrefs from @@ -908,7 +941,7 @@ final class RendererTests: XCTestCase { XCTAssertTrue(cell.contains("$5 and $10\n")) XCTAssertEqual(String(renderer.renderQuotedBody("Pay \\$5").characters), "Pay $5") // The segmenter itself: offsets of escaped dollars never delimit. - XCTAssertEqual(InlineMathSegmenter.split("a $5 and $6", literalDollars: [2, 9]), [.text("a $5 and $6")]) + XCTAssertEqual(InlineMathSegmenter.split("a $5 and $6", escaped: [2, 9]), [.text("a $5 and $6")]) } } From fa3f125f42f0c20894283718d7a0614ea9b86a33 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:01:20 +0100 Subject: [PATCH 067/105] fix(alerts): an alert marker takes spaces and tabs around it, nothing else AlertKind trimmed its marker line with .whitespaces, so `> [!NOTE]` followed by a no-break space was an alert here and a plain quote on GitHub (gh api markdown, gfm mode). It trims spaces and tabs only now (MarkdownRenderer.markerWhitespace); two trailing spaces or a tab still make the alert, on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlock.swift | 4 +++- QuickMD/QuickMDTests/VerticalRhythmTests.swift | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift index 23c2a74..e36d1be 100644 --- a/QuickMD/QuickMD/MarkdownBlock.swift +++ b/QuickMD/QuickMD/MarkdownBlock.swift @@ -12,8 +12,10 @@ enum AlertKind: String, Sendable, CaseIterable { /// Parses a marker line (already stripped of `>` prefixes). The marker must /// be the only thing on the line — `[!NOTE] text` is a regular blockquote. /// Case-insensitive, matching GitHub's behavior. + /// Spaces and tabs around the marker only (`MarkdownRenderer.markerWhitespace`): + /// GitHub reads `[!NOTE]` + a no-break space as a plain quote. init?(markerLine: String) { - let trimmed = markerLine.trimmingCharacters(in: .whitespaces) + let trimmed = markerLine.trimmingCharacters(in: MarkdownRenderer.markerWhitespace) guard trimmed.hasPrefix("[!"), trimmed.hasSuffix("]") else { return nil } let name = trimmed.dropFirst(2).dropLast().lowercased() self.init(rawValue: name) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index bb3e623..955d088 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -495,6 +495,18 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertNil(aim.blockId) // a new table } + /// An alert marker is `[!KIND]` with spaces or tabs around it (GitHub, + /// gh api markdown in gfm mode): a no-break space after it leaves a + /// plain quote; two trailing spaces still make the alert. + func testAnAlertMarkerTakesSpacesAndTabsOnly() throws { + XCTAssertEqual(try quoteShape("> [!NOTE]\u{00A0}\n> body\n"), ["q1:[!NOTE]\u{00A0}\nbody"]) + XCTAssertEqual(try quoteShape("> [!NOTE] \n> body\n"), ["alert:note:body"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\t\n> body\n"), ["alert:note:body"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> body\n"), ["alert:note:body"]) + XCTAssertNil(AlertKind(markerLine: "\u{2003}[!TIP]")) + XCTAssertEqual(AlertKind(markerLine: " [!TIP] "), .tip) + } + // MARK: - Block markers: spaces and tabs only /// Parses on a worker thread: a parser that never returns fails the test From 9894f25ac387283400241f8f429fd13bcb8fdae4 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:02:17 +0100 Subject: [PATCH 068/105] fix(search): only a wheel event that scrolls drops a jump's aim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlockScrollView.scrollWheel dropped the pending aim at the match for every wheel event, including fingers resting on the trackpad (phase .mayBegin), a gesture's end and zero-delta events: ⌘G with fingers on the trackpad landed on the block's centre instead of the match. PendingMatchAim.isReaderScroll: a vertical delta, or a gesture or its momentum beginning or going on. The other events leave the aim alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/VerticalRhythm.swift | 11 +++++++++++ QuickMD/QuickMD/Views/VirtualBlockList.swift | 4 +++- .../QuickMDTests/VerticalRhythmTests.swift | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/VerticalRhythm.swift b/QuickMD/QuickMD/VerticalRhythm.swift index ca9d9a0..5bfc14c 100644 --- a/QuickMD/QuickMD/VerticalRhythm.swift +++ b/QuickMD/QuickMD/VerticalRhythm.swift @@ -356,6 +356,17 @@ struct PendingMatchAim: Equatable { /// Taken, given up, or a new table. mutating func drop() { blockId = nil } + + /// Whether a scroll-wheel event is the reader's scroll: it moves the + /// document (a vertical delta), or a gesture or its momentum begins or + /// goes on. Fingers resting on the trackpad (`.mayBegin`), a gesture's + /// end and zero-delta events move nothing and leave the aim alone. + static func isReaderScroll(_ phase: NSEvent.Phase, _ momentumPhase: NSEvent.Phase, + _ deltaY: CGFloat) -> Bool { + if deltaY != 0 { return true } + let moving: NSEvent.Phase = [.began, .changed] + return !phase.isDisjoint(with: moving) || !momentumPhase.isDisjoint(with: moving) + } } // MARK: - Print / PDF diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index b96e247..8f5926e 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -1522,7 +1522,9 @@ final class BlockScrollView: NSScrollView { var onReaderScroll: (() -> Void)? override func scrollWheel(with event: NSEvent) { - onReaderScroll?() + if PendingMatchAim.isReaderScroll(event.phase, event.momentumPhase, event.scrollingDeltaY) { + onReaderScroll?() + } super.scrollWheel(with: event) } /// Clicks in the content insets above the first / below the last row land diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 955d088..df6aa2b 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -507,6 +507,25 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(AlertKind(markerLine: " [!TIP] "), .tip) } + /// Which wheel events are the reader's scroll and drop a jump's aim: the + /// ones that move the document. Fingers resting on the trackpad + /// (`.mayBegin`), a gesture's end and zero-delta events used to drop it + /// too, so ⌘G with fingers on the trackpad landed on the block's centre. + func testOnlyAWheelEventThatScrollsDropsTheAim() { + let scrolls = PendingMatchAim.isReaderScroll + XCTAssertFalse(scrolls([.mayBegin], [], 0)) // fingers resting + XCTAssertFalse(scrolls([], [], 0)) // a zero-delta event + XCTAssertFalse(scrolls([.ended], [], 0)) // the gesture lifts + XCTAssertFalse(scrolls([.cancelled], [], 0)) + XCTAssertFalse(scrolls([.stationary], [], 0)) + XCTAssertFalse(scrolls([], [.ended], 0)) // momentum runs out + XCTAssertTrue(scrolls([], [], -3)) // a mouse wheel notch + XCTAssertTrue(scrolls([.began], [], 0)) // a gesture starts + XCTAssertTrue(scrolls([.changed], [], 1.5)) + XCTAssertTrue(scrolls([], [.began], 0)) // momentum + XCTAssertTrue(scrolls([], [.changed], -12)) + } + // MARK: - Block markers: spaces and tabs only /// Parses on a worker thread: a parser that never returns fails the test From 1ed16c1afc43686b3206163d55aef5471e94c398 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:02:21 +0100 Subject: [PATCH 069/105] fix(links): a link's text with a backslash escape is inline-parsed Review R12: link text was drawn raw unless it held an image, so [cost \$5 and \$6](u) showed "cost \" plus a formula, and [a \* b](u) kept its backslash; GitHub shows "cost $5 and $6" and "a * b". MarkdownRenderer.parsesLinkText (an image or a backslash) decides it for the renderer and for ReferenceLinks' rewrite alike; the text is parsed with links disabled, as the image case always was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownRenderer.swift | 18 ++++++++++++++---- QuickMD/QuickMD/ReferenceLinks.swift | 4 ++-- QuickMD/QuickMDTests/RendererTests.swift | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index b3dd341..abf5d01 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -981,15 +981,25 @@ struct MarkdownRenderer: Sendable { return nil } + /// Whether a link's text is inline-parsed (with links disabled) rather + /// than drawn as written: when it holds an image (`[![b](img)](link)`) + /// or a backslash escape (`[cost \$5 and \$6](u)`, `[a \* b](u)` — GitHub + /// shows "cost $5 and $6", "a * b"). `ReferenceLinks` follows the same + /// rule when it rewrites a link's text. + static func parsesLinkText(_ text: String) -> Bool { + text.contains("![") || text.contains("\\") + } + private func makeLink(text: String, url: String, remaining: Substring) -> (AttributedString, Substring) { var attr: AttributedString - if text.contains("![") { + if Self.parsesLinkText(text) { // Link-wrapped image (`[![b](img)](link)`): the text is rendered // inline so the image becomes its "[Image: b]" placeholder, and the // whole run carries the OUTER link (set below, over the image's - // own). Only for text holding an image, so every other link keeps - // its exact plain-text rendering. Rendered with links disabled: - // a link inside a link is literal text (bounds the recursion). + // own); an escape loses its backslash (and an escaped `$` is never + // math). Every other link keeps its exact plain-text rendering. + // Rendered with links disabled: a link inside a link is literal + // text (bounds the recursion). var linkText = self linkText.linksDisabled = true attr = linkText.renderInlineFormatting(text) diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index 7b5e67b..d116b04 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -153,7 +153,7 @@ enum ReferenceLinks { if remaining[afterBracket...].hasPrefix("(") { guard let inline = InlineSyntaxScanner.parenthesizedDestination(remaining[afterBracket...], pairs: pairs) else { return nil } - guard linkText.contains("![") else { + guard MarkdownRenderer.parsesLinkText(String(linkText)) else { return (String(remaining[..<inline.end]), remaining[inline.end...]) } let text = inlinedLine(linkText, sources: sources, linksDisabled: true) @@ -164,7 +164,7 @@ enum ReferenceLinks { let destination = inlineDestination(url) // The renderer parses a link's text only when it holds an image // (and then with links disabled) — rewrite it under the same rule. - let text = linkText.contains("![") + let text = MarkdownRenderer.parsesLinkText(String(linkText)) ? inlinedLine(linkText, sources: sources, linksDisabled: true) : linkText return ("[\(text)](\(destination))", rest) } diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index b6143bd..55e2306 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -861,6 +861,27 @@ final class RendererTests: XCTestCase { XCTAssertEqual(InlineMathSegmenter.pieces(of: attributed).map(\.length), [2, 5], "spans in the rendered text: $50%$") } + // MARK: - Escapes in link text (review R12) + + /// A link's text with a backslash escape is parsed like the image case: + /// GitHub shows "cost $5 and $6" (no formula) and "a * b"; a link without + /// escapes or images keeps its exact plain text. + func testLinkTextEscapesAreParsed() { + let cost = shownParagraph("[cost \\$5 and \\$6](https://x.example)") + XCTAssertEqual(cost.text, "cost $5 and $6") + XCTAssertEqual(cost.formulas, []) + let star = renderer.renderInline("[a \\* b](u) and [*plain*](v)") + XCTAssertEqual(String(star.characters), "a * b and *plain*") + XCTAssertEqual(Set(star.runs.compactMap { $0.link?.absoluteString }), ["u", "v"]) + XCTAssertTrue(star.runs.filter { String(star[$0.range].characters).contains("*") }.allSatisfy { $0.link != nil }, + "the whole link text stays linked") + // The same in a quote, through ReferenceLinks' rewrite. + let blocks = MarkdownBlockParser(theme: MarkdownTheme.cached(for: .light)) + .parse("> [a \\* b][d] and [c \\$5][d]\n\n[d]: https://d.example") + guard case .blockquote(let quote, _) = blocks[0].content else { return XCTFail("quote") } + XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "a * b and c $5") + } + // MARK: - References in link destinations (review R10) /// A destination's character references are decoded (hrefs from From 3dccab028df3759a145afe326fb3ae2cd4985964 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:03:08 +0100 Subject: [PATCH 070/105] =?UTF-8?q?fix(source-edit):=20the=20editor's=20fo?= =?UTF-8?q?nt=20follows=20=E2=8C=98+=20at=20once,=20its=20column=20the=20l?= =?UTF-8?q?ist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 636de0e fed renderedFontScale to the whole editor style, so the editor's text changed size only when the zoom's re-parse landed: about 2 s on an 11.7K-line document, and with ⌘+⌘+⌘+ (earlier parses superseded) once, at the end. Before it the editor zoomed at once. The style has two zooms now: fontScale (the font, the zoom asked for) and columnFontScale (the Reading Mode column cap, the zoom the list shows). Style.overDocument(theme:fontScale:shownFontScale:isReadingMode:) sets both; the editor's column stays the list's at every moment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownView.swift | 3 +- QuickMD/QuickMD/SourceEditorController.swift | 30 ++++++++----- .../SourceEditorControllerTests.swift | 45 ++++++++++++++++--- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index d8af58d..eaea016 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -324,7 +324,8 @@ struct MarkdownView: View { Group { if editSession.isActive { SourceEditorView(controller: editSession.editor, - style: .overDocument(theme: theme, shownFontScale: renderedFontScale, + style: .overDocument(theme: theme, fontScale: fontScale, + shownFontScale: renderedFontScale, isReadingMode: isReadingMode)) } } diff --git a/QuickMD/QuickMD/SourceEditorController.swift b/QuickMD/QuickMD/SourceEditorController.swift index 23ac428..a025d65 100644 --- a/QuickMD/QuickMD/SourceEditorController.swift +++ b/QuickMD/QuickMD/SourceEditorController.swift @@ -103,14 +103,20 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD /// `apply(style:)`, so SwiftUI may hand the same one on every update. struct Style: Equatable { var theme: MarkdownTheme + /// The editor's font size (the zoom asked for: it follows ⌘+ at once). var fontScale: Double var isReadingLayout: Bool + /// The zoom the Reading Mode column is capped at (`columnGeometry`); + /// nil = `fontScale`. Over a rendered document, the zoom the list + /// SHOWS (`overDocument`). + var columnFontScale: Double? = nil /// `MarkdownTheme` is not `Equatable`; compare what the editor reads /// from it. Colours as well as the name: a custom theme re-imported /// under the same name may change them. static func == (lhs: Style, rhs: Style) -> Bool { lhs.fontScale == rhs.fontScale + && lhs.columnFontScale == rhs.columnFontScale && lhs.isReadingLayout == rhs.isReadingLayout && lhs.theme.name == rhs.theme.name && lhs.theme.isDark == rhs.theme.isDark @@ -120,16 +126,18 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD && lhs.hasSameTintColors(as: rhs) } - /// The editor's style over a rendered document: at the zoom the - /// document is SHOWN at (`MarkdownView.renderedFontScale` — the scale - /// its blocks were parsed at and its Reading Mode column is capped - /// at), not the zoom just asked for. Between ⌘+ and the parse that - /// follows it the two differ; the editor's column (`columnGeometry`) - /// has to be the list's then too, or leaving Source Edit before the - /// parse lands moves the text sideways. The editor's font follows the - /// zoom when the parse lands, with the document's. - static func overDocument(theme: MarkdownTheme, shownFontScale: Double, isReadingMode: Bool) -> Style { - Style(theme: theme, fontScale: shownFontScale, isReadingLayout: isReadingMode) + /// The editor's style over a rendered document. Two zooms, because + /// between ⌘+ and the re-parse that follows it they differ: + /// - the font follows `fontScale`, the zoom asked for, at once; + /// - the Reading Mode column follows `shownFontScale` + /// (`MarkdownView.renderedFontScale` — the zoom the list's blocks + /// were parsed at and its column is capped at), so the editor's + /// column is the list's at every moment and leaving Source Edit + /// before the parse lands does not move the text sideways. + static func overDocument(theme: MarkdownTheme, fontScale: Double, shownFontScale: Double, + isReadingMode: Bool) -> Style { + Style(theme: theme, fontScale: fontScale, isReadingLayout: isReadingMode, + columnFontScale: shownFontScale) } /// The colours of the block tint (`paintTint`). @@ -479,7 +487,7 @@ final class SourceEditorController: NSObject, NSTextViewDelegate, NSTextStorageD defer { isUpdatingGeometry = false } let clip = scrollView.contentView.bounds.size let geometry = Self.columnGeometry(clipWidth: clip.width, isReadingLayout: style.isReadingLayout, - fontScale: CGFloat(style.fontScale)) + fontScale: CGFloat(style.columnFontScale ?? style.fontScale)) let inset = NSSize(width: geometry.horizontalInset, height: geometry.verticalInset) // At least as tall as the clip, so a click below a short text still // lands in the text view (and places the caret at the end). diff --git a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift index 9c95bde..6cea7b8 100644 --- a/QuickMD/QuickMDTests/SourceEditorControllerTests.swift +++ b/QuickMD/QuickMDTests/SourceEditorControllerTests.swift @@ -613,15 +613,16 @@ final class SourceEditorControllerTests: XCTestCase { /// ⌘+ in Reading Mode with the editor open: the list keeps showing the /// blocks of the old zoom, with that zoom's column cap, until the zoom's - /// parse lands. The editor took the REQUESTED zoom (`fontScale`), so for - /// that while its column was 1440 pt over a 720 pt list column, and - /// leaving Source Edit then moved the text sideways. It follows the - /// zoom the document is shown at now. + /// parse lands. The editor's COLUMN follows the zoom the document is + /// shown at (1.14.4 took the requested one: a 1440 pt column over a + /// 720 pt list column, and leaving Source Edit then moved the text + /// sideways); its FONT follows the requested zoom at once + /// (`testTheEditorFontFollowsTheRequestedZoomAtOnce`). func testTheEditorColumnFollowsTheZoomTheDocumentIsShownAt() { let requested = 2.0, shown = 1.0 let (controller, scrollView, _) = makeEditor( "text", width: 1800, - style: .overDocument(theme: light, shownFontScale: shown, isReadingMode: true)) + style: .overDocument(theme: light, fontScale: requested, shownFontScale: shown, isReadingMode: true)) let inner = floor(scrollView.contentView.bounds.width) - 64 func listColumn(_ scale: Double) -> CGFloat { BlockLayout.Document.columnWidth(inner: inner, @@ -631,10 +632,42 @@ final class SourceEditorControllerTests: XCTestCase { XCTAssertEqual(listColumn(requested), 1440) // the column the editor used to take XCTAssertEqual(controller.textView.textContainer!.size.width, listColumn(shown)) // The zoom's parse lands: the list and the editor move together. - controller.apply(style: .overDocument(theme: light, shownFontScale: requested, isReadingMode: true)) + controller.apply(style: .overDocument(theme: light, fontScale: requested, shownFontScale: requested, + isReadingMode: true)) XCTAssertEqual(controller.textView.textContainer!.size.width, listColumn(requested)) } + /// The editor's text changes size the moment ⌘+ is pressed, as before + /// 636de0e — not when the zoom's re-parse lands (about 2 s on an + /// 11.7K-line document, and only once at the end of ⌘+⌘+⌘+, whose earlier + /// parses are superseded). Only the Reading Mode column waits for it. + func testTheEditorFontFollowsTheRequestedZoomAtOnce() { + let (controller, scrollView, _) = makeEditor( + "text", width: 1800, + style: .overDocument(theme: light, fontScale: 1, shownFontScale: 1, isReadingMode: true)) + let base = BlockLayout.Code.codeFontSize + XCTAssertEqual(controller.textView.font?.pointSize, base) + let inner = floor(scrollView.contentView.bounds.width) - 64 + for requested in [1.25, 1.5, 2.0] { + // ⌘+ pressed, the parse still out: new font, the list's column. + controller.apply(style: .overDocument(theme: light, fontScale: requested, shownFontScale: 1, + isReadingMode: true)) + XCTAssertEqual(controller.textView.font?.pointSize, base * CGFloat(requested), "× \(requested)") + XCTAssertEqual(controller.textView.textContainer!.size.width, 720, "× \(requested)") + } + // The last parse lands: the column follows; the font stays. + controller.apply(style: .overDocument(theme: light, fontScale: 2, shownFontScale: 2, isReadingMode: true)) + XCTAssertEqual(controller.textView.font?.pointSize, base * 2) + XCTAssertEqual(controller.textView.textContainer!.size.width, + BlockLayout.Document.columnWidth(inner: inner, + cap: BlockLayout.Document.readingColumnCap(fontScale: 2))) + // Outside Reading Mode there is no cap to wait for. + let standard = SourceEditorController.Style.overDocument(theme: light, fontScale: 1.5, shownFontScale: 1, + isReadingMode: false) + XCTAssertEqual(standard.fontScale, 1.5) + XCTAssertEqual(standard.columnFontScale, 1) + } + func testTextContainerFollowsTheViewWidthAndTheLayoutStyle() { let (controller, scrollView, window) = makeEditor("text", width: 1000) let view = controller.textView From fef7f52f82be9bc99c48c8e1cbe8f32567b4268f Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:06:40 +0100 Subject: [PATCH 071/105] fix(links): a link's text is inline Markdown, as on GitHub Review R13: link text was drawn as written unless it held an image or a backslash, so [**Docs**](u), [*plain*](v), [`npm install`](u) and [~~old~~](u) showed their asterisks, backticks and tildes; GitHub renders <a><strong>Docs</strong></a> and so on. makeLink now always inline-parses the text with links disabled (bold, italic, code, strikethrough, references, escapes, image placeholders) and the outer link covers the whole run; ReferenceLinks rewrites every link's text by the same rule (parsesLinkText is gone). Search, copy, the measurer, selection and print all use the renderer's string, so they follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownRenderer.swift | 36 ++++++++---------------- QuickMD/QuickMD/ReferenceLinks.swift | 18 +++++------- QuickMD/QuickMDTests/RendererTests.swift | 30 +++++++++++++++++++- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index abf5d01..8c1603a 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -981,32 +981,18 @@ struct MarkdownRenderer: Sendable { return nil } - /// Whether a link's text is inline-parsed (with links disabled) rather - /// than drawn as written: when it holds an image (`[![b](img)](link)`) - /// or a backslash escape (`[cost \$5 and \$6](u)`, `[a \* b](u)` — GitHub - /// shows "cost $5 and $6", "a * b"). `ReferenceLinks` follows the same - /// rule when it rewrites a link's text. - static func parsesLinkText(_ text: String) -> Bool { - text.contains("![") || text.contains("\\") - } - private func makeLink(text: String, url: String, remaining: Substring) -> (AttributedString, Substring) { - var attr: AttributedString - if Self.parsesLinkText(text) { - // Link-wrapped image (`[![b](img)](link)`): the text is rendered - // inline so the image becomes its "[Image: b]" placeholder, and the - // whole run carries the OUTER link (set below, over the image's - // own); an escape loses its backslash (and an escaped `$` is never - // math). Every other link keeps its exact plain-text rendering. - // Rendered with links disabled: a link inside a link is literal - // text (bounds the recursion). - var linkText = self - linkText.linksDisabled = true - attr = linkText.renderInlineFormatting(text) - } else { - attr = AttributedString(decodedText(text)) - attr.setDualFont(size: scaled(14), fonts: theme.fonts) - } + // A link's text is inline Markdown, as on GitHub: `[**Docs**](u)` is + // bold, ``[`npm install`](u)`` code, `[~~old~~](u)` struck through, an + // escape loses its backslash (an escaped `$` is never math), an image + // becomes its "[Image: b]" placeholder (`[![b](img)](link)`). The + // whole run then carries the OUTER link (set below, over any inner + // one, an autolink's or an image's). Rendered with links disabled: a + // link inside a link is literal text (bounds the recursion). + // `ReferenceLinks` rewrites a link's text by the same rule. + var linkText = self + linkText.linksDisabled = true + var attr = linkText.renderInlineFormatting(text) attr.setDualForeground(theme.linkColor) attr.setDualUnderline() diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index d116b04..2a603e8 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -146,26 +146,22 @@ enum ReferenceLinks { let afterBracket = remaining.index(after: closeBracket) let linkText = String(remaining[remaining.index(after: remaining.startIndex)..<closeBracket]) - // Inline link: its destination is copied as written; its text only - // when it holds no image — the renderer parses a link's text (with - // links disabled) only then, so `[![build][badge]](https://ci…)`, the - // usual README badge, needs its image reference inlined too. + // Inline link: its destination is copied as written; its text is + // rewritten (links disabled) because the renderer parses a link's text + // as inline Markdown — `[![build][badge]](https://ci…)`, the usual + // README badge, needs its image reference inlined. if remaining[afterBracket...].hasPrefix("(") { guard let inline = InlineSyntaxScanner.parenthesizedDestination(remaining[afterBracket...], pairs: pairs) else { return nil } - guard MarkdownRenderer.parsesLinkText(String(linkText)) else { - return (String(remaining[..<inline.end]), remaining[inline.end...]) - } let text = inlinedLine(linkText, sources: sources, linksDisabled: true) return ("[\(text)]" + remaining[afterBracket..<inline.end], remaining[inline.end...]) } func resolved(_ url: String, rest: Substring) -> (String, Substring) { let destination = inlineDestination(url) - // The renderer parses a link's text only when it holds an image - // (and then with links disabled) — rewrite it under the same rule. - let text = MarkdownRenderer.parsesLinkText(String(linkText)) - ? inlinedLine(linkText, sources: sources, linksDisabled: true) : linkText + // The renderer parses a link's text (links disabled): rewrite it + // under the same rule. + let text = inlinedLine(linkText, sources: sources, linksDisabled: true) return ("[\(text)](\(destination))", rest) } diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 55e2306..8dfefc3 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -861,6 +861,34 @@ final class RendererTests: XCTestCase { XCTAssertEqual(InlineMathSegmenter.pieces(of: attributed).map(\.length), [2, 5], "spans in the rendered text: $50%$") } + // MARK: - Formatting inside link text (review R13) + + /// A link's text is inline Markdown, as on GitHub (`gh api markdown`): + /// `<a><strong>Docs</strong></a>`, `<a><em>plain</em></a>`, + /// `<a><code>npm install</code></a>`, `<a><del>old</del></a>`, + /// `<a>a <strong>b</strong> c</a>`, `<a href=u>https://x.com</a>`, + /// `<a>a & b</a>` — and the whole visible text carries the link. + func testLinkTextIsInlineMarkdown() { + let line = "[**Docs**](u) [*plain*](v) [`npm install`](u) [~~old~~](u) [a **b** c](u) [https://x.com](u) [a & b](u)" + let attributed = renderer.renderInline(line) + XCTAssertEqual(String(attributed.characters), "Docs plain npm install old a b c https://x.com a & b") + func run(_ text: String) -> AttributedString.Runs.Run? { + attributed.runs.first { String(attributed[$0.range].characters) == text } + } + func font(_ text: String) -> NSFont? { run(text)?[AttributeScopes.AppKitAttributes.FontAttribute.self] } + XCTAssertTrue(font("Docs")?.fontDescriptor.symbolicTraits.contains(.bold) ?? false) + XCTAssertTrue(font("plain")?.fontDescriptor.symbolicTraits.contains(.italic) ?? false) + XCTAssertTrue(font("npm install")?.fontDescriptor.symbolicTraits.contains(.monoSpace) ?? false) + XCTAssertNotNil(run("old")?[AttributeScopes.AppKitAttributes.StrikethroughStyleAttribute.self]) + XCTAssertTrue(font("b")?.fontDescriptor.symbolicTraits.contains(.bold) ?? false) + // Every visible character outside the spaces between links is linked; + // the autolink-looking text links to the OUTER destination. + for linked in ["Docs", "plain", "npm install", "old", "a ", "b", " c", "a & b"] { + XCTAssertNotNil(run(linked)?.link, linked) + } + XCTAssertEqual(run("https://x.com")?.link?.absoluteString, "u") + } + // MARK: - Escapes in link text (review R12) /// A link's text with a backslash escape is parsed like the image case: @@ -871,7 +899,7 @@ final class RendererTests: XCTestCase { XCTAssertEqual(cost.text, "cost $5 and $6") XCTAssertEqual(cost.formulas, []) let star = renderer.renderInline("[a \\* b](u) and [*plain*](v)") - XCTAssertEqual(String(star.characters), "a * b and *plain*") + XCTAssertEqual(String(star.characters), "a * b and plain", "R13: link text is inline Markdown") XCTAssertEqual(Set(star.runs.compactMap { $0.link?.absoluteString }), ["u", "v"]) XCTAssertTrue(star.runs.filter { String(star[$0.range].characters).contains("*") }.allSatisfy { $0.link != nil }, "the whole link text stays linked") From e2fd34d3b1aee9c06c39c0c60829443d07e233b3 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:08:48 +0100 Subject: [PATCH 072/105] fix(parser): a lazy quote line is paragraph text, never structure a32f42d passed lazily absorbed lines through QuoteTip.take, which still read structure into them: a lazy `|---|` or `===` closed the quote's paragraph as a table separator or a Setext underline, and the next lazy line fell out of the quote. CommonMark never reads a lazy line as structure; GitHub keeps "> a\n| x | y |\n|---|---|\n| 1 | 2 |" and "> a\nb\n===\nc" in one quoted paragraph (gh api markdown). QuoteTip.takeLazy keeps the paragraph open; take is for the group's own marked lines only. A marked table followed by an unmarked row still ends the quote, as on GitHub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 14 ++++++++++++-- QuickMD/QuickMDTests/VerticalRhythmTests.swift | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 7b93f8e..f9fda93 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -395,7 +395,7 @@ struct MarkdownBlockParser: Sendable { references: referenceDefinitions) else { break } groupLines.append(lines[i]) - tip.take(lines[i]) + tip.takeLazy() i += 1 continue } @@ -421,7 +421,7 @@ struct MarkdownBlockParser: Sendable { references: referenceDefinitions) else { break } groupLines.append(content) - tip.take(content) + tip.takeLazy() i += 1 continue } @@ -1004,6 +1004,16 @@ struct MarkdownBlockParser: Sendable { private var inTable = false private var previousHadPipe = false + /// A lazily continued line (no marker, or fewer markers than the + /// group): paragraph text whatever it looks like — CommonMark never + /// reads a lazy line as a table separator, a Setext underline or any + /// other structure, so the paragraph stays open for the next one. + mutating func takeLazy() { + endsInParagraph = true + previousHadPipe = false + } + + /// A line of the group itself (marked at the group's level). mutating func take(_ content: String) { let marker = MarkdownBlockParser.markerLine(content) if let open = fence { diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index df6aa2b..531b0e2 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -714,6 +714,23 @@ final class VerticalRhythmTests: XCTestCase { ["q1:| x | y |\n|---|---|\n| 1 | 2 |", "t:lazy"]) } + /// A lazy line is paragraph text, never structure: a lazy `|---|` or + /// `===` read as a table separator or a Setext underline closed the + /// quote's paragraph, and the NEXT lazy line fell out of the quote. + /// GitHub keeps every one of them in the paragraph (gfm/g5_*.html). + func testALazyLineIsNeverReadAsStructure() throws { + XCTAssertEqual(try quoteShape("> a\n| x | y |\n|---|---|\n| 1 | 2 |\n"), + ["q1:a\n| x | y |\n|---|---|\n| 1 | 2 |"]) + XCTAssertEqual(try quoteShape("> a\nb\n===\nc\n"), ["q1:a\nb\n===\nc"]) + XCTAssertEqual(try quoteShape("> > a\n> ===\n> c\n"), ["q2:a\n===\nc"]) + XCTAssertEqual(try quoteShape("> > a\n> | x |\n> |---|\n> c\n"), ["q2:a\n| x |\n|---|\nc"]) + // Marked lines still are structure: a marked table, then an + // unmarked row, ends the quote (GitHub too). + XCTAssertEqual(try quoteShape("> | x | y |\n> |---|---|\n> | 1 | 2 |\nlazy\n"), + ["q1:| x | y |\n|---|---|\n| 1 | 2 |", "t:lazy"]) + XCTAssertEqual(try quoteShape("> a\n> ===\nb\n"), ["q1:a\n===", "t:b"]) + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From 9a288872734c4e58f2a42413faa1804858af47c5 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:10:13 +0100 Subject: [PATCH 073/105] fix(headings): a formula in a heading shows its source with its LaTeX escapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review R14: headings typeset no inline math and show a formula's source, and since a633405 the Markdown escape pass removed its backslashes: "# H \% $a\,b$" showed "H % $a,b$". renderHeader now rebuilds every $…$ segment from InlineMathSegmenter.pieces(of:) ("$" + LaTeX + "$", backslashes back by offset); text outside formulas is unchanged. The view, measurer, title cache, ids, Contents sidebar, search, copy and print all read renderHeader, so they show the same text. Math rendering in headings stays a 1.15 candidate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownRenderer.swift | 37 +++++++++++++++++++++++- QuickMD/QuickMDTests/RendererTests.swift | 20 +++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 8c1603a..a8cee0c 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -466,12 +466,47 @@ struct MarkdownRenderer: Sendable { // MARK: - Block Renderers func renderHeader(_ text: String, level: Int) -> AttributedString { - var attr = renderInlineFormatting(text) + var attr = Self.formulaSourceWithEscapes(renderInlineFormatting(text)) attr.setDualFont(size: headerFontSize(level: level), bold: true, fonts: theme.fonts) attr.setDualForeground(theme.textColor) return attr } + /// Headings typeset no inline math (the title view converts with + /// `hasInlineMath: false`), so a `$…$` shows its source — and that source + /// must keep its LaTeX escapes, which the Markdown escape pass removed: + /// `# H \% $a\,b$` shows "H % $a\,b$" (text outside the formula loses + /// its backslash as everywhere). The same split as math paragraphs + /// (`InlineMathSegmenter.pieces(of:)`); every heading consumer — view, + /// measurer, title cache, ids, Contents, search, copy, print — reads + /// `renderHeader`, so they all see this text. + private static func formulaSourceWithEscapes(_ attributed: AttributedString) -> AttributedString { + guard attributed.characters.contains("$") else { return attributed } + let pieces = InlineMathSegmenter.pieces(of: attributed) + guard pieces.contains(where: { if case .math = $0.segment { return true } else { return false } }) else { + return attributed + } + var result = AttributedString() + var index = attributed.startIndex + for piece in pieces { + guard let end = attributed.characters.index(index, offsetBy: piece.length, + limitedBy: attributed.endIndex) else { break } + switch piece.segment { + case .text: + result.append(attributed[index..<end]) + case .math(let latex): + // The formula's own attributes (its first run), its source with escapes. + var source = AttributedString("$" + latex + "$") + if let first = attributed[index..<end].runs.first { + source.setAttributes(first.attributes) + } + result.append(source) + } + index = end + } + return result + } + /// Point size of a level-`level` heading at this renderer's scale. func headerFontSize(level: Int) -> CGFloat { let sizes: [CGFloat] = [32, 26, 22, 18, 16, 14] diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 8dfefc3..02e4015 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -910,6 +910,26 @@ final class RendererTests: XCTestCase { XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "a * b and c $5") } + // MARK: - Formula source in headings (review R14) + + /// Headings show a formula's SOURCE (no inline math there yet), and it + /// keeps its LaTeX escapes: `# H \% $a\,b$` shows "H % $a\,b$". The + /// heading view, ids, the Contents sidebar and search all read this text. + func testHeadingFormulaSourceKeepsItsEscapes() { + let title = "H \\% $a\\,b$ and $50\\%$" + let shown = String(renderer.renderHeader(title, level: 1).characters) + XCTAssertEqual(shown, "H % $a\\,b$ and $50\\%$") + XCTAssertEqual(String(renderer.renderHeader("Price \\$5 and \\$6", level: 2).characters), "Price $5 and $6", + "escaped dollars are no formula") + let entry = ToCEntry(id: "heading-0", level: 1, title: title, sourceLine: 0) + XCTAssertEqual(entry.shownTitle(renderer: renderer), shown) + let theme = MarkdownTheme.cached(for: .light) + let blocks = MarkdownBlockParser(theme: theme).parse("# " + title) + XCTAssertEqual(HeadingAnchors.headings(of: blocks, renderer: renderer).map(\.text), [shown]) + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "a\\,b").matchBlockIds.count, 1, + "search finds what the heading shows") + } + // MARK: - References in link destinations (review R10) /// A destination's character references are decoded (hrefs from From 9cd2e27a0edb52492d5257935b041e436ecb030a Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:13:02 +0100 Subject: [PATCH 074/105] fix(parser): a definition inside a fenced code block is the block's text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-pass took link and footnote definitions out of the document wherever they stood, inside fenced code too: "```\n[x]: https://…\n```" rendered an EMPTY code block, and the definition linked `[x]` in the rest of the document. GitHub keeps it as code (gh api markdown), with `~~~`, a longer opening fence and an unclosed fence alike. The pre-pass now follows the document's fenced code and `$$` blocks the way the block loop reads them (VerbatimRegions) and leaves their lines alone. One set of predicates — fenceOpening, closesFence, opensMultilineMath, closesMath — serves the block loop, quote groups and the pre-pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 109 +++++++++++++----- .../QuickMDTests/VerticalRhythmTests.swift | 24 ++++ 2 files changed, 106 insertions(+), 27 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index f9fda93..115f018 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -84,9 +84,17 @@ struct MarkdownBlockParser: Sendable { // Headings carry the original line number so section copy can slice the // raw text without re-detecting headings itself. var lineMap: [Int] = [] + // Fenced code and `$$` blocks, read as the block loop reads them: a + // definition inside one is that block's text (GitHub). + var verbatim = VerbatimRegions() for originalIndex in firstContentLine..<allLines.count { let line = allLines[originalIndex] let trimmed = Self.markerLine(line) + if verbatim.contains(trimmed) { + lines.append(line) + lineMap.append(originalIndex) + continue + } let nsRange = NSRange(trimmed.startIndex..., in: trimmed) // Footnote definitions must be checked BEFORE reference links // because [^id]: text also matches the reference link pattern [id]: url @@ -143,16 +151,9 @@ struct MarkdownBlockParser: Sendable { let trimmedLine = Self.markerLine(line) // Fenced code block — check BEFORE math to protect $$ inside code blocks - if trimmedLine.hasPrefix("```") || trimmedLine.hasPrefix("~~~") { + if let opening = Self.fenceOpening(trimmedLine) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) - - // Store the fence characters (just the backticks/tildes prefix) - guard let fenceChar = trimmedLine.first else { i += 1; continue } - var fenceLength = 0 - for ch in trimmedLine { - if ch == fenceChar { fenceLength += 1 } else { break } - } - let openingFence = String(repeating: fenceChar, count: fenceLength) + let fenceLength = opening.length // Extract language after fence let language = String(trimmedLine.dropFirst(fenceLength)).trimmingCharacters(in: .whitespaces) @@ -161,8 +162,7 @@ struct MarkdownBlockParser: Sendable { // Closing fence must be same char, same or greater length, nothing else while i < lines.count { - let closeLine = Self.markerLine(lines[i]) - if closeLine.hasPrefix(openingFence) && MarkdownRenderer.isBlankLine(closeLine.drop(while: { $0 == fenceChar })) { + if Self.closesFence(Self.markerLine(lines[i]), opening) { i += 1 break } @@ -191,7 +191,7 @@ struct MarkdownBlockParser: Sendable { let afterDollar = String(trimmedLine.dropFirst(2)).trimmingCharacters(in: .whitespaces) // Single-line: $$content$$ (must have content between delimiters) - if afterDollar.hasSuffix("$$") { + if !Self.opensMultilineMath(trimmedLine) { let latex = String(afterDollar.dropLast(2)).trimmingCharacters(in: .whitespaces) if !latex.isEmpty { blocks.append(.mathBlock(index: blockIndex, latex: latex, sourceLine: sourceLine)) @@ -206,8 +206,7 @@ struct MarkdownBlockParser: Sendable { if !afterDollar.isEmpty { mathLines.append(afterDollar) } i += 1 while i < lines.count { - let mLine = Self.markerLine(lines[i]) - if mLine == "$$" { + if Self.closesMath(Self.markerLine(lines[i])) { i += 1 break } @@ -991,6 +990,68 @@ struct MarkdownBlockParser: Sendable { flushRow() } + // MARK: - Fences and `$$` blocks + + /// A code fence's marker character and length. + typealias Fence = (marker: Character, length: Int) + + /// The fence `marker` (a `markerLine`) opens: three or more backticks or + /// tildes. THE opening test — the block loop, quote groups and the + /// definition pre-pass all ask it. + static func fenceOpening(_ marker: String) -> Fence? { + guard let first = marker.first, first == "`" || first == "~" else { return nil } + let length = marker.prefix(while: { $0 == first }).count + return length >= 3 ? (first, length) : nil + } + + /// Whether `marker` closes `fence`: the same character, at least as many, + /// and nothing after them but spaces and tabs. + static func closesFence(_ marker: String, _ fence: Fence) -> Bool { + marker.hasPrefix(String(repeating: fence.marker, count: fence.length)) + && MarkdownRenderer.isBlankLine(marker.drop(while: { $0 == fence.marker })) + } + + /// Whether `marker` opens a `$$` block that runs to a `$$` line — not a + /// one-line `$$…$$` formula. + static func opensMultilineMath(_ marker: String) -> Bool { + marker.hasPrefix("$$") + && !String(marker.dropFirst(2)).trimmingCharacters(in: .whitespaces).hasSuffix("$$") + } + + /// Whether `marker` closes a `$$` block. + static func closesMath(_ marker: String) -> Bool { marker == "$$" } + + /// The fenced code and `$$` blocks of a document, followed line by line + /// the way the block loop reads them (fence first, then `$$`; inside one, + /// only its closing line matters). The definition pre-pass leaves their + /// lines alone: a definition inside one is that block's text. + struct VerbatimRegions { + private var fence: Fence? + private var inMath = false + + /// Whether the line (its `markerLine`) opens, continues or closes + /// such a block. + mutating func contains(_ marker: String) -> Bool { + if let open = fence { + if MarkdownBlockParser.closesFence(marker, open) { fence = nil } + return true + } + if inMath { + if MarkdownBlockParser.closesMath(marker) { inMath = false } + return true + } + if let opening = MarkdownBlockParser.fenceOpening(marker) { + fence = opening + return true + } + if marker.hasPrefix("$$") { + inMath = MarkdownBlockParser.opensMultilineMath(marker) + return true + } + return false + } + } + // MARK: - Quote laziness /// What a quote group's lines end in, as CommonMark reads them: an OPEN @@ -1000,7 +1061,7 @@ struct MarkdownBlockParser: Sendable { /// underline, a rule and a table. struct QuoteTip { private(set) var endsInParagraph = false - private var fence: (marker: Character, length: Int)? + private var fence: Fence? private var inTable = false private var previousHadPipe = false @@ -1018,10 +1079,7 @@ struct MarkdownBlockParser: Sendable { let marker = MarkdownBlockParser.markerLine(content) if let open = fence { // Inside the quote's fence: only its closing line matters. - if marker.hasPrefix(String(repeating: open.marker, count: open.length)), - MarkdownRenderer.isBlankLine(marker.drop(while: { $0 == open.marker })) { - fence = nil - } + if MarkdownBlockParser.closesFence(marker, open) { fence = nil } endsInParagraph = false return } @@ -1031,14 +1089,11 @@ struct MarkdownBlockParser: Sendable { inTable = false return } - if let first = marker.first, first == "`" || first == "~" { - let length = marker.prefix(while: { $0 == first }).count - if length >= 3 { - fence = (first, length) - endsInParagraph = false - inTable = false - return - } + if let opening = MarkdownBlockParser.fenceOpening(marker) { + fence = opening + endsInParagraph = false + inTable = false + return } if inTable, content.contains("|") { endsInParagraph = false diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 531b0e2..8763c41 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -731,6 +731,30 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("> a\n> ===\nb\n"), ["q1:a\n===", "t:b"]) } + // MARK: - Link and footnote definitions + + /// A definition inside a fenced code block is the block's text: the + /// pre-pass took it out wherever it stood, so the code block came out + /// EMPTY and the definition linked `[x]` in the rest of the document. + /// GitHub (gfm/def_fence*.html, def_fn_fence.html) keeps it as code — + /// `~~~`, a longer opening fence and an unclosed one alike. A `$$` block + /// is read the way the block loop reads it. + func testADefinitionInsideAFencedBlockIsCode() throws { + XCTAssertEqual(try quoteShape("```\n[x]: https://example.com\n```\n\n[x]\n"), + ["c:[x]: https://example.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("~~~~\n[x]: https://example.com\n~~~\nstill code\n~~~~\n\n[x]\n"), + ["c:[x]: https://example.com\n~~~\nstill code", "t:[x]"]) + XCTAssertEqual(try quoteShape("```\n[x]: https://example.com\n\n[x]"), // unclosed: to the end + ["c:[x]: https://example.com\n\n[x]"]) + XCTAssertEqual(try quoteShape("Text[^1]\n\n```\n[^1]: in code\n```\n"), + ["t:Text[^1]", "c:[^1]: in code"]) + // A fence inside a `$$` block is the formula's; the definition after + // the block is one. + XCTAssertEqual(try quoteShape("$$\n```\n$$\n\n[x]: http://e.com\n\n[x]\n"), ["other", "t:x"]) + // Outside the fence it still is one. + XCTAssertEqual(try quoteShape("```\ncode\n```\n\n[x]: http://e.com\n\n[x]\n"), ["c:code", "t:x"]) + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From 24a23d9d788b9293a96552cc93766489b79123f7 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:14:50 +0100 Subject: [PATCH 075/105] fix(parser): a link definition cannot interrupt a paragraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-pass took a link definition out wherever it stood. CommonMark (and GitHub, gh api markdown) do not let one interrupt a paragraph: on a line that would continue a paragraph — plain text, a list item's, a table's rows, a quote's lazy line — it is that text, and `[x]` stays literal. "> a\n[x]: http://e.com" is one quoted paragraph on GitHub. DefinitionPlacement follows, line by line, whether a paragraph would go on (a quote's through QuoteTip, its lazy lines included); a link definition is taken out only where none would. Footnote definitions may interrupt a paragraph on GitHub and are taken as before (outside fenced code). The set of definitions only shrinks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 92 +++++++++++++++++-- .../QuickMDTests/VerticalRhythmTests.swift | 24 +++++ 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 115f018..5cd127a 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -87,12 +87,15 @@ struct MarkdownBlockParser: Sendable { // Fenced code and `$$` blocks, read as the block loop reads them: a // definition inside one is that block's text (GitHub). var verbatim = VerbatimRegions() + // A link definition cannot interrupt a paragraph (`DefinitionPlacement`). + var placement = DefinitionPlacement() for originalIndex in firstContentLine..<allLines.count { let line = allLines[originalIndex] let trimmed = Self.markerLine(line) if verbatim.contains(trimmed) { lines.append(line) lineMap.append(originalIndex) + placement.verbatim() continue } let nsRange = NSRange(trimmed.startIndex..., in: trimmed) @@ -105,13 +108,17 @@ struct MarkdownBlockParser: Sendable { let fnContent = String(trimmed[contentRange]) footnoteDefinitions.append((id: fnId, content: fnContent)) if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } - } else if let match = Self.refLinkDefRegex.firstMatch(in: trimmed, range: nsRange), + placement.definition() + } else if !placement.paragraphOpen, + let match = Self.refLinkDefRegex.firstMatch(in: trimmed, range: nsRange), let idRange = Range(match.range(at: 1), in: trimmed), let urlRange = Range(match.range(at: 2), in: trimmed) { referenceDefinitions[String(trimmed[idRange]).lowercased()] = String(trimmed[urlRange]) + placement.definition() } else { lines.append(line) lineMap.append(originalIndex) + placement.kept(line) } } @@ -1132,18 +1139,89 @@ struct MarkdownBlockParser: Sendable { /// and `$$` stay the picture and the formula they are everywhere else. private func continuesQuoteLazily(_ line: String, htmlImageLine: Bool, references: [String: String]) -> Bool { - guard !Self.isBlank(line), !htmlImageLine else { return false } - let marker = Self.markerLine(line) - if marker.hasPrefix("```") || marker.hasPrefix("~~~") || marker.hasPrefix("$$") { return false } + guard !htmlImageLine, Self.startsNoBlock(line) else { return false } + if InlineSyntaxScanner.standaloneImages(in: line[...], references: references) != nil { return false } + return true + } + + /// CommonMark's part of `continuesQuoteLazily`: not blank, and no block + /// starts on the line (fence, `$$`, ATX heading, rule, list item, HTML + /// block) — checked against the document, as cmark does. + static func startsNoBlock(_ line: String) -> Bool { + guard !isBlank(line) else { return false } + let marker = markerLine(line) + if fenceOpening(marker) != nil || marker.hasPrefix("$$") { return false } let nsRange = NSRange(line.startIndex..., in: line) - if Self.headerRegex.firstMatch(in: line, range: nsRange) != nil { return false } + if headerRegex.firstMatch(in: line, range: nsRange) != nil { return false } if MarkdownRenderer.isThematicBreak(line) || MarkdownRenderer.isListItem(line) { return false } if ["-", "*", "+"].contains(marker) { return false } // an empty item - if Self.startsHTMLBlock(marker) { return false } - if InlineSyntaxScanner.standaloneImages(in: line[...], references: references) != nil { return false } + if startsHTMLBlock(marker) { return false } return true } + /// Whether the definition pre-pass may take a LINK definition out on the + /// next line: not where it would continue a paragraph. CommonMark: a + /// link definition cannot interrupt a paragraph; GitHub keeps such a + /// line as the paragraph's text — a list item's, a table's (its rows run + /// on) and a quote's lazy line included. (A footnote definition may + /// interrupt one, GitHub: the pre-pass takes those wherever they stand + /// outside fenced code.) + struct DefinitionPlacement { + private(set) var paragraphOpen = false + /// The quote being read: what its lines end in (`QuoteTip`). + private var quote: QuoteTip? + + /// A line of a fenced code or `$$` block (`VerbatimRegions`). + mutating func verbatim() { + paragraphOpen = false + quote = nil + } + + /// A definition the pre-pass took out: it ends what came before. + mutating func definition() { + paragraphOpen = false + quote = nil + } + + /// Any other line, kept for the block loop. + mutating func kept(_ line: String) { + if MarkdownRenderer.isBlankLine(line) { + paragraphOpen = false + quote = nil + return + } + let marker = MarkdownBlockParser.markerLine(line) + if marker.hasPrefix(">") { + var content = marker[...] + while content.hasPrefix(">") { + content = content.dropFirst() + if content.hasPrefix(" ") { content = content.dropFirst() } + } + var tip = quote ?? QuoteTip() + tip.take(String(content)) + quote = tip + paragraphOpen = tip.endsInParagraph + return + } + if var tip = quote, tip.endsInParagraph, MarkdownBlockParser.startsNoBlock(line) { + tip.takeLazy() + quote = tip + paragraphOpen = true + return + } + quote = nil + let nsRange = NSRange(line.startIndex..., in: line) + if MarkdownBlockParser.headerRegex.firstMatch(in: line, range: nsRange) != nil + || MarkdownRenderer.isThematicBreak(line) + || (paragraphOpen && marker.range(of: MarkdownTheme.setextH1Pattern, + options: .regularExpression) != nil) { + paragraphOpen = false + return + } + paragraphOpen = true + } + } + /// CommonMark's HTML block starts (kinds 1-7), as far as a lazy line needs /// them: `<!…`/`<?…`, a block-level or raw tag (`<div`, `</p>`, `<pre`), /// or a line that is one complete tag. `<span>text` is inline HTML. diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 8763c41..e79136c 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -755,6 +755,30 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("```\ncode\n```\n\n[x]: http://e.com\n\n[x]\n"), ["c:code", "t:x"]) } + /// A link definition cannot interrupt a paragraph (CommonMark): on a + /// line that would continue one — plain text, a list item's, a quote's + /// lazy line, a table's — it is that text, and `[x]` stays literal. + /// The pre-pass took it out anywhere. A footnote definition MAY + /// interrupt a paragraph (GitHub). Every shape: gfm/def_*.html, fn_*.html. + func testALinkDefinitionCannotInterruptAParagraph() throws { + XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("- a\n[x]: http://e.com\n\n[x]\n"), ["t:• a [x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("| a |\n|---|\n| 1 |\n[x]: http://e.com\n\n[x]\n").last, "t:[x]") + // Where no paragraph goes on, it is a definition. + XCTAssertEqual(try quoteShape("Para\n\n[x]: http://e.com\n\n[x]\n"), ["t:Para", "t:x"]) + XCTAssertEqual(try quoteShape("# H\n[x]: http://e.com\n\n[x]\n"), ["h:H", "t:x"]) + XCTAssertEqual(try quoteShape("---\n[x]: http://e.com\n\n[x]\n").last, "t:x") + XCTAssertEqual(try quoteShape("Title\n===\n[x]: http://e.com\n\n[x]\n"), ["h:Title", "t:x"]) + XCTAssertEqual(try quoteShape("```\ncode\n```\n[x]: http://e.com\n\n[x]\n"), ["c:code", "t:x"]) + XCTAssertEqual(try quoteShape("[x]: http://e.com\n[y]: http://f.com\n\n[x] [y]\n"), ["t:x y"]) + XCTAssertEqual(try quoteShape("> a\n>\n[x]: http://e.com\n\n[x]\n"), ["q1:a", "t:x"]) + XCTAssertEqual(try quoteShape("> ```\n> code\n[x]: http://e.com\n\n[x]\n"), ["q1:```\ncode", "t:x"]) + // Footnote definitions interrupt a paragraph, as before. + XCTAssertEqual(try quoteShape("Text[^1]\n\nPara\n[^1]: note\n").prefix(2), ["t:Text¹", "t:Para"]) + XCTAssertEqual(try quoteShape("Text[^1]\n\n> a\n[^1]: note\n").prefix(2), ["t:Text¹", "q1:a"]) + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From 82c34c71afa68536a555817c72f4c5b56c38d2db Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:17:42 +0100 Subject: [PATCH 076/105] fix(parser): an indented fence drops its indentation from its lines CommonMark: a code fence indented N columns drops up to N columns of indentation from each of its lines. The parser kept them, so every fenced block of a numbered-steps list ("1. Step\n\n ```bash\n cmd") was drawn, searched, copied and printed shifted right (" cmd"); GitHub shows "cmd" (gh api markdown). The block's content is the stripped text now, so the view, search, copy and print all read it; the Source Edit tint goes by lines and is unchanged. Source Edit's selection carry-over matched the rendered selection in the source literally; a multi-line selection in such a block reads "a\nb" where the source has "a\n b". It now falls back to a match that skips the spaces and tabs at the start of each source line after the first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 45 ++++++++++++++++++- QuickMD/QuickMD/SourceEditSupport.swift | 16 ++++++- .../QuickMDTests/VerticalRhythmTests.swift | 34 ++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 5cd127a..75e0e0d 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -161,6 +161,10 @@ struct MarkdownBlockParser: Sendable { if let opening = Self.fenceOpening(trimmedLine) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) let fenceLength = opening.length + // CommonMark: a fence indented N columns drops up to N columns + // of indentation from each of its lines (a fence under a list + // item is not drawn shifted right). + let fenceIndent = Self.indentColumns(line) // Extract language after fence let language = String(trimmedLine.dropFirst(fenceLength)).trimmingCharacters(in: .whitespaces) @@ -173,7 +177,7 @@ struct MarkdownBlockParser: Sendable { i += 1 break } - codeLines.append(lines[i]) + codeLines.append(Self.removingIndentation(lines[i], upTo: fenceIndent)) i += 1 } @@ -999,6 +1003,45 @@ struct MarkdownBlockParser: Sendable { // MARK: - Fences and `$$` blocks + /// Columns of the spaces and tabs a line starts with (a tab runs to the + /// next multiple of 4, CommonMark). + static func indentColumns(_ line: String) -> Int { + var columns = 0 + for character in line { + if character == " " { + columns += 1 + } else if character == "\t" { + columns += 4 - columns % 4 + } else { + break + } + } + return columns + } + + /// `line` without up to `columns` columns of its leading spaces and tabs + /// (a tab that would reach past them stays). + static func removingIndentation(_ line: String, upTo columns: Int) -> String { + guard columns > 0 else { return line } + var used = 0 + var index = line.startIndex + while index < line.endIndex { + let character = line[index] + let width: Int + if character == " " { + width = 1 + } else if character == "\t" { + width = 4 - used % 4 + } else { + break + } + guard used + width <= columns else { break } + used += width + index = line.index(after: index) + } + return String(line[index...]) + } + /// A code fence's marker character and length. typealias Fence = (marker: Character, length: Int) diff --git a/QuickMD/QuickMD/SourceEditSupport.swift b/QuickMD/QuickMD/SourceEditSupport.swift index 5c1bf0c..5389084 100644 --- a/QuickMD/QuickMD/SourceEditSupport.swift +++ b/QuickMD/QuickMD/SourceEditSupport.swift @@ -192,7 +192,21 @@ enum SourceEditSupport { if wantBefore.isEmpty && wantAfter.isEmpty { break } searchFrom = found.location + 1 } - return best + return best ?? indentedMatch(of: needle, in: text, scope: scope) + } + + /// A multi-line selection that is not in the source literally, found + /// with the spaces and tabs at the start of each source line after the + /// first skipped: the renderer drops a fence's indentation from its + /// lines (CommonMark), so a selection inside a fenced block under a list + /// item reads "a\nb" where the source has "a\n b". The first match. + private static func indentedMatch(of needle: String, in text: NSString, scope: NSRange) -> NSRange? { + guard needle.contains("\n") else { return nil } + let pattern = needle.components(separatedBy: "\n") + .map { NSRegularExpression.escapedPattern(for: $0) } + .joined(separator: "\n[ \t]*") + guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } + return regex.firstMatch(in: text as String, range: scope)?.range } static func sourceRange(ofSelection selectedText: String, before: String, after: String, diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index e79136c..c65e0df 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -779,6 +779,40 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("Text[^1]\n\n> a\n[^1]: note\n").prefix(2), ["t:Text¹", "q1:a"]) } + // MARK: - Indented fences + + /// A fence indented N spaces drops up to N spaces from each of its lines + /// (CommonMark; GitHub, gfm/b1_*.html). It kept them: every code block + /// of a numbered-steps list was drawn, searched and copied shifted right. + func testAnIndentedFenceDropsItsIndentationFromItsLines() throws { + XCTAssertEqual(try quoteShape("1. Step\n\n ```bash\n cmd\n nested\n ```\n"), + ["t:1. Step", "c:cmd\n nested"]) + XCTAssertEqual(try quoteShape(" ```\n code\n code2\nflush\n ```\n"), ["c:code\n code2\nflush"]) + XCTAssertEqual(try quoteShape("1. Step\n ```\n cmd\n ```\n"), ["t:1. Step", "c:cmd"]) + XCTAssertEqual(try quoteShape("```\n keep\n```\n"), ["c: keep"]) + XCTAssertEqual(try quoteShape(" ~~~\n a\n\n b\n ~~~\n"), ["c:a\n\nb"]) + // Search counts, and copy reads, what is drawn. + let blocks = try XCTUnwrap(parseReturning("1. Step\n\n ```\n let needle = 1\n ```\n")) + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: " let").matchBlockIds, []) + XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "let needle").matchBlockIds, [blocks[1].id]) + } + + /// Source Edit finds a selection made in such a block in the source, + /// whose lines still carry the indentation: a multi-line selection is + /// matched with the spaces and tabs at the start of each source line + /// skipped (a literal match finds it no longer). + func testASelectionInAnIndentedFenceIsFoundInTheSource() { + let source = "1. Step\n\n ```\n cmd\n nested\n ```\n" as NSString + let range = SourceEditSupport.sourceRange(ofSelection: "cmd\n nested", before: "", after: "", + in: source, lines: 3..<5) + XCTAssertEqual(range.map { source.substring(with: $0) }, "cmd\n nested") + // A literal match wins where there is one. + let flat = "```\ncmd\n nested\n```\n" as NSString + let literal = SourceEditSupport.sourceRange(ofSelection: "cmd\n nested", before: "", after: "", + in: flat, lines: 1..<3) + XCTAssertEqual(literal.map { flat.substring(with: $0) }, "cmd\n nested") + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From e29a51cf4acfb9fb9f6273d9ee7c141584f59147 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:19:17 +0100 Subject: [PATCH 077/105] perf(links): plain link text skips the inline pass Review R15: since fef7f52 every link's text ran a full renderInlineFormatting (+25 % parse time on a link-heavy document). renderLinkText takes the parsed path only when the text holds a character an inline parser starts on (\ ` * _ ~ ! [ or an http(s):// autolink); otherwise it builds the one plain run that parse produces (decoded references, the renderer's font). A test asserts the two paths are identical for plain texts at two zoom levels, and the ReferenceLinks equivalence test is unchanged. 5,000 paragraphs with one plain link each, Debug test build, median of 7 parses: 970 ms before, 817 ms after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownRenderer.swift | 41 ++++++++++++++++++++++-- QuickMD/QuickMDTests/RendererTests.swift | 29 +++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index a8cee0c..53450d2 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1025,9 +1025,7 @@ struct MarkdownRenderer: Sendable { // one, an autolink's or an image's). Rendered with links disabled: a // link inside a link is literal text (bounds the recursion). // `ReferenceLinks` rewrites a link's text by the same rule. - var linkText = self - linkText.linksDisabled = true - var attr = linkText.renderInlineFormatting(text) + var attr = renderLinkText(text) attr.setDualForeground(theme.linkColor) attr.setDualUnderline() @@ -1037,6 +1035,43 @@ struct MarkdownRenderer: Sendable { return (attr, remaining) } + /// A link's text before the link colour, underline and URL go on: + /// inline-parsed with links disabled — or, when `parse` is false, the one + /// plain run that parse produces for text with no syntax in it (the + /// renderer's font, the colour is replaced by the caller). `parse: nil` + /// picks the plain run exactly when `linkTextNeedsParsing` is false, which + /// keeps the common `[Install](…)` from a full inline pass (+25 % on a + /// link-heavy document, review R15). Tests compare both paths. + func renderLinkText(_ text: String, parse: Bool? = nil) -> AttributedString { + if parse ?? Self.linkTextNeedsParsing(text) { + var linkText = self + linkText.linksDisabled = true + return linkText.renderInlineFormatting(text) + } + var attr = AttributedString(decodedText(text)) + attr.setDualFont(size: scaled(14), fonts: theme.fonts) + attr.setDualForeground(theme.textColor) + return attr + } + + /// Whether `renderInlineFormatting` could do anything but one plain run: + /// a character some inline parser starts on — `\` escape, `` ` `` code, + /// `*` / `_` emphasis, `~` strikethrough, `!` image, `[` footnote or + /// link, `h` an autolink (`http://`, `https://`). Character references + /// (`&`) are decoded on the plain run too; `$` is only looked at later + /// (inline math, on the rendered text), as on the parsed path. + static func linkTextNeedsParsing(_ text: String) -> Bool { + text.utf8.contains { byte in + switch byte { + case UInt8(ascii: "\\"), UInt8(ascii: "`"), UInt8(ascii: "*"), UInt8(ascii: "_"), + UInt8(ascii: "~"), UInt8(ascii: "!"), UInt8(ascii: "["): + return true + default: + return false + } + } || text.contains("http://") || text.contains("https://") + } + /// The URL of a link destination as written. /// /// `URL(string:)` first, exactly as before 1.14.4: on macOS 14+ it encodes diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index 02e4015..d7827e3 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -910,6 +910,35 @@ final class RendererTests: XCTestCase { XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "a * b and c $5") } + // MARK: - Plain link text fast path (review R15) + + /// Link text with no inline syntax skips the inline pass; the run it gets + /// must be exactly the parsed one (characters and every attribute), at any + /// zoom, and text with syntax must take the parsed path. + func testPlainLinkTextFastPathEqualsTheParsedPath() { + let plain = ["Install", "Read the guide", "Zażółć gęślą", "a & b", "AT&T ©", "50% off $5", + "x < y > z", "e-mail: a@b.c", "1. not a list", "# not a heading", "", " spaced ", "emoji 🚀"] + for scale in [1.0, 1.5] as [CGFloat] { + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .dark), fontScale: scale) + for text in plain { + XCTAssertFalse(MarkdownRenderer.linkTextNeedsParsing(text), text) + XCTAssertEqual(renderer.renderLinkText(text, parse: false), renderer.renderLinkText(text, parse: true), + "\(text) @\(scale)") + let link = renderer.renderInline("[\(text)](https://x.example)") + XCTAssertEqual(link, { + var parsed = renderer.renderLinkText(text, parse: true) + parsed.setDualForeground(renderer.theme.linkColor) + parsed.setDualUnderline() + parsed.link = URL(string: "https://x.example") + return parsed + }(), text) + } + } + for text in ["**b**", "`c`", "a_b", "~~s~~", "![i](p)", "[^1]", "a \\* b", "see https://x.y", "http://a"] { + XCTAssertTrue(MarkdownRenderer.linkTextNeedsParsing(text), text) + } + } + // MARK: - Formula source in headings (review R14) /// Headings show a formula's SOURCE (no inline math there yet), and it From cbd0d7c5172c42b53b8563baf8338a71228ea3db Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:20:37 +0100 Subject: [PATCH 078/105] fix(parser): a Setext underline makes the whole paragraph above it the heading CommonMark (and GitHub, gh api markdown): the heading is the paragraph the underline ends, its lines joined like soft breaks, anchored on the paragraph's first line. Only the last line was the heading here; the lines above it stayed a paragraph ("Para one\nline two\n===" drew "Para one" as text and "line two" as the heading). - The block loop takes the paragraph out of the text buffer (after its last blank line or rule) as the heading's title; its source line is the paragraph's first, so Section copy, the ToC and the anchor (para-one-line-two) see the whole heading. - A list item's paragraph is no heading: "- a\nb\n===" is the item's text, "- a\nb\n---" the list and a rule (setextParagraphStart, by the rule rows are cut by). A quote's lazy lines stay the quote's (F6). - The Source Edit tint finds a Setext heading's underline from its first line on, so it spans every line of the title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 69 ++++++++++++------- QuickMD/QuickMD/SourceTint.swift | 38 ++++++---- .../QuickMDTests/VerticalRhythmTests.swift | 46 +++++++++++++ 3 files changed, 117 insertions(+), 36 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 75e0e0d..4136156 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -294,33 +294,37 @@ struct MarkdownBlockParser: Sendable { continue } - // Setext headers (underlined headers). The title is paragraph - // text: a quote line or a list item never is one (GitHub: `> a` - // or `- a` over `---` is the quote or list, then a rule; over - // `===`, the quote's or item's text goes on). + // Setext headers (underlined headers): the WHOLE paragraph above + // the underline is the heading (CommonMark, GitHub), on the + // paragraph's first line, its lines joined like soft breaks. Not a + // list item's paragraph (`- a` over `===` is the item's text, over + // `---` the list and a rule) nor a quote line's (the quote branch + // takes those, lazy lines included). + // + // NOTE: no `!isTableSeparator(nextLine)` guard — a line of pure + // dashes ALWAYS matches the table-separator pattern (pipes are + // optional in it), which once made setext H2 unreachable. A + // pipe-bearing separator can't match setextH2Pattern anyway. + // (Caught by ParserTests.testSetextHeadings.) let trimmed = trimmedLine - if i + 1 < lines.count && !trimmed.isEmpty && !trimmed.contains("|") - && !trimmed.hasPrefix(">") && !MarkdownRenderer.isListItem(line) { + if i + 1 < lines.count && !trimmed.isEmpty && !trimmed.contains("|") && !trimmed.hasPrefix(">") { let nextLine = Self.markerLine(lines[i + 1]) - + let level: Int? if nextLine.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) - // Setext: the TEXT line owns the heading, not the `===` underline. - blocks.append(.heading(index: blockIndex, level: 1, title: trimmed, sourceLine: sourceLine)) - blockIndex += 1 - i += 2 - continue + level = 1 + } else if nextLine.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { + level = 2 + } else { + level = nil } - - // NOTE: no `!isTableSeparator(nextLine)` guard here — a line of - // pure dashes ALWAYS matches the table-separator pattern (pipes - // are optional in it), which made this branch unreachable and - // silently disabled setext H2 for years. A pipe-bearing table - // separator can't match setextH2Pattern anyway, so the guard - // added nothing. (Caught by ParserTests.testSetextHeadings.) - if nextLine.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { - flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) - blocks.append(.heading(index: blockIndex, level: 2, title: trimmed, sourceLine: sourceLine)) + if let level, let start = Self.setextParagraphStart(in: textBuffer, endingWith: line) { + let title = (textBuffer[start...].map { Self.markerLine($0.line) } + [trimmed]) + .joined(separator: " ") + let headingLine = start < textBuffer.count ? textBuffer[start].originalIndex : sourceLine + var preceding = Array(textBuffer[..<start]) + textBuffer.removeAll() + flushTextBuffer(&preceding, to: &blocks, index: &blockIndex, pending: &pendingRows) + blocks.append(.heading(index: blockIndex, level: level, title: title, sourceLine: headingLine)) blockIndex += 1 i += 2 continue @@ -1001,6 +1005,25 @@ struct MarkdownBlockParser: Sendable { flushRow() } + // MARK: - Setext headings + + /// Where the paragraph that `line` (the line over a Setext underline) + /// ends starts in the text buffer: after its last blank line or rule. + /// Nil when that paragraph is a list item's — an item line starts it or + /// interrupts it, by the rule rows are cut by (`flushTextBuffer`). + static func setextParagraphStart(in buffer: [(line: String, originalIndex: Int)], + endingWith line: String) -> Int? { + var start = buffer.count + while start > 0, !isBlank(buffer[start - 1].line), !MarkdownRenderer.isThematicBreak(buffer[start - 1].line) { + start -= 1 + } + let paragraph = buffer[start...].map(\.line) + [line] + for (offset, text) in paragraph.enumerated() where MarkdownRenderer.isListItem(text) { + if offset == 0 || MarkdownRenderer.canInterruptParagraph(text) { return nil } + } + return start + } + // MARK: - Fences and `$$` blocks /// Columns of the spaces and tabs a line starts with (a tab runs to the diff --git a/QuickMD/QuickMD/SourceTint.swift b/QuickMD/QuickMD/SourceTint.swift index 1b9f2cd..9505e5e 100644 --- a/QuickMD/QuickMD/SourceTint.swift +++ b/QuickMD/QuickMD/SourceTint.swift @@ -94,12 +94,9 @@ enum SourceTint { let boundary = blocks[(index + 1)...].first(where: { $0.sourceLine > first })?.sourceLine ?? lineCount let limit = min(boundary, lineCount) - 1 var last: Int - if case .heading(_, let title, _) = block.content { - last = first - if first + 1 <= limit, isSetext(line: first, title: title, in: text, contentEnd: contentEnd(first), - lineStarts: lineStarts) { - last = first + 1 - } + if case .heading = block.content { + last = setextUnderline(from: first, through: limit, in: text, lineStarts: lineStarts, + contentEnd: contentEnd) ?? first } else { last = limit while last > first, isBlank(last) { last -= 1 } @@ -134,12 +131,27 @@ enum SourceTint { } } - /// Whether the heading on `line` is a Setext one: its line, trimmed the - /// way the parser trims it, IS the parser's title. - private static func isSetext(line: Int, title: String, in text: NSString, contentEnd: Int, - lineStarts: [Int]) -> Bool { - let start = lineStarts[line] - let content = text.substring(with: NSRange(location: start, length: max(0, contentEnd - start))) - return MarkdownBlockParser.markerLine(content) == title + /// The underline of the heading starting on `first` when it is a Setext + /// one — its title may run over several lines (the whole paragraph + /// above the underline, CommonMark) — or nil for an ATX heading, whose + /// one line is all of it. The first underline line before `limit`. + private static func setextUnderline(from first: Int, through limit: Int, in text: NSString, + lineStarts: [Int], contentEnd: (Int) -> Int) -> Int? { + func line(_ index: Int) -> String { + let start = lineStarts[index] + return text.substring(with: NSRange(location: start, length: max(0, contentEnd(index) - start))) + } + let opening = line(first) + guard MarkdownBlockParser.headerRegex.firstMatch(in: opening, range: NSRange(opening.startIndex..., + in: opening)) == nil, + first < limit else { return nil } + for index in (first + 1)...limit { + let marker = MarkdownBlockParser.markerLine(line(index)) + if marker.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil + || marker.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { + return index + } + } + return nil } } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index c65e0df..abdb95a 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -813,6 +813,52 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(literal.map { flat.substring(with: $0) }, "cmd\n nested") } + // MARK: - Setext headings over a paragraph + + /// A Setext underline makes the WHOLE paragraph above it the heading + /// (CommonMark; GitHub, gfm/g4_*.html), its lines joined like soft + /// breaks, on the paragraph's first line. Only the last line was the + /// heading; the lines above stayed a paragraph. A paragraph that is a + /// list item's is no heading (its `===` goes on as text, its `---` is a + /// rule), and a quote's lazy lines are the quote's (F6). + func testASetextUnderlineMakesTheWholeParagraphTheHeading() throws { + XCTAssertEqual(try quoteShape("Para one\nline two\n===\n"), ["h:Para one line two"]) + XCTAssertEqual(try quoteShape("one\ntwo\nthree\n---\n"), ["h:one two three"]) + let markdown = "Intro\n\nPara one\nline two\n---\n\nAfter\n" + let blocks = try XCTUnwrap(parseReturning(markdown)) + XCTAssertEqual(try quoteShape(markdown), ["t:Intro", "h:Para one line two", "t:After"]) + XCTAssertEqual(blocks.map(\.sourceLine), [0, 2, 6]) + if case .heading(let level, _, let line) = blocks[1].content { + XCTAssertEqual(level, 2) + XCTAssertEqual(line, 2) + } + // After blocks that close a paragraph. + XCTAssertEqual(try quoteShape("# H\nline a\nline b\n===\n"), ["h:H", "h:line a line b"]) + XCTAssertEqual(try quoteShape("```\nx\n```\nline a\nline b\n===\n"), ["c:x", "h:line a line b"]) + XCTAssertEqual(try quoteShape("***\nline a\nline b\n===\n").last, "h:line a line b") + // A list item's paragraph is no heading. + XCTAssertEqual(try quoteShape("- a\nb\n===\n"), ["t:• a b ==="]) + XCTAssertEqual(try quoteShape("- a\nb\n---\n").first, "t:• a b") + XCTAssertEqual(try quoteShape("- a\nb\n---\n").count, 2) + XCTAssertEqual(try quoteShape("Para\n- item\nb\n===\n"), ["t:Para", "t:• item b ==="]) + XCTAssertEqual(try quoteShape("1984. A year\nline\n===\n"), ["t:1984. A year line ==="]) + XCTAssertEqual(try quoteShape("> a\nb\n===\n"), ["q1:a\nb\n==="]) + + // The tint spans the paragraph and its underline; Section copy + // starts at its first line; the anchor is the whole title's. + let ns = markdown as NSString + let spans = SourceTint.spans(for: blocks, lineStarts: SourceEditSupport.lineStarts(in: ns), in: ns) + XCTAssertEqual(spans.filter { $0.kind == .heading }.map { ns.substring(with: $0.range) }, + ["Para one\nline two\n---"]) + let entries = blocks.compactMap { block -> ToCEntry? in + guard case .heading(let level, let title, let line) = block.content else { return nil } + return ToCEntry(id: block.id, level: level, title: title, sourceLine: line) + } + XCTAssertEqual(SectionExtractor.extractSection(from: markdown, entry: entries[0], headings: entries), + "Para one\nline two\n---\n\nAfter") + XCTAssertEqual(HeadingAnchors.slug(entries[0].title), "para-one-line-two") + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From 4c0afbfb01528f6493c9896edf58a55558ec66e8 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:25:02 +0100 Subject: [PATCH 079/105] fix(links): code spans outrank brackets Review R16: the bracket pair table and the lookup-less bracket scan paired a ] inside a code span, so [a `](` b](u) was no link (GitHub: a link whose text holds the code "](") and [x `]` y] lost its brackets. Both now skip what MarkdownRenderer.tryParseInlineCode draws as code, with its exact rules (1-2 backtick runs close on the first later run of that size, a 2-run closer followed by a third backtick opens nothing, 3+ runs are literal), remembering a failed closer search per run length so unclosed backticks stay linear. Parens are still paired inside code (a destination is not code). Links, reference rewrites and image lines share the scanner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 51 +++++++++++++++++++++++ QuickMD/QuickMDTests/RendererTests.swift | 32 ++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index 6907ede..3638063 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -58,9 +58,26 @@ enum InlineSyntaxScanner { InlineSyntaxScanner.withBytes(text[...]) { bytes in var brackets: [Int: Int] = [:], parens: [Int: Int] = [:] var openBrackets: [Int] = [], openParens: [Int] = [] + var codeSpans = CodeSpanScanner() var i = 0 while i < bytes.count { if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } + // Code spans outrank brackets: a `]` or `[` inside one pairs + // with nothing. Parens are not skipped — a destination is + // not code — so only brackets look at the span. + if bytes[i] == backtick, let end = codeSpans.end(bytes, at: i) { + var j = i + while j < end { + switch bytes[j] { + case UInt8(ascii: "("): openParens.append(j) + case UInt8(ascii: ")"): if let open = openParens.popLast() { parens[open] = j } + default: break + } + j += 1 + } + i = end + continue + } switch bytes[i] { case UInt8(ascii: "["): openBrackets.append(i) case UInt8(ascii: "]"): if let open = openBrackets.popLast() { brackets[open] = i } @@ -238,6 +255,38 @@ enum InlineSyntaxScanner { } private static let backslash = UInt8(ascii: "\\") + private static let backtick = UInt8(ascii: "`") + + /// Where `MarkdownRenderer.tryParseInlineCode` would end a code span + /// starting at a backtick — so bracket matching skips what the renderer + /// will draw as code (CommonMark: code spans outrank brackets; + /// `[a `](` b](u)` is a link whose text holds the code "]("). + /// Its rules exactly: a run of 1 or 2 backticks closes at the FIRST later + /// occurrence of that many backticks (a 2-run whose closer is followed by + /// a third backtick opens nothing); a run of 3+ is literal and skipped + /// whole. No span → nil (the caller moves on one byte, as the renderer + /// moves on one character). A search that found no closer is remembered + /// per run length, so many unclosed backticks stay linear. + fileprivate struct CodeSpanScanner { + private var noCloserAfter: [Int: Int] = [:] // run length → offset searched from + + mutating func end(_ bytes: Bytes, at start: Int) -> Int? { + var run = 0 + while start + run < bytes.count, bytes[start + run] == backtick { run += 1 } + if run > 2 { return start + run } + if let from = noCloserAfter[run], from <= start + run { return nil } + var i = start + run + while i + run <= bytes.count { + if bytes[i] == backtick, run == 1 || bytes[i + 1] == backtick { + if run == 2, i + 2 < bytes.count, bytes[i + 2] == backtick { return nil } + return i + run + } + i += 1 + } + noCloserAfter[run] = start + run + return nil + } + } private static let ampersand = UInt8(ascii: "&") /// CommonMark whitespace for these purposes: space, tab, LF, VT, FF, CR. @@ -274,8 +323,10 @@ enum InlineSyntaxScanner { if let lookup { return lookup.closingBracket(open).flatMap { $0 < bytes.count ? $0 : nil } } var depth = 0 var i = open + var codeSpans = CodeSpanScanner() while i < bytes.count { if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } + if bytes[i] == backtick, let end = codeSpans.end(bytes, at: i) { i = end; continue } switch bytes[i] { case UInt8(ascii: "["): depth += 1 case UInt8(ascii: "]"): diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index d7827e3..f7cf077 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -910,6 +910,38 @@ final class RendererTests: XCTestCase { XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "a * b and c $5") } + // MARK: - Code spans outrank brackets (review R16) + + /// A `]` or `[` inside a code span pairs with nothing (CommonMark); cases + /// and expected output from `gh api markdown`. (A ```` ``` ```` run stays + /// literal in QuickMD's inline code, so that GitHub case is not here.) + func testCodeSpansOutrankBrackets() { + func check(_ markdown: String, text: String, links: [String], code: [String], + file: StaticString = #filePath, line: UInt = #line) { + let attributed = renderer.renderInline(markdown) + XCTAssertEqual(String(attributed.characters), text, markdown, file: file, line: line) + XCTAssertEqual(Set(attributed.runs.compactMap { $0.link?.absoluteString }), Set(links), markdown, + file: file, line: line) + let monospaced = attributed.runs.filter { + $0[AttributeScopes.AppKitAttributes.FontAttribute.self]?.fontDescriptor.symbolicTraits + .contains(.monoSpace) ?? false + }.map { String(attributed[$0.range].characters) } + XCTAssertEqual(monospaced, code, markdown, file: file, line: line) + } + check("[a `](` b](u)", text: "a ]( b", links: ["u"], code: ["]("]) + check("[x `]` y]", text: "[x ] y]", links: [], code: ["]"]) + check("[a ``]`` b](u)", text: "a ] b", links: ["u"], code: ["]"]) + check("[a ` b](u)", text: "a ` b", links: ["u"], code: []) + check("[a \\` b](u) c`", text: "a ` b c`", links: ["u"], code: []) + check("`[a](u)`", text: "[a](u)", links: [], code: ["[a](u)"]) + + // A standalone image whose alt holds a code span with `]`. + let blocks = MarkdownBlockParser(theme: MarkdownTheme.cached(for: .light)).parse("![alt `]` x](i.png)") + guard case .image(let url, let alt, _)? = blocks.first?.content else { return XCTFail("image") } + XCTAssertEqual(url, "i.png") + XCTAssertEqual(alt, "alt `]` x") + } + // MARK: - Plain link text fast path (review R15) /// Link text with no inline syntax skips the inline pass; the run it gets From 44eaf2333bbf48827005407189c94e9661a592db Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:32:11 +0100 Subject: [PATCH 080/105] perf(quote): a long quote is rendered once per parse, not per re-measure or row update A quote or alert body is rendered from its source (renderQuotedBody); a paragraph's string is stored in its block. Two places rendered it again: - The measurer, on every width change and reading-mode toggle. It now takes the string the previous measurement of the same parse built (measure(reusing:), the heights task's measured.converted, the same contract as measureMathRows) and only lays it out again. - BlockquoteView and AlertBlockView, on the main thread on every update of the row (it comes into view, a search keystroke, Cmd-G), although TextBlockView reads that string only without preconverted. They render it now only when there is no measured string. 5,000-line quote, -O: re-measure 532 -> 156 ms (the same text as a paragraph: 163 ms); view update 368 ms on main -> none. Opening is unchanged and equal to the paragraph's (parse + measure 576 vs 581 ms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 17 ++++++-- QuickMD/QuickMD/MarkdownView.swift | 16 +++---- QuickMD/QuickMD/Views/AlertBlockView.swift | 6 ++- QuickMD/QuickMD/Views/BlockquoteView.swift | 8 +++- .../QuickMDTests/VerticalRhythmTests.swift | 42 +++++++++++++++++++ 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index eb686f9..de4d5ee 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -1201,6 +1201,15 @@ enum BlockHeightMeasurer { /// computed (`BlockLayout.ImageBlock`) instead of starting at the /// loading view. SVG sizes come from `SVGImageDecoder`, diagram /// geometry from `MermaidGeometryStore`. + /// - reusing: strings a PREVIOUS measurement of the same parse built, + /// keyed by block id — the caller's last `MeasuredBlocks.converted`. + /// Quote and alert bodies found here are laid out again as they are: + /// a paragraph's string is stored in its block, a quote's is rendered + /// from its source (`renderQuotedBody`), and that render is the whole + /// cost of a long quote (a 5,000-line one: ~370 ms of ~530). A width + /// change does not change a character, so the render is done once per + /// parse. Pass it ONLY for the same content, theme and zoom (the + /// `measureMathRows` contract); a new parse passes nothing. /// - math: the math engine, or `nil` when the call is NOT on the main /// thread — see the thread rule in the file header. With `nil`, rows /// that need the engine come back at a placeholder height, listed in @@ -1221,6 +1230,7 @@ enum BlockHeightMeasurer { contentWidth: CGFloat, heightSeeds: [String: CGFloat] = [:], imageSizes: [NSString: CGSize] = [:], + reusing: [String: NSAttributedString] = [:], math: MathRendering?) -> MeasuredBlocks { #if DEBUG let signpostID = heightSignpost.makeSignpostID() @@ -1296,8 +1306,9 @@ enum BlockHeightMeasurer { // The quote body is inline-rendered exactly as BlockquoteView // does it, and passed to TextBlockView WITHOUT inline math (the // view does not opt in), so we must not opt in either — which is - // also why a quote never needs the math engine. - let ns = BlockTextConverter.makeNSAttributedString( + // also why a quote never needs the math engine. A string the + // previous measurement built is only laid out again (`reusing`). + let ns = reusing[block.id] ?? BlockTextConverter.makeNSAttributedString( from: renderer.renderQuotedBody(content), hasInlineMath: false, theme: theme, fontScale: fontScale, math: engine) converted[block.id] = ns @@ -1312,7 +1323,7 @@ enum BlockHeightMeasurer { + 2 * BlockLayout.Alert.verticalPadding + 2 * BlockLayout.Document.alertOuterVerticalPadding if !content.isEmpty { - let ns = BlockTextConverter.makeNSAttributedString( + let ns = reusing[block.id] ?? BlockTextConverter.makeNSAttributedString( from: renderer.renderQuotedBody(content), hasInlineMath: false, theme: theme, fontScale: fontScale, math: engine) converted[block.id] = ns diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index eaea016..d0e7958 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -772,17 +772,19 @@ struct MarkdownView: View { let seeds = heightCache.snapshot // Images shown before: their rows are computed at the new width. let imageSizes = heightCache.imageSizesSnapshot - // Inline-math paragraphs: hand the previous pass's strings back so the - // main-actor step re-wraps them instead of re-rendering every `$…$` - // segment through SwiftMath. Safe only because this task never runs - // across a content change — the guard below drops the result if a - // parse landed meanwhile, and everything that alters the characters - // (text, theme, fonts, zoom) goes through a re-parse. + // The previous pass's strings, handed back so they are only re-wrapped: + // quote and alert bodies (off-main, `measure` — not rendered from + // their source again) and inline-math paragraphs (the main-actor + // step — not every `$…$` segment through SwiftMath again). Safe + // only because this task never runs across a content change — the + // guard below drops the result if a parse landed meanwhile, and + // everything that alters the characters (text, theme, fonts, zoom) + // goes through a re-parse. let reusableStrings = measured.converted let raw: MeasuredBlocks = await Task.detached(priority: .userInitiated) { BlockHeightMeasurer.measure(blocks: blocks, theme: currentTheme, fontScale: scale, contentWidth: width, heightSeeds: seeds, - imageSizes: imageSizes, math: nil) + imageSizes: imageSizes, reusing: reusableStrings, math: nil) }.value // A parse that landed while we were measuring owns the state now — // its own transaction published a matching table. diff --git a/QuickMD/QuickMD/Views/AlertBlockView.swift b/QuickMD/QuickMD/Views/AlertBlockView.swift index 723fd78..4e25fc7 100644 --- a/QuickMD/QuickMD/Views/AlertBlockView.swift +++ b/QuickMD/QuickMD/Views/AlertBlockView.swift @@ -93,8 +93,10 @@ struct AlertBlockView: View { /// One attributed string for the whole alert body — see /// `MarkdownRenderer.renderQuotedBody` (shared with the blockquote view and - /// the height measurer). + /// the height measurer) — rendered only when the measurer has not built + /// the string (`BlockquoteView.renderedContent`: `TextBlockView` reads it + /// only without `preconverted`; this body runs on every row update). private func renderedContent() -> AttributedString { - renderer.renderQuotedBody(content) + preconverted == nil ? renderer.renderQuotedBody(content) : AttributedString() } } diff --git a/QuickMD/QuickMD/Views/BlockquoteView.swift b/QuickMD/QuickMD/Views/BlockquoteView.swift index 68a8a31..198eb4b 100644 --- a/QuickMD/QuickMD/Views/BlockquoteView.swift +++ b/QuickMD/QuickMD/Views/BlockquoteView.swift @@ -82,8 +82,12 @@ struct BlockquoteView: View { /// One attributed string for the whole quote body — see /// `MarkdownRenderer.renderQuotedBody` (shared with the alert view and the - /// height measurer). + /// height measurer) — rendered only when the measurer has not built the + /// string: `TextBlockView` reads `attributed` only without `preconverted`, + /// and this body runs on the main thread on every update of the row (it + /// comes into view, a search keystroke, ⌘G). A long quote's render is + /// hundreds of milliseconds; a paragraph's string is stored in its block. private func renderedContent() -> AttributedString { - renderer.renderQuotedBody(content) + preconverted == nil ? renderer.renderQuotedBody(content) : AttributedString() } } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index abdb95a..0718e86 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -859,6 +859,48 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(HeadingAnchors.slug(entries[0].title), "para-one-line-two") } + // MARK: - Long quotes + + /// A width change lays a quote or alert body out again WITHOUT rendering + /// it: the measurer takes the string the previous measurement of the same + /// parse built (`reusing`, the caller's `measured.converted`), as a + /// paragraph's string is stored in its block. It rendered the body again + /// on every width change and reading-mode toggle — a 5,000-line quote + /// cost ~530 ms per re-measure (-O) against ~160 ms for the same text as + /// a paragraph. The heights and the rhythm are a fresh measurement's. + func testAWidthChangeLaysAQuoteOutAgainWithoutRenderingIt() throws { + let long = "words that wrap at a narrow column but not at a wide one, and a few more of them" + let markdown = "Para\n\n> \(long)\nand a lazy line\n\n> > nested \(long)\n\n" + + "> [!NOTE]\n> \(long)\n\n> [!TIP]\n\nEnd\n" + let blocks = try XCTUnwrap(parseReturning(markdown)) + let theme = MarkdownTheme.cached(for: .light) + let first = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: 700, math: nil) + let again = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: 260, reusing: first.converted, math: nil) + let fresh = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: 260, math: nil) + XCTAssertNotEqual(first.table.heights, fresh.table.heights) // the wrap moved + XCTAssertEqual(again.table.heights, fresh.table.heights) + XCTAssertEqual(again.rhythm, fresh.rhythm) + let bodies = blocks.filter { block in + switch block.content { + case .blockquote: return true + case .alert(_, let content): return !content.isEmpty + default: return false + } + } + XCTAssertEqual(bodies.count, 3) + for block in bodies { + let reused = try XCTUnwrap(again.converted[block.id], block.id) + XCTAssertTrue(reused === first.converted[block.id], block.id) + XCTAssertEqual(reused, fresh.converted[block.id], block.id) + } + // Only those: a paragraph's string is built from its block again. + let paragraph = try XCTUnwrap(blocks.first) + XCTAssertFalse(again.converted[paragraph.id] === first.converted[paragraph.id]) + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From cc49795d5361bcd318c7822da0b5b23264a88f63 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:33:10 +0100 Subject: [PATCH 081/105] fix(links): a backtick in a link destination opens no code span for bracket matching Review R16 follow-up: 4c0afbf's bracket scans looked for code spans from the start of the line, but the renderer never starts one inside a destination it consumed, so [a](u`x) and [b `c](v) and [a](<u`>) [b](v) ` lost their second link. PairTable.build now pairs parens in a first pass and, in the bracket pass, jumps over the destination after a "](" (to its paired paren); the lookup-less matchBracket skips an inner link's destination the same way. Both lines and the reviewer's corpus are in the test, with GitHub's result noted where QuickMD's own inline rules differ (emphasis before code, literal 3+ backtick runs). Linear-time tests unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 48 +++++++++++++++-------- QuickMD/QuickMDTests/RendererTests.swift | 38 ++++++++++++++++++ 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index 3638063..d04f03e 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -54,35 +54,44 @@ enum InlineSyntaxScanner { fileprivate func closingBracket(atAbsolute offset: Int) -> Int? { pairs.brackets[offset] } fileprivate func closingParen(atAbsolute offset: Int) -> Int? { pairs.parens[offset] } + /// Two linear passes. Parens first, everywhere (a destination is not + /// code). Then brackets, skipping code spans (code spans outrank + /// brackets) and the destination after a `](` — the renderer consumes + /// it as the link's, so a backtick in a URL (`[a](u`x) [b](v)`) must + /// not open a "span" that swallows a later link's `[`. private static func build(_ text: String) -> (brackets: [Int: Int], parens: [Int: Int]) { InlineSyntaxScanner.withBytes(text[...]) { bytes in var brackets: [Int: Int] = [:], parens: [Int: Int] = [:] var openBrackets: [Int] = [], openParens: [Int] = [] - var codeSpans = CodeSpanScanner() var i = 0 while i < bytes.count { if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } - // Code spans outrank brackets: a `]` or `[` inside one pairs - // with nothing. Parens are not skipped — a destination is - // not code — so only brackets look at the span. + switch bytes[i] { + case UInt8(ascii: "("): openParens.append(i) + case UInt8(ascii: ")"): if let open = openParens.popLast() { parens[open] = i } + default: break + } + i += 1 + } + + var codeSpans = CodeSpanScanner() + i = 0 + while i < bytes.count { + if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } if bytes[i] == backtick, let end = codeSpans.end(bytes, at: i) { - var j = i - while j < end { - switch bytes[j] { - case UInt8(ascii: "("): openParens.append(j) - case UInt8(ascii: ")"): if let open = openParens.popLast() { parens[open] = j } - default: break - } - j += 1 - } i = end continue } switch bytes[i] { case UInt8(ascii: "["): openBrackets.append(i) - case UInt8(ascii: "]"): if let open = openBrackets.popLast() { brackets[open] = i } - case UInt8(ascii: "("): openParens.append(i) - case UInt8(ascii: ")"): if let open = openParens.popLast() { parens[open] = i } + case UInt8(ascii: "]"): + if let open = openBrackets.popLast() { + brackets[open] = i + if i + 1 < bytes.count, bytes[i + 1] == UInt8(ascii: "("), let close = parens[i + 1] { + i = close + 1 + continue + } + } default: break } i += 1 @@ -332,6 +341,13 @@ enum InlineSyntaxScanner { case UInt8(ascii: "]"): depth -= 1 if depth == 0 { return i } + // An inner link's destination is not scanned for code spans + // (see `PairTable.build`). + if i + 1 < bytes.count, bytes[i + 1] == UInt8(ascii: "("), + let close = scanParenDestination(bytes, open: i + 1, nil)?.end { + i = close + continue + } default: break } i += 1 diff --git a/QuickMD/QuickMDTests/RendererTests.swift b/QuickMD/QuickMDTests/RendererTests.swift index f7cf077..7f77870 100644 --- a/QuickMD/QuickMDTests/RendererTests.swift +++ b/QuickMD/QuickMDTests/RendererTests.swift @@ -942,6 +942,44 @@ final class RendererTests: XCTestCase { XCTAssertEqual(alt, "alt `]` x") } + /// The renderer's runs as "<link>text" / "`code`" pieces joined by "|". + private func runs(_ markdown: String) -> String { + let attributed = renderer.renderInline(markdown) + return attributed.runs.map { run in + let text = String(attributed[run.range].characters) + let mono = run[AttributeScopes.AppKitAttributes.FontAttribute.self]?.fontDescriptor.symbolicTraits + .contains(.monoSpace) ?? false + return (run.link.map { "<\($0.absoluteString)>" } ?? "") + (mono ? "`\(text)`" : text) + }.joined(separator: "|") + } + + /// The renderer never starts a code span inside a link destination it + /// consumed, so the bracket scans must not either: a backtick in a URL + /// must not swallow a later link's `[` (4c0afbf did). The reviewer's + /// corpus; GitHub's result (`gh api markdown`) noted where QuickMD's own + /// inline rules differ (emphasis/strikethrough before code — 1.15, R17; + /// 3+ backtick code spans are literal in QuickMD). + func testLinkDestinationsHideBackticksFromTheBracketScan() { + // GitHub: two links "a" and "b `c". + XCTAssertEqual(runs("[a](u`x) and [b `c](v)"), "<u%60x>a| and |<v>b `c") + // GitHub: both links, a trailing backtick. + XCTAssertEqual(runs("[a](<u`>) [b](v) `"), "<u%60>a| |<v>b| `") + // GitHub-equal. + XCTAssertEqual(runs("[a `](` b](u)"), "<u>a |<u>`](`|<u> b") + XCTAssertEqual(runs("![a`b](i.png) [c](u) x`y"), "![a|`b](i.png) [c](u) x`|y") + XCTAssertEqual(runs("[x](u) `` [y]` `` [z](v)"), "<u>x| |`[y]``| |<v>z") + XCTAssertEqual(runs("[a ``b]`` c](u)"), "<u>a |<u>`b]`|<u> c") + XCTAssertEqual(runs("[a \\`b](u) c`"), "<u>a |<u>`|<u>b| c`") + XCTAssertEqual(runs("[^n`] [d](u) `"), "[^n|`] [d](u) `") + XCTAssertEqual(runs("`[` [a](u) `]`"), "`[`| |<u>a| |`]`") + // QuickMD's own rules (GitHub differs, not this fix): no link in + // either, as on GitHub, but emphasis / strikethrough win over code. + XCTAssertEqual(runs("***a `b*** [c` d](u)"), "a `b| [c` d](u)") + XCTAssertEqual(runs("~~x`~~ [y](u) z`"), "x`| [y](u) z`") + // GitHub: "[a " + code "b](u) c" (QuickMD keeps ``` runs literal). + XCTAssertEqual(runs("[a ```b](u) c```"), "<u>a ```b| c```") + } + // MARK: - Plain link text fast path (review R15) /// Link text with no inline syntax skips the inline pass; the run it gets From 9b6891409cab8fb97767cba92ff1f844520424d9 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:43:07 +0100 Subject: [PATCH 082/105] perf(links): the image-line bracket scan pairs parens once, not per inner link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 7: matchBracket (no pair table: every ![ line in the parser) skipped an inner link's destination with a depth scan per "]("; one that never closes scanned to the end of the line each time, so ![x + [a](×N was quadratic (40k: 34.5 s). It now builds the paren map once, on the first inner "](" (the same one-pass pairing PairTable uses, shared as pairParens). 5k → 40k: 0.63 s → 34.5 s before, 0.06 s → 0.47 s after (Debug); the new test asserts the scaling, not an absolute time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 50 +++++++++++++++-------- QuickMD/QuickMDTests/ParserTests.swift | 22 ++++++++++ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index d04f03e..d22c3ee 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -61,21 +61,11 @@ enum InlineSyntaxScanner { /// not open a "span" that swallows a later link's `[`. private static func build(_ text: String) -> (brackets: [Int: Int], parens: [Int: Int]) { InlineSyntaxScanner.withBytes(text[...]) { bytes in - var brackets: [Int: Int] = [:], parens: [Int: Int] = [:] - var openBrackets: [Int] = [], openParens: [Int] = [] - var i = 0 - while i < bytes.count { - if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } - switch bytes[i] { - case UInt8(ascii: "("): openParens.append(i) - case UInt8(ascii: ")"): if let open = openParens.popLast() { parens[open] = i } - default: break - } - i += 1 - } - + var brackets: [Int: Int] = [:] + let parens = pairParens(bytes) + var openBrackets: [Int] = [] var codeSpans = CodeSpanScanner() - i = 0 + var i = 0 while i < bytes.count { if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } if bytes[i] == backtick, let end = codeSpans.end(bytes, at: i) { @@ -101,6 +91,24 @@ enum InlineSyntaxScanner { } } + /// Every `(` → its `)`, one stack pass, escapes respected — exactly the + /// closer a depth scan from that `(` finds (`scanParenDestination`). + fileprivate static func pairParens(_ bytes: Bytes) -> [Int: Int] { + var parens: [Int: Int] = [:] + var open: [Int] = [] + var i = 0 + while i < bytes.count { + if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } + switch bytes[i] { + case UInt8(ascii: "("): open.append(i) + case UInt8(ascii: ")"): if let start = open.popLast() { parens[start] = i } + default: break + } + i += 1 + } + return parens + } + /// A pair table seen from a slice: offsets in the slice's byte buffer are /// `base` bytes into the table's text. fileprivate struct Lookup { @@ -333,6 +341,10 @@ enum InlineSyntaxScanner { var depth = 0 var i = open var codeSpans = CodeSpanScanner() + /// Built on the first inner `](` only: a depth scan per `](` that + /// never closes went to the end of the line each time (quadratic on + /// `![x` + `[a](` × N, review round 7). + var parens: [Int: Int]? while i < bytes.count { if isEscape(bytes, at: i, before: bytes.count) { i += 2; continue } if bytes[i] == backtick, let end = codeSpans.end(bytes, at: i) { i = end; continue } @@ -343,10 +355,12 @@ enum InlineSyntaxScanner { if depth == 0 { return i } // An inner link's destination is not scanned for code spans // (see `PairTable.build`). - if i + 1 < bytes.count, bytes[i + 1] == UInt8(ascii: "("), - let close = scanParenDestination(bytes, open: i + 1, nil)?.end { - i = close - continue + if i + 1 < bytes.count, bytes[i + 1] == UInt8(ascii: "(") { + if parens == nil { parens = pairParens(bytes) } + if let close = parens?[i + 1] { + i = close + 1 + continue + } } default: break } diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 86949d3..ea96d07 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -494,6 +494,28 @@ final class ParserTests: XCTestCase { .nonZero, "no list hang") } + /// An image line whose alt holds many inner `[a](` that never close: the + /// bracket scan must stay linear (a depth scan per `](` to the end of the + /// line made it quadratic, review round 7). Relative, not absolute: 8× + /// the input may take at most ~3× the linear share. + func testImageLineWithUnclosedInnerLinksParsesInLinearTime() { + let parser = MarkdownBlockParser(theme: MarkdownTheme.cached(for: .light)) + func time(_ count: Int) -> TimeInterval { + let line = "![x" + String(repeating: "[a](", count: count) + "]" + _ = parser.parse(line) // warm + var best = TimeInterval.infinity + for _ in 0..<3 { + let start = Date() + _ = parser.parse(line) + best = min(best, Date().timeIntervalSince(start)) + } + return best + } + let small = time(5_000) + let large = time(40_000) + XCTAssertLessThan(large, max(small, 0.005) * 24, "5k: \(small) s, 40k: \(large) s (quadratic would be ~64×)") + } + // MARK: - Reference links in quotes, alerts and table cells /// Quotes, alerts and table cells keep their markdown and are rendered From 0e8ced066beb8710f639853ca4a9b39dce0e443f Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:52:20 +0100 Subject: [PATCH 083/105] fix(anchors): a multi-line Setext title gets GitHub's id GitHub keeps a Setext title's line breaks in the heading's text and the id drops them: `Alpha` / `Beta` / `===` is `#alphabeta` (a hard break too). The title was joined with spaces, which the slug turned into `-` (`alpha-beta`), so `[go](#alphabeta)` went to another heading or nowhere. The title now keeps its line breaks (MarkdownBlockParser.setextTitle, which also drops a hard break's backslash). Anchors read them (MarkdownRenderer.headingAnchorText); the heading, search and the Contents sidebar show each as a space (shownHeadingTitle, ToCEntry.entries). Copy Section is unchanged (source lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 21 +++++++-- QuickMD/QuickMD/MarkdownRenderer.swift | 17 +++++++- QuickMD/QuickMD/MarkdownView.swift | 7 +-- QuickMD/QuickMD/SectionExtractor.swift | 18 ++++++-- .../QuickMDTests/SectionExtractorTests.swift | 43 ++++++++++++++++--- .../QuickMDTests/VerticalRhythmTests.swift | 11 +++-- 6 files changed, 91 insertions(+), 26 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 4136156..1d0b635 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -296,7 +296,8 @@ struct MarkdownBlockParser: Sendable { // Setext headers (underlined headers): the WHOLE paragraph above // the underline is the heading (CommonMark, GitHub), on the - // paragraph's first line, its lines joined like soft breaks. Not a + // paragraph's first line. Its title keeps the lines' breaks + // (`setextTitle`): shown as spaces, dropped by the anchor. Not a // list item's paragraph (`- a` over `===` is the item's text, over // `---` the list and a rule) nor a quote line's (the quote branch // takes those, lazy lines included). @@ -318,8 +319,7 @@ struct MarkdownBlockParser: Sendable { level = nil } if let level, let start = Self.setextParagraphStart(in: textBuffer, endingWith: line) { - let title = (textBuffer[start...].map { Self.markerLine($0.line) } + [trimmed]) - .joined(separator: " ") + let title = Self.setextTitle(textBuffer[start...].map(\.line) + [line]) let headingLine = start < textBuffer.count ? textBuffer[start].originalIndex : sourceLine var preceding = Array(textBuffer[..<start]) textBuffer.removeAll() @@ -1007,6 +1007,21 @@ struct MarkdownBlockParser: Sendable { // MARK: - Setext headings + /// A Setext heading's title: its lines, trimmed, joined with the line + /// breaks between them — GitHub's heading text keeps them, and its id + /// drops them (`Alpha⏎Beta` is `alphabeta`, where a space would be a + /// `-`); `MarkdownRenderer.shownHeadingTitle` shows each as a space. A + /// hard break's backslash is not text (an odd run of them ends the + /// line; trailing spaces are trimmed anyway). + static func setextTitle(_ lines: [String]) -> String { + let trimmed = lines.map(markerLine) + return trimmed.enumerated().map { index, text in + guard index < trimmed.count - 1 else { return text } + let backslashes = text.reversed().prefix(while: { $0 == "\\" }).count + return backslashes % 2 == 1 ? String(text.dropLast()) : text + }.joined(separator: "\n") + } + /// Where the paragraph that `line` (the line over a Setext underline) /// ends starts in the text buffer: after its last blank line or rule. /// Nil when that paragraph is a list item's — an item line starts it or diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index d6dc714..d7b19b8 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -458,8 +458,23 @@ struct MarkdownRenderer: Sendable { // MARK: - Block Renderers + /// A heading's title as shown: a Setext title keeps its paragraph's line + /// breaks (`MarkdownBlockParser.setextTitle`) and each reads as a space, + /// like a soft break in a paragraph — in the heading, the Contents + /// sidebar and search alike. + static func shownHeadingTitle(_ title: String) -> String { + title.replacingOccurrences(of: "\n", with: " ") + } + + /// The text GitHub takes a heading's id from: the rendered title with a + /// Setext title's line breaks KEPT — the slug drops them (`Alpha⏎Beta` + /// is `alphabeta`), where the space shown would be a `-`. + func headingAnchorText(_ title: String) -> String { + String(renderInlineFormatting(title).characters) + } + func renderHeader(_ text: String, level: Int) -> AttributedString { - var attr = renderInlineFormatting(text) + var attr = renderInlineFormatting(Self.shownHeadingTitle(text)) attr.setDualFont(size: headerFontSize(level: level), bold: true, fonts: theme.fonts) attr.setDualForeground(theme.textColor) return attr diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index d0e7958..4d44476 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -732,12 +732,7 @@ struct MarkdownView: View { theme: currentTheme, fontScale: scale, contentWidth: width, math: .swiftMath) : .empty - headings = blocks.compactMap { block in - if case .heading(let level, let title, let sourceLine) = block.content { - return ToCEntry(id: block.id, level: level, title: title, sourceLine: sourceLine) - } - return nil - } + headings = ToCEntry.entries(of: blocks) // A landing that waited for THIS parse (Source Edit saved, then // left before the re-parse arrived): requested in the transaction // that installs the blocks, so it wins over the list's own anchor diff --git a/QuickMD/QuickMD/SectionExtractor.swift b/QuickMD/QuickMD/SectionExtractor.swift index f038db9..d008461 100644 --- a/QuickMD/QuickMD/SectionExtractor.swift +++ b/QuickMD/QuickMD/SectionExtractor.swift @@ -9,8 +9,18 @@ import Foundation struct ToCEntry: Identifiable, Sendable { let id: String let level: Int + /// As shown (`MarkdownRenderer.shownHeadingTitle`): one line. let title: String let sourceLine: Int + + /// The headings of `blocks`, in document order. + static func entries(of blocks: [MarkdownBlock]) -> [ToCEntry] { + blocks.compactMap { block in + guard case .heading(let level, let title, let sourceLine) = block.content else { return nil } + return ToCEntry(id: block.id, level: level, title: MarkdownRenderer.shownHeadingTitle(title), + sourceLine: sourceLine) + } + } } // MARK: - Contents Depth @@ -86,12 +96,14 @@ enum HeadingAnchors { } /// The headings of `blocks` as links see them: block id and the text the - /// heading shows (`renderHeader`'s characters, as `HeadingBlockView`). + /// heading shows (`renderHeader`'s characters, as `HeadingBlockView`), + /// with a Setext title's line breaks kept for the slug to drop + /// (`MarkdownRenderer.headingAnchorText`). static func headings(of blocks: [MarkdownBlock], renderer: MarkdownRenderer) -> [(id: String, text: String)] { blocks.compactMap { block in - guard case .heading(let level, let title, _) = block.content else { return nil } - return (block.id, String(renderer.renderHeader(title, level: level).characters)) + guard case .heading(_, let title, _) = block.content else { return nil } + return (block.id, renderer.headingAnchorText(title)) } } diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index beb40c2..2f3d52a 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -7,13 +7,7 @@ import XCTest final class SectionExtractorTests: XCTestCase { private func headings(of markdown: String) -> [ToCEntry] { - let blocks = MarkdownBlockParser(theme: MarkdownTheme.cached(for: .light)).parse(markdown) - return blocks.compactMap { block in - if case .heading(let level, let title, let sourceLine) = block.content { - return ToCEntry(id: block.id, level: level, title: title, sourceLine: sourceLine) - } - return nil - } + ToCEntry.entries(of: MarkdownBlockParser(theme: MarkdownTheme.cached(for: .light)).parse(markdown)) } func testSectionEndsAtNextSameLevelHeading() { @@ -166,6 +160,41 @@ final class SectionExtractorTests: XCTestCase { ]) } + /// A Setext title over several lines: GitHub keeps its line breaks in + /// the heading's text, and the id DROPS them — `Alpha⏎Beta` is + /// `alphabeta`, after a hard break too (gfm/h1_*.html). The title was + /// joined with spaces, which the slug made `-` (`alpha-beta`), so + /// `[go](#alphabeta)` went nowhere. Shown — the heading, the Contents + /// sidebar, search — each break is a space; Copy Section is the source. + func testAMultiLineSetextTitleHasGitHubsId() { + let md = "Alpha\nBeta\n===\n\nAlphaBeta\n===\n\nAlpha\nBeta\n---\n\nPara one\nline two\n---\n\n" + + "One \nTwo\\\nThree\n---\n\n**Bold\nend**\n===\n\n[go](#alphabeta)\n" + let anchors = anchorHeadings(of: md) + XCTAssertEqual(HeadingAnchors.ids(for: anchors.map(\.text)), [ + "alphabeta", "alphabeta-1", "alphabeta-2", "para-oneline-two", "onetwothree", "boldend", + ]) + XCTAssertEqual(HeadingAnchors.target(forFragment: "alphabeta", in: anchors), .heading(anchors[0].id)) + XCTAssertEqual(HeadingAnchors.target(forFragment: "para-oneline-two", in: anchors), + .heading(anchors[3].id)) + + let theme = MarkdownTheme.cached(for: .light) + let blocks = MarkdownBlockParser(theme: theme).parse(md) + let renderer = MarkdownRenderer(theme: theme) + let shown = blocks.compactMap { block -> String? in + guard case .heading(let level, let title, _) = block.content else { return nil } + return String(renderer.renderHeader(title, level: level).characters) + } + XCTAssertEqual(shown, ["Alpha Beta", "AlphaBeta", "Alpha Beta", "Para one line two", "One Two Three", + "Bold end"]) + let entries = headings(of: md) + XCTAssertEqual(entries.map(\.title), ["Alpha Beta", "AlphaBeta", "Alpha Beta", "Para one line two", + "One Two Three", "**Bold end**"]) + XCTAssertEqual(SectionExtractor.extractSection(from: md, entry: entries[0], headings: entries), + "Alpha\nBeta\n===") + XCTAssertEqual(SectionExtractor.extractSection(from: md, entry: entries[4], headings: entries), + "One \nTwo\\\nThree\n---") + } + func testFragmentFindsItsHeading() { let headings = anchorHeadings(of: """ # Guide diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 0718e86..44f8511 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -653,7 +653,7 @@ final class VerticalRhythmTests: XCTestCase { case .alert(let kind, let content): return "alert:\(kind.rawValue):\(content)" case .text(let attributed): return "t:" + String(attributed.characters) case .codeBlock(let code, _): return "c:\(code)" - case .heading(_, let title, _): return "h:\(title)" + case .heading(_, let title, _): return "h:" + MarkdownRenderer.shownHeadingTitle(title) case .image: return "img" case .table: return "table" default: return "other" @@ -850,13 +850,12 @@ final class VerticalRhythmTests: XCTestCase { let spans = SourceTint.spans(for: blocks, lineStarts: SourceEditSupport.lineStarts(in: ns), in: ns) XCTAssertEqual(spans.filter { $0.kind == .heading }.map { ns.substring(with: $0.range) }, ["Para one\nline two\n---"]) - let entries = blocks.compactMap { block -> ToCEntry? in - guard case .heading(let level, let title, let line) = block.content else { return nil } - return ToCEntry(id: block.id, level: level, title: title, sourceLine: line) - } + let entries = ToCEntry.entries(of: blocks) XCTAssertEqual(SectionExtractor.extractSection(from: markdown, entry: entries[0], headings: entries), "Para one\nline two\n---\n\nAfter") - XCTAssertEqual(HeadingAnchors.slug(entries[0].title), "para-one-line-two") + // GitHub's id drops the line break (gfm/h1_words.html). + let anchors = HeadingAnchors.headings(of: blocks, renderer: MarkdownRenderer(theme: .cached(for: .light))) + XCTAssertEqual(HeadingAnchors.ids(for: anchors.map(\.text)), ["para-oneline-two"]) } // MARK: - Long quotes From cbe5c1c8a0de3a129678b7f9a50d036a428698f4 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:55:18 +0100 Subject: [PATCH 084/105] fix(parser): a block start ends a table's body A table's body took every line with a `|`. GitHub closes a table for any block start: a fence, a quote, an ATX heading, a list item or an HTML block. Here `$$` opens a formula too. A fence or `$$` line with a pipe became a row while the definition pre-pass read it as a block, and the block's closing line then opened one that ran to the end of the document. MarkdownBlockParser.interruptsTable ends the body, and a quote's table the same way (QuoteTip): an item after the table goes on lazily, as on GitHub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 15 ++++++-- .../QuickMDTests/VerticalRhythmTests.swift | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 1d0b635..80b2c1c 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -345,7 +345,8 @@ struct MarkdownBlockParser: Sendable { var rows: [[String]] = [] i += 2 // Skip header and separator - while i < lines.count && lines[i].contains("|") && !isTableSeparator(lines[i]) { + while i < lines.count && lines[i].contains("|") && !isTableSeparator(lines[i]) + && !Self.interruptsTable(lines[i]) { rows.append(parseTableRow(lines[i])) i += 1 } @@ -1183,7 +1184,8 @@ struct MarkdownBlockParser: Sendable { inTable = false return } - if inTable, content.contains("|") { + // A row; a block start ends the table (`interruptsTable`). + if inTable, content.contains("|"), !MarkdownBlockParser.interruptsTable(content) { endsInParagraph = false return } @@ -1225,6 +1227,15 @@ struct MarkdownBlockParser: Sendable { return true } + /// Whether a block starts on `line`, which ends a table's body: GitHub + /// closes a table for any block start — a fence, a quote, an ATX + /// heading, a list item, an HTML block (gfm/h2_*.html) — and `$$` opens + /// a formula here. A fence or `$$` line taken as a row disagreed with the + /// definition pre-pass (`VerbatimRegions`), which reads it as a block. + static func interruptsTable(_ line: String) -> Bool { + !startsNoBlock(line) || markerLine(line).hasPrefix(">") + } + /// CommonMark's part of `continuesQuoteLazily`: not blank, and no block /// starts on the line (fence, `$$`, ATX heading, rule, list item, HTML /// block) — checked against the document, as cmark does. diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 44f8511..e1b0e47 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -900,6 +900,40 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertFalse(again.converted[paragraph.id] === first.converted[paragraph.id]) } + // MARK: - Tables + + /// A block start ends a table's body (GFM: GitHub closes the table for + /// any of them — a fence, a quote, an ATX heading, a list item, an HTML + /// block; gfm/h2_*.html — and `$$` opens a formula here). The body took + /// every line with a `|`: a fence line with one became a row while the + /// definition pre-pass read it as a fence, and the fence's closing line + /// then opened a fence that ran to the end of the document. + func testABlockStartEndsATablesBody() throws { + let table = "| a |\n|---|\n| 1 |\n" + func bodyRows(_ markdown: String) throws -> [Int] { + try XCTUnwrap(parseReturning(markdown)).compactMap { block in + guard case .table(_, let rows, _, _) = block.content else { return nil } + return rows.count + } + } + XCTAssertEqual(try quoteShape(table + "```x|\ncode\n```\nafter\n"), ["table", "c:code", "t:after"]) + XCTAssertEqual(try quoteShape(table + "~~~ |\ncode\n~~~\n"), ["table", "c:code"]) + XCTAssertEqual(try quoteShape(table + "> q | r\n"), ["table", "q1:q | r"]) + XCTAssertEqual(try quoteShape(table + "# H | x\n"), ["table", "h:H | x"]) + XCTAssertEqual(try quoteShape(table + "- b | c\n"), ["table", "t:• b | c"]) + XCTAssertEqual(try quoteShape(table + "2. b | c\n"), ["table", "t:2. b | c"]) + XCTAssertEqual(try bodyRows(table + "<div>|</div>\n"), [1]) + XCTAssertEqual(try bodyRows(table + "$$ |\nx\n$$\n\n[x]: http://e.com\n\n[x]\n"), [1]) + XCTAssertEqual(try quoteShape(table + "$$ |\nx\n$$\n\n[x]: http://e.com\n\n[x]\n").last, "t:x") + // A row stays a row. + XCTAssertEqual(try bodyRows(table + "b | c\n| 3 |\n"), [3]) + // In a quote too: the item's paragraph goes on lazily, a heading's + // does not (gfm/h2_quote_table_*_lazy.html). + let quoted = "> | a |\n> |---|\n> | 1 |\n" + XCTAssertEqual(try quoteShape(quoted + "> - b | c\nlazy\n"), ["q1:| a |\n|---|\n| 1 |\n- b | c\nlazy"]) + XCTAssertEqual(try quoteShape(quoted + "> # H | x\nlazy\n"), ["q1:| a |\n|---|\n| 1 |\n# H | x", "t:lazy"]) + } + /// The lines a quote absorbed are the quote's: its source line is its /// first, the next block starts after them, the tint spans them, search /// finds and copy reads them in the quote — and a long hard-wrapped quote From f5425f94fd95a9edac10b8fe451b03ddd6be121d Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:56:29 +0100 Subject: [PATCH 085/105] fix(parser): a tab past an indented fence's indentation leaves spaces A fence indented N columns drops up to N columns from each of its lines. A tab reaching past them was kept whole, so the line was drawn too deep: `\tTab` under a fence indented 2 showed a tab where CommonMark and GitHub show " Tab". The columns the tab has left are now spaces; a later tab stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 13 ++++++++----- QuickMD/QuickMDTests/VerticalRhythmTests.swift | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 80b2c1c..e9f0a4b 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -1058,13 +1058,14 @@ struct MarkdownBlockParser: Sendable { return columns } - /// `line` without up to `columns` columns of its leading spaces and tabs - /// (a tab that would reach past them stays). + /// `line` without up to `columns` columns of its leading spaces and tabs. + /// A tab that reaches past them leaves the columns it has left as spaces + /// (CommonMark: `\tTab` under a fence indented 2 is ` Tab`). static func removingIndentation(_ line: String, upTo columns: Int) -> String { guard columns > 0 else { return line } var used = 0 var index = line.startIndex - while index < line.endIndex { + while index < line.endIndex, used < columns { let character = line[index] let width: Int if character == " " { @@ -1074,9 +1075,11 @@ struct MarkdownBlockParser: Sendable { } else { break } - guard used + width <= columns else { break } - used += width index = line.index(after: index) + if used + width > columns { + return String(repeating: " ", count: used + width - columns) + line[index...] + } + used += width } return String(line[index...]) } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index e1b0e47..2ece021 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -797,6 +797,21 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(DocumentSearch.computeMatches(in: blocks, term: "let needle").matchBlockIds, [blocks[1].id]) } + /// A tab that reaches past the fence's indentation keeps the columns it + /// has left, as spaces (CommonMark; GitHub, gfm/h3_*.html): the tab was + /// kept whole, a code line drawn two columns too deep. A later tab stays. + func testAnIndentedFenceKeepsWhatATabHasLeft() throws { + XCTAssertEqual(try quoteShape("- a\n\n ```\n\tTab\n ```\n"), ["t:• a", "c: Tab"]) + XCTAssertEqual(try quoteShape(" ```\n\tTab\n ```\n"), ["c: Tab"]) + XCTAssertEqual(try quoteShape(" ```\n\tTab\n ```\n"), ["c: Tab"]) + XCTAssertEqual(try quoteShape(" ```\n\t\tTab\n ```\n"), ["c: \tTab"]) + XCTAssertEqual(try quoteShape(" ```\n \tTab\n ```\n"), ["c: Tab"]) + // A tab inside the indentation goes, one after it stays whole. + XCTAssertEqual(MarkdownBlockParser.removingIndentation("\tx", upTo: 4), "x") + XCTAssertEqual(MarkdownBlockParser.removingIndentation(" \tx", upTo: 2), "\tx") + XCTAssertEqual(MarkdownBlockParser.removingIndentation("x", upTo: 2), "x") + } + /// Source Edit finds a selection made in such a block in the source, /// whose lines still carry the indentation: a multi-line selection is /// matched with the spaces and tabs at the start of each source line From 61b9a6d741dbe83b642432ff7cf9bcef6ba0dfe9 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 22:59:33 +0100 Subject: [PATCH 086/105] fix(parser): a link definition may start a quote's paragraph `> [x]: url` is a definition on GitHub: it defines `x` and leaves the quote empty. It was shown as the quote's text. The definition pre-pass now takes a link definition on a quote line that starts a paragraph of its quote (DefinitionPlacement.linkDefinitionText, QuoteTip.admitsDefinition). It stays text after the quote's text (a definition cannot interrupt a paragraph), in the quote's fence or table, and after an alert's marker, as on GitHub. One GitHub definition stays text: a deeper quote opened right under the quote's text (`> a`, then `> > [x]: url`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 47 ++++++++++++++----- .../QuickMDTests/VerticalRhythmTests.swift | 27 +++++++++++ 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index e9f0a4b..9ac3c1b 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -109,11 +109,11 @@ struct MarkdownBlockParser: Sendable { footnoteDefinitions.append((id: fnId, content: fnContent)) if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } placement.definition() - } else if !placement.paragraphOpen, - let match = Self.refLinkDefRegex.firstMatch(in: trimmed, range: nsRange), - let idRange = Range(match.range(at: 1), in: trimmed), - let urlRange = Range(match.range(at: 2), in: trimmed) { - referenceDefinitions[String(trimmed[idRange]).lowercased()] = String(trimmed[urlRange]) + } else if let text = placement.linkDefinitionText(trimmed), + let match = Self.refLinkDefRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), + let idRange = Range(match.range(at: 1), in: text), + let urlRange = Range(match.range(at: 2), in: text) { + referenceDefinitions[String(text[idRange]).lowercased()] = String(text[urlRange]) placement.definition() } else { lines.append(line) @@ -1157,6 +1157,11 @@ struct MarkdownBlockParser: Sendable { private var inTable = false private var previousHadPipe = false + /// Whether the group's next line would start a paragraph: none is + /// open, and it is not in the quote's fence or table — where a link + /// definition may stand (`DefinitionPlacement`). + var admitsDefinition: Bool { fence == nil && !inTable && !endsInParagraph } + /// A lazily continued line (no marker, or fewer markers than the /// group): paragraph text whatever it looks like — CommonMark never /// reads a lazy line as a table separator, a Setext underline or any @@ -1278,6 +1283,31 @@ struct MarkdownBlockParser: Sendable { quote = nil } + /// What a LINK definition on the next line (its `markerLine`) would + /// be read from, or nil where none may stand: the line itself where + /// no paragraph is open; what follows a quote line's markers where + /// the line starts a paragraph of its quote (GitHub: `> [x]: url` + /// defines `x` and leaves the quote empty) — not after the quote's + /// text, nor in its fence or table. One case GitHub reads as a + /// definition stays text: a deeper quote opened right under the + /// quote's text (`> a`, then `> > [x]: url`). + func linkDefinitionText(_ marker: String) -> String? { + guard marker.hasPrefix(">") else { return paragraphOpen ? nil : marker } + guard quote?.admitsDefinition ?? true else { return nil } + return MarkdownBlockParser.markerLine(String(Self.quoteContent(of: marker))) + } + + /// What follows a quote line's markers (each `>` with one optional + /// space after it). + static func quoteContent(of marker: String) -> Substring { + var content = marker[...] + while content.hasPrefix(">") { + content = content.dropFirst() + if content.hasPrefix(" ") { content = content.dropFirst() } + } + return content + } + /// Any other line, kept for the block loop. mutating func kept(_ line: String) { if MarkdownRenderer.isBlankLine(line) { @@ -1287,13 +1317,8 @@ struct MarkdownBlockParser: Sendable { } let marker = MarkdownBlockParser.markerLine(line) if marker.hasPrefix(">") { - var content = marker[...] - while content.hasPrefix(">") { - content = content.dropFirst() - if content.hasPrefix(" ") { content = content.dropFirst() } - } var tip = quote ?? QuoteTip() - tip.take(String(content)) + tip.take(String(Self.quoteContent(of: marker))) quote = tip paragraphOpen = tip.endsInParagraph return diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 2ece021..06391df 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -760,6 +760,33 @@ final class VerticalRhythmTests: XCTestCase { /// lazy line, a table's — it is that text, and `[x]` stays literal. /// The pre-pass took it out anywhere. A footnote definition MAY /// interrupt a paragraph (GitHub). Every shape: gfm/def_*.html, fn_*.html. + /// A link definition that starts a paragraph of a quote is a definition + /// (GitHub, gfm/h4_*.html): `> [x]: url` defines `x` and leaves the + /// quote empty — it was the quote's text. After the quote's text it is + /// that text (G7), in the quote's fence code, in its table a row, after + /// an alert's marker the alert's text. + func testALinkDefinitionMayStartAQuoteParagraph() throws { + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n\n[x]\n"), ["t:x"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> text\n\n[x]\n"), ["q1:text", "t:x"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> [y]: http://f.com\n\n[x] [y]\n"), ["t:x y"]) + XCTAssertEqual(try quoteShape("> > [x]: http://e.com\n\n[x]\n"), ["t:x"]) + XCTAssertEqual(try quoteShape("Para\n> [x]: http://e.com\n\n[x]\n"), ["t:Para", "t:x"]) + XCTAssertEqual(try quoteShape("> # H\n> [x]: http://e.com\n\n[x]\n"), ["q1:# H", "t:x"]) + XCTAssertEqual(try quoteShape("> text\n>\n> [x]: http://e.com\n\n[x]\n"), ["q1:text", "t:x"]) + XCTAssertEqual(try quoteShape("> text\n>\n> [x]: http://e.com\n>\n> more\n\n[x]\n"), + ["q1:text\n\n\nmore", "t:x"]) + // Text where GitHub has text. + XCTAssertEqual(try quoteShape("> text\n> [x]: http://e.com\n\n[x]\n"), + ["q1:text\n[x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("> ```\n> [x]: http://e.com\n> ```\n\n[x]\n").last, "t:[x]") + XCTAssertEqual(try quoteShape("> | a |\n> |---|\n> | 1 |\n> [x]: http://e.com\n\n[x]\n").last, "t:[x]") + XCTAssertEqual(try quoteShape("> [!NOTE]\n> [x]: http://e.com\n\n[x]\n").last, "t:[x]") + XCTAssertEqual(try quoteShape("> > a\n> [x]: http://e.com\n\n[x]\n").last, "t:[x]") + // What follows keeps its source line. + let blocks = try XCTUnwrap(parseReturning("> [x]: http://e.com\n> text\n\n[x]\n")) + XCTAssertEqual(blocks.map(\.sourceLine), [1, 3]) + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) From 11f924d3c3999dd284ed011becfa06439c5199f7 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:07:07 +0100 Subject: [PATCH 087/105] fix(quote): a run of blank lines in a quote is one paragraph break Two blank `>` lines drew two gaps in a quote or an alert, at any nesting level. GitHub gives one paragraph break, as our top-level rhythm does, where spacing comes from structure, not from blank lines. A definition the pre-pass takes out between two blank lines left the same double gap. The parser now makes each run of blank lines in a quote group one (MarkdownBlockParser.collapsingBlankRuns); blank lines inside the quote's fence are code and stay. The view, measurer, search and print all read that content. Blank lines at a quote's start or end still add nothing. Copy pasted the quote's blank line as a line holding one space (the body draws a blank line as a space to keep its height). A line of only spaces and tabs is now copied as an empty line, so paragraphs copy with one blank line between them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/DocumentSelection.swift | 17 ++++++++++ QuickMD/QuickMD/MarkdownBlockParser.swift | 25 ++++++++++++++ .../QuickMDTests/VerticalRhythmTests.swift | 34 +++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/QuickMD/QuickMD/DocumentSelection.swift b/QuickMD/QuickMD/DocumentSelection.swift index e80da23..7921990 100644 --- a/QuickMD/QuickMD/DocumentSelection.swift +++ b/QuickMD/QuickMD/DocumentSelection.swift @@ -371,6 +371,7 @@ enum DocumentCopyBuilder { case .text(let text): let cleaned = replacingAttachments(in: text) trimBlankLines(cleaned) + emptyWhitespaceLines(cleaned) collapseBlankLineRuns(cleaned) return cleaned case .code(let code): @@ -487,6 +488,22 @@ enum DocumentCopyBuilder { } } + private static let whitespaceLine = try? NSRegularExpression(pattern: "^[ \t]+$", + options: .anchorsMatchLines) + + /// A line of nothing but spaces and tabs becomes an empty line: a quote + /// or alert body draws a blank line as one space so it keeps its height + /// (`renderQuotedBody`), and a paragraph break must paste as an empty + /// line, not as a space. Only lines between others are left by + /// `trimBlankLines`; a line of no-break spaces is text and stays. + private static func emptyWhitespaceLines(_ text: NSMutableAttributedString) { + guard let whitespaceLine else { return } + let matches = whitespaceLine.matches(in: text.string, range: NSRange(location: 0, length: text.length)) + for match in matches.reversed() { + text.replaceCharacters(in: match.range, with: "") + } + } + private static let blankLineRun = try? NSRegularExpression(pattern: "\n{3,}") /// Three or more consecutive line breaks become two: a renderer chunk diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 9ac3c1b..d26c5cf 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -445,6 +445,8 @@ struct MarkdownBlockParser: Sendable { i += 1 } + // One paragraph break per run of blank lines (`collapsingBlankRuns`). + groupLines = Self.collapsingBlankRuns(groupLines) let content = groupLines.joined(separator: "\n") .trimmingCharacters(in: .whitespacesAndNewlines) if !content.isEmpty { @@ -1042,6 +1044,29 @@ struct MarkdownBlockParser: Sendable { // MARK: - Fences and `$$` blocks + /// A quote's or alert's lines with each run of blank lines made ONE: a + /// blank line only separates paragraphs, and any run of them is one + /// paragraph break (GitHub, every nesting level; a definition the + /// pre-pass took out between two blank lines leaves none of its own). + /// Inside the quote's fence a blank line is code and stays. + static func collapsingBlankRuns(_ lines: [String]) -> [String] { + var result: [String] = [] + result.reserveCapacity(lines.count) + var fence: Fence? + for line in lines { + let marker = markerLine(line) + if let open = fence { + if closesFence(marker, open) { fence = nil } + } else if let opening = fenceOpening(marker) { + fence = opening + } else if isBlank(line), let last = result.last, isBlank(last) { + continue + } + result.append(line) + } + return result + } + /// Columns of the spaces and tabs a line starts with (a tab runs to the /// next multiple of 4, CommonMark). static func indentColumns(_ line: String) -> Int { diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 06391df..a1d3e11 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -765,6 +765,38 @@ final class VerticalRhythmTests: XCTestCase { /// quote empty — it was the quote's text. After the quote's text it is /// that text (G7), in the quote's fence code, in its table a row, after /// an alert's marker the alert's text. + /// A run of blank lines in a quote or an alert is ONE paragraph break, + /// at every nesting level (GitHub, gfm/h5_*.html; at the top level our + /// spacing comes from structure, not from blank lines): two blank `>` + /// lines drew two gaps. Blank lines at a quote's start or end add + /// nothing; inside the quote's fence they are code. The body every + /// consumer reads (view, measurer, search, copy, print) has one. + func testARunOfBlankQuoteLinesIsOneParagraphBreak() throws { + XCTAssertEqual(try quoteShape("> a\n>\n>\n> b\n"), ["q1:a\n\nb"]) + XCTAssertEqual(try quoteShape("> a\n>\n>\n>\n> b\n>\n>\n> c\n"), ["q1:a\n\nb\n\nc"]) + XCTAssertEqual(try quoteShape("> > a\n> >\n> >\n> > b\n"), ["q2:a\n\nb"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> a\n>\n>\n> b\n"), ["alert:note:a\n\nb"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n>\n>\n> a\n>\n>\n"), ["alert:note:a"]) + XCTAssertEqual(try quoteShape(">\n>\n> a\n>\n>\n\nafter\n"), ["q1:a", "t:after"]) + XCTAssertEqual(try quoteShape("> a\n>\n>\n> > b\n> >\n> >\n> c\n"), ["q1:a", "q2:b", "q1:c"]) + // A definition taken out (H4) leaves no gap of its own. + XCTAssertEqual(try quoteShape("> text\n>\n> [x]: http://e.com\n>\n> more\n\n[x]\n"), + ["q1:text\n\nmore", "t:x"]) + // Code keeps its blank lines. + XCTAssertEqual(try quoteShape("> ```\n> a\n>\n>\n> b\n> ```\n"), ["q1:```\na\n\n\nb\n```"]) + + let theme = MarkdownTheme.cached(for: .light) + let blocks = try XCTUnwrap(parseReturning("> a\n>\n>\n> b\n\n> [!TIP]\n> c\n>\n>\n> d\n")) + let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: 600, math: nil) + XCTAssertEqual(blocks.map { measured.converted[$0.id]?.string }, ["a\n \nb", "c\n \nd"]) + let all = try XCTUnwrap(DocumentSelection.selectAll(rowCount: blocks.count, + lengths: { measured.converted[blocks[$0].id]?.length ?? 0 })) + let parts = SelectionRows.parts(of: all, blocks: blocks, text: { measured.converted[$0.id] }, + table: { _ in nil }) + XCTAssertEqual(DocumentCopyBuilder.build(parts: parts).plain, "a\n\nb\n\nc\n\nd") + } + func testALinkDefinitionMayStartAQuoteParagraph() throws { XCTAssertEqual(try quoteShape("> [x]: http://e.com\n\n[x]\n"), ["t:x"]) XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> text\n\n[x]\n"), ["q1:text", "t:x"]) @@ -773,8 +805,6 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("Para\n> [x]: http://e.com\n\n[x]\n"), ["t:Para", "t:x"]) XCTAssertEqual(try quoteShape("> # H\n> [x]: http://e.com\n\n[x]\n"), ["q1:# H", "t:x"]) XCTAssertEqual(try quoteShape("> text\n>\n> [x]: http://e.com\n\n[x]\n"), ["q1:text", "t:x"]) - XCTAssertEqual(try quoteShape("> text\n>\n> [x]: http://e.com\n>\n> more\n\n[x]\n"), - ["q1:text\n\n\nmore", "t:x"]) // Text where GitHub has text. XCTAssertEqual(try quoteShape("> text\n> [x]: http://e.com\n\n[x]\n"), ["q1:text\n[x]: http://e.com", "t:[x]"]) From 324453f028d6a3a71420ed1560341d806669ad48 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:13:07 +0100 Subject: [PATCH 088/105] fix(anchors): a code span across a Setext title's break keeps a space in the id A line break inside a code span is a space (CommonMark), so on GitHub `code` / `span` inside one code span, under a Setext underline, is `#code-span`. Outside a code span the break is dropped (`#codespan` for two spans). Since the anchor text keeps a multi-line title's breaks (H1), the span kept its break too and the slug dropped it. The inline code renderer now turns a line break inside a span into a space. Only the anchor text still has one there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownRenderer.swift | 5 ++++- QuickMD/QuickMDTests/SectionExtractorTests.swift | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index d7b19b8..818d5b4 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -840,7 +840,10 @@ struct MarkdownRenderer: Sendable { } } - var code = String(afterOpening[..<closeRange.lowerBound]) + // A line break inside a code span is a space (CommonMark). Only a + // Setext title's anchor text still has one here + // (`headingAnchorText`): `code⏎span` is `code-span` on GitHub. + var code = String(afterOpening[..<closeRange.lowerBound]).replacingOccurrences(of: "\n", with: " ") // Strip one leading and one trailing space for double-backtick (CommonMark spec) if backtickCount == 2 && code.hasPrefix(" ") && code.hasSuffix(" ") && code.count > 1 { diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index 2f3d52a..2e142cf 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -195,6 +195,20 @@ final class SectionExtractorTests: XCTestCase { "One \nTwo\\\nThree\n---") } + /// A line break inside a code span is a space (CommonMark), so a code + /// span across a Setext title's break keeps one in the id: `code-span`. + /// Outside a code span the break is dropped, also between two spans + /// (GitHub, gfm/h6_*.html). The span kept the break, which the slug + /// dropped: `codespan`. + func testACodeSpanAcrossASetextBreakKeepsASpaceInTheId() { + let md = "`code\nspan`\n===\n\n`code`\n`span`\n===\n\n``a\nb``\n===\n\nUse `a\nb` and\nmore\n---\n" + let anchors = anchorHeadings(of: md) + XCTAssertEqual(HeadingAnchors.ids(for: anchors.map(\.text)), + ["code-span", "codespan", "a-b", "use-a-b-andmore"]) + XCTAssertEqual(HeadingAnchors.target(forFragment: "code-span", in: anchors), .heading(anchors[0].id)) + XCTAssertEqual(headings(of: md).map(\.title), ["`code span`", "`code` `span`", "``a b``", "Use `a b` and more"]) + } + func testFragmentFindsItsHeading() { let headings = anchorHeadings(of: """ # Guide From 8c0fd97bef4ac50274d8a1bf83c3409e232d869a Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:15:12 +0100 Subject: [PATCH 089/105] fix(parser): a definition opening a deeper quote is a definition A deeper `>` opens a nested quote whose first line starts a new paragraph, so on GitHub `> a` then `>> [x]: url` is a definition: an empty nested quote, with `[x]` a link. The definition pre-pass read every quote level as one quote, so the line continued "a" and stayed the nested quote's text. DefinitionPlacement now groups quote lines as the block loop does: a deeper line opens a new group with a fresh QuoteTip, and a shallower line continues the group lazily or starts a new one. Lazy text stays text (`>> a` then `> [x]: url`), and so does a nested quote that a lazy line kept open. A deeper `>` inside the quote's fence is code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 66 +++++++++++++++---- .../QuickMDTests/VerticalRhythmTests.swift | 19 ++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index d26c5cf..3537341 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -1187,6 +1187,9 @@ struct MarkdownBlockParser: Sendable { /// definition may stand (`DefinitionPlacement`). var admitsDefinition: Bool { fence == nil && !inTable && !endsInParagraph } + /// Inside the quote's fence. + var inFence: Bool { fence != nil } + /// A lazily continued line (no marker, or fewer markers than the /// group): paragraph text whatever it looks like — CommonMark never /// reads a lazy line as a table separator, a Setext underline or any @@ -1293,8 +1296,12 @@ struct MarkdownBlockParser: Sendable { /// outside fenced code.) struct DefinitionPlacement { private(set) var paragraphOpen = false - /// The quote being read: what its lines end in (`QuoteTip`). + /// The quote group being read, grouped as the block loop groups it: + /// what its lines end in (`QuoteTip`) and its `>` count. A line with + /// more markers opens a nested quote, a new group; one with fewer + /// goes on lazily or starts a new group. private var quote: QuoteTip? + private var quoteLevel = 0 /// A line of a fenced code or `$$` block (`VerbatimRegions`). mutating func verbatim() { @@ -1312,25 +1319,50 @@ struct MarkdownBlockParser: Sendable { /// be read from, or nil where none may stand: the line itself where /// no paragraph is open; what follows a quote line's markers where /// the line starts a paragraph of its quote (GitHub: `> [x]: url` - /// defines `x` and leaves the quote empty) — not after the quote's - /// text, nor in its fence or table. One case GitHub reads as a - /// definition stays text: a deeper quote opened right under the - /// quote's text (`> a`, then `> > [x]: url`). + /// defines `x` and leaves the quote empty) — a new quote, a deeper + /// one opened under the quote's text (`> a`, then `>> [x]: url`), or + /// a line of the group after no open paragraph. Not lazy text, nor + /// in the quote's fence or table. func linkDefinitionText(_ marker: String) -> String? { guard marker.hasPrefix(">") else { return paragraphOpen ? nil : marker } - guard quote?.admitsDefinition ?? true else { return nil } - return MarkdownBlockParser.markerLine(String(Self.quoteContent(of: marker))) + let (level, content) = Self.quoteMarkers(of: marker) + let text = MarkdownBlockParser.markerLine(String(content)) + switch step(level: level, content: String(content)) { + case .same: + return quote?.admitsDefinition == true ? text : nil + case .lazy: + return nil + case .newGroup: + // A deeper `>` inside the quote's fence is that fence's code. + if level > quoteLevel, quote?.inFence == true { return nil } + return text + } } - /// What follows a quote line's markers (each `>` with one optional - /// space after it). - static func quoteContent(of marker: String) -> Substring { + /// How a quote line goes on from the group being read — the block + /// loop's grouping (lazy: fewer markers, continuing its paragraph). + private enum QuoteStep { case same, lazy, newGroup } + + private func step(level: Int, content: String) -> QuoteStep { + guard let tip = quote else { return .newGroup } + if level == quoteLevel { return .same } + if level < quoteLevel, tip.endsInParagraph, MarkdownBlockParser.startsNoBlock(content) { + return .lazy + } + return .newGroup + } + + /// A quote line's `>` count and what follows its markers (each `>` + /// with one optional space after it). + static func quoteMarkers(of marker: String) -> (level: Int, content: Substring) { var content = marker[...] + var level = 0 while content.hasPrefix(">") { content = content.dropFirst() + level += 1 if content.hasPrefix(" ") { content = content.dropFirst() } } - return content + return (level, content) } /// Any other line, kept for the block loop. @@ -1342,8 +1374,18 @@ struct MarkdownBlockParser: Sendable { } let marker = MarkdownBlockParser.markerLine(line) if marker.hasPrefix(">") { + let (level, content) = Self.quoteMarkers(of: marker) var tip = quote ?? QuoteTip() - tip.take(String(Self.quoteContent(of: marker))) + switch step(level: level, content: String(content)) { + case .same: + tip.take(String(content)) + case .lazy: + tip.takeLazy() + case .newGroup: + tip = QuoteTip() + tip.take(String(content)) + quoteLevel = level + } quote = tip paragraphOpen = tip.endsInParagraph return diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index a1d3e11..41517d5 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -817,6 +817,25 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(blocks.map(\.sourceLine), [1, 3]) } + /// A deeper `>` opens a nested quote, whose first line starts a new + /// paragraph: a link definition there is a definition, also right under + /// the quote's text (GitHub, gfm/h7_*.html). The pre-pass read every + /// level as one quote, so `> a` then `>> [x]: url` was the nested + /// quote's text and `[x]` stayed literal. Fewer markers still continue + /// the paragraph lazily, and so does a nested quote a lazy line kept + /// open; inside the quote's fence a deeper `>` is code. + func testADefinitionOpeningADeeperQuoteIsADefinition() throws { + XCTAssertEqual(try quoteShape("> a\n>> [x]: http://e.com\n\n[x]\n"), ["q1:a", "t:x"]) + XCTAssertEqual(try quoteShape("> a\n> > [x]: http://e.com\n\n[x]\n"), ["q1:a", "t:x"]) + XCTAssertEqual(try quoteShape("> a\n> > b\n> > > [x]: http://e.com\n\n[x]\n"), ["q1:a", "q2:b", "t:x"]) + XCTAssertEqual(try quoteShape("> | a |\n> |---|\n> | 1 |\n>> [x]: http://e.com\n\n[x]\n").last, "t:x") + XCTAssertEqual(try quoteShape("> > ```\n> > c\n> > ```\n> [x]: http://e.com\n\n[x]\n").last, "t:x") + // Text where GitHub has text. + XCTAssertEqual(try quoteShape(">> a\n> [x]: http://e.com\n\n[x]\n"), ["q2:a\n[x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("> > a\n> b\n> > [x]: http://e.com\n\n[x]\n").last, "t:[x]") + XCTAssertEqual(try quoteShape("> ```\n>> [x]: http://e.com\n> ```\n\n[x]\n").last, "t:[x]") + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) From 7aea0cd89779ba58294037fa516c8d22aae08525 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:30:14 +0100 Subject: [PATCH 090/105] fix(parser): a lazy line after a quote's definition stays in the quote A link definition the pre-pass takes out of a quote opens a paragraph of that quote (CommonMark), so on GitHub a lazy line right after it stays in the quote: `> [y]: url` then `lazy` is a quote "lazy", and under `> a` / `>> [z]: url` the line `code` is the inner quote's. The pre-pass dropped the line, so the block loop never saw that paragraph and the lazy line landed outside the quote or in the outer one. The line now stays in `lines` as a marker without text (definitionLines). The block loop's quote group takes it as an open paragraph of definitions only (QuoteTip.takeDefinition) and draws nothing for it: a group with only markers is no block, and a group starts at its first line of text. The pre-pass follows the same state (DefinitionPlacement.linkDefinition), so more definitions may follow: marked, lazy or with fewer markers. After them `===` is text, as on GitHub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 109 +++++++++++++++--- .../QuickMDTests/VerticalRhythmTests.swift | 37 ++++++ 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 3537341..7b8a681 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -84,6 +84,10 @@ struct MarkdownBlockParser: Sendable { // Headings carry the original line number so section copy can slice the // raw text without re-detecting headings itself. var lineMap: [Int] = [] + // Indices in `lines` of quote lines a link definition was taken out + // of: kept as markers without text, so the block loop knows the + // quote's paragraph they open (`DefinitionPlacement.linkDefinition`). + var definitionLines = Set<Int>() // Fenced code and `$$` blocks, read as the block loop reads them: a // definition inside one is that block's text (GitHub). var verbatim = VerbatimRegions() @@ -114,7 +118,11 @@ struct MarkdownBlockParser: Sendable { let idRange = Range(match.range(at: 1), in: text), let urlRange = Range(match.range(at: 2), in: text) { referenceDefinitions[String(text[idRange]).lowercased()] = String(text[urlRange]) - placement.definition() + if placement.linkDefinition(trimmed) { + lines.append(line) + lineMap.append(originalIndex) + definitionLines.insert(lines.count - 1) + } } else { lines.append(line) lineMap.append(originalIndex) @@ -393,11 +401,20 @@ struct MarkdownBlockParser: Sendable { // A single `>` run can yield SEVERAL blocks (one per nesting // level), so each group needs its OWN first line — the outer // `sourceLine` only describes the first group. - let groupSourceLine = lineMap[i] + // A definition marker (`definitionLines`) opens the + // quote's paragraph for a lazy line and draws nothing: + // the group starts at its first line of text. + var groupSourceLine: Int? let groupLevel = level - var groupLines = [String(scanner)] + var groupLines: [String] = [] var tip = QuoteTip() - tip.take(groupLines[0]) + if definitionLines.contains(i) { + tip.takeDefinition() + } else { + groupLines.append(String(scanner)) + groupSourceLine = lineMap[i] + tip.take(groupLines[0]) + } i += 1 while i < lines.count { @@ -410,6 +427,7 @@ struct MarkdownBlockParser: Sendable { references: referenceDefinitions) else { break } groupLines.append(lines[i]) + groupSourceLine = groupSourceLine ?? lineMap[i] tip.takeLazy() i += 1 continue @@ -435,13 +453,23 @@ struct MarkdownBlockParser: Sendable { htmlImageLine: HTMLImageSyntax.scan(content[...]) != nil, references: referenceDefinitions) else { break } - groupLines.append(content) - tip.takeLazy() + if definitionLines.contains(i) { + tip.takeDefinition() + } else { + groupLines.append(content) + groupSourceLine = groupSourceLine ?? lineMap[i] + tip.takeLazy() + } i += 1 continue } - groupLines.append(String(nextScanner)) - tip.take(groupLines[groupLines.count - 1]) + if definitionLines.contains(i) { + tip.takeDefinition() + } else { + groupLines.append(String(nextScanner)) + groupSourceLine = groupSourceLine ?? lineMap[i] + tip.take(groupLines[groupLines.count - 1]) + } i += 1 } @@ -449,7 +477,7 @@ struct MarkdownBlockParser: Sendable { groupLines = Self.collapsingBlankRuns(groupLines) let content = groupLines.joined(separator: "\n") .trimmingCharacters(in: .whitespacesAndNewlines) - if !content.isEmpty { + if !content.isEmpty, let groupSourceLine { // GFM alert: level-1 quote whose first line is exactly // `[!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / // `[!CAUTION]` (marker alone on the line, per GitHub spec). @@ -1181,11 +1209,24 @@ struct MarkdownBlockParser: Sendable { private var fence: Fence? private var inTable = false private var previousHadPipe = false - - /// Whether the group's next line would start a paragraph: none is - /// open, and it is not in the quote's fence or table — where a link - /// definition may stand (`DefinitionPlacement`). - var admitsDefinition: Bool { fence == nil && !inTable && !endsInParagraph } + /// The open paragraph holds link definitions only, so far: another + /// may follow, and it draws nothing (`takeDefinition`). + private(set) var onlyDefinitions = false + + /// Whether a link definition may stand on the group's next line: it + /// would start a paragraph, or go on with one of definitions only — + /// not in the quote's fence or table (`DefinitionPlacement`). + var admitsDefinition: Bool { fence == nil && !inTable && (!endsInParagraph || onlyDefinitions) } + + /// A link definition the pre-pass took out: it opens (or goes on + /// with) a paragraph of definitions only, which a lazy line continues + /// in this quote (CommonMark, GitHub). + mutating func takeDefinition() { + endsInParagraph = true + onlyDefinitions = true + previousHadPipe = false + inTable = false + } /// Inside the quote's fence. var inFence: Bool { fence != nil } @@ -1196,12 +1237,16 @@ struct MarkdownBlockParser: Sendable { /// other structure, so the paragraph stays open for the next one. mutating func takeLazy() { endsInParagraph = true + onlyDefinitions = false previousHadPipe = false } /// A line of the group itself (marked at the group's level). mutating func take(_ content: String) { let marker = MarkdownBlockParser.markerLine(content) + // Under definitions only, `===` is text (CommonMark). + let afterDefinitions = onlyDefinitions + onlyDefinitions = false if let open = fence { // Inside the quote's fence: only its closing line matters. if MarkdownBlockParser.closesFence(marker, open) { fence = nil } @@ -1238,7 +1283,7 @@ struct MarkdownBlockParser: Sendable { endsInParagraph = false return } - if endsInParagraph, + if endsInParagraph, !afterDefinitions, marker.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil || marker.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { endsInParagraph = false @@ -1324,14 +1369,20 @@ struct MarkdownBlockParser: Sendable { /// a line of the group after no open paragraph. Not lazy text, nor /// in the quote's fence or table. func linkDefinitionText(_ marker: String) -> String? { - guard marker.hasPrefix(">") else { return paragraphOpen ? nil : marker } + guard marker.hasPrefix(">") else { + // A lazy line of a quote paragraph of definitions only: another. + if let tip = quote, tip.endsInParagraph { + return tip.onlyDefinitions && MarkdownBlockParser.startsNoBlock(marker) ? marker : nil + } + return paragraphOpen ? nil : marker + } let (level, content) = Self.quoteMarkers(of: marker) let text = MarkdownBlockParser.markerLine(String(content)) switch step(level: level, content: String(content)) { case .same: return quote?.admitsDefinition == true ? text : nil case .lazy: - return nil + return quote?.onlyDefinitions == true ? text : nil case .newGroup: // A deeper `>` inside the quote's fence is that fence's code. if level > quoteLevel, quote?.inFence == true { return nil } @@ -1339,6 +1390,30 @@ struct MarkdownBlockParser: Sendable { } } + /// A LINK definition taken out of `marker` (a `markerLine`). Out of a + /// quote line, it opens a paragraph of that quote — of definitions + /// only, which draws nothing and which a lazy line continues in the + /// quote (GitHub) — and the line stays for the block loop as a + /// marker: true. A lazy definition after such a paragraph goes on + /// with it; any other definition ends what came before. + mutating func linkDefinition(_ marker: String) -> Bool { + if marker.hasPrefix(">") { + let (level, content) = Self.quoteMarkers(of: marker) + var tip = quote ?? QuoteTip() + if step(level: level, content: String(content)) == .newGroup { + tip = QuoteTip() + quoteLevel = level + } + tip.takeDefinition() + quote = tip + paragraphOpen = true + return true + } + if let tip = quote, tip.endsInParagraph, tip.onlyDefinitions { return false } + definition() + return false + } + /// How a quote line goes on from the group being read — the block /// loop's grouping (lazy: fewer markers, continuing its paragraph). private enum QuoteStep { case same, lazy, newGroup } diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 41517d5..fa764e8 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -836,6 +836,43 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("> ```\n>> [x]: http://e.com\n> ```\n\n[x]\n").last, "t:[x]") } + /// A link definition taken out of a quote opens a paragraph of that + /// quote — of definitions only, which draws nothing — and a lazy line + /// right after it goes on in that quote (GitHub, gfm/h8_*.html). The + /// pre-pass dropped the line, so the lazy line landed outside the quote + /// or in the outer one. More definitions may follow (marked, lazy or at + /// fewer markers); after them `===` is text. Nothing follows: nothing is + /// drawn, as before. + func testALazyLineAfterAQuoteDefinitionStaysInTheQuote() throws { + XCTAssertEqual(try quoteShape("> [y]: http://e.com\nlazy\n\n[y]\n"), ["q1:lazy", "t:y"]) + XCTAssertEqual(try quoteShape("> a\n>\n> [y]: http://e.com\nlazy\n\n[y]\n"), ["q1:a\n\nlazy", "t:y"]) + XCTAssertEqual(try quoteShape("> a\n>> [z]: http://e.com\ncode\n\n[z]\n"), ["q1:a", "q2:code", "t:z"]) + XCTAssertEqual(try quoteShape("> a\n>> [z]: http://e.com\n> b\n\n[z]\n"), ["q1:a", "q2:b", "t:z"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> text\nlazy\n\n[x]\n"), ["q1:text\nlazy", "t:x"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n[y]: http://f.com\nlazy\n\n[x] [y]\n"), + ["q1:lazy", "t:x y"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> [y]: http://f.com\nlazy\n\n[x] [y]\n"), + ["q1:lazy", "t:x y"]) + XCTAssertEqual(try quoteShape("> > [x]: http://e.com\n> [y]: http://f.com\n\n[x] [y]\n"), ["t:x y"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> ===\nlazy\n\n[x]\n"), ["q1:===\nlazy", "t:x"]) + XCTAssertEqual(try quoteShape("> [x]: http://e.com\n> [!NOTE]\n> body\n\n[x]\n"), ["alert:note:body", "t:x"]) + // Nothing follows, or a block does: no quote drawn. + XCTAssertEqual(try quoteShape("> [y]: http://e.com\n\n[y]\n"), ["t:y"]) + XCTAssertEqual(try quoteShape("> [y]: http://e.com\n# H\n\n[y]\n"), ["h:H", "t:y"]) + + // The quote starts at its first drawn line; every later block keeps + // its source line; no definition is drawn. + let markdown = "> a\n>> [z]: http://e.com\ncode\n\n> [y]: http://f.com\nlazy\n\n[z] [y]\n" + let blocks = try XCTUnwrap(parseReturning(markdown)) + XCTAssertEqual(blocks.map(\.sourceLine), [0, 2, 5, 7]) + let theme = MarkdownTheme.cached(for: .light) + let measured = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, + contentWidth: 600, math: nil) + let shown = blocks.compactMap { measured.converted[$0.id]?.string } + XCTAssertEqual(shown, ["a", "code", "lazy", "z y"]) + XCTAssertEqual(blocks.map(\.joinsPrevious), [false, true, false, false]) + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) From 24863d94f4390007392a830edfbaa6672a061629 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:33:43 +0100 Subject: [PATCH 091/105] fix(parser): reference links in headings resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `# [Link text][r]` showed its source and its id was `link-textr` (GitHub: a link, `link-text`); collapsed and shortcut references and footnote references in headings drew their source too. Headings are stored raw and drawn by definition-less renderers (view, measurer, anchors, search, Contents, print), so their references are now inlined at parse time, as quotes, alerts and cells are (ReferenceLinks.inlinedHeading). The title is scanned as one run with its line breaks kept, so a Setext `[Link⏎text][r]` is one link with GitHub's id `linktext`. A footnote reference counts as its ASCII number in the heading's id (FootnoteNumberAttribute): GitHub's slug reads `Note<sup>1</sup> here` as `note1-here`, and the superscript digit drawn here is no word character for the slug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 11 +++- QuickMD/QuickMD/MarkdownRenderer.swift | 24 +++++++- QuickMD/QuickMD/ReferenceLinks.swift | 17 +++++- .../QuickMDTests/SectionExtractorTests.swift | 55 +++++++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index c89e30b..20db539 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -245,7 +245,11 @@ struct MarkdownBlockParser: Sendable { let level = min(max(line[hashRange].count, 1), 6) let title = Self.withoutClosingSequence( String(line[contentRange]).trimmingCharacters(in: .whitespaces)) - blocks.append(.heading(index: blockIndex, level: level, title: title, sourceLine: sourceLine)) + // Headings are rendered later without the definitions (`ReferenceLinks`). + blocks.append(.heading(index: blockIndex, level: level, + title: ReferenceLinks.inlinedHeading(title, definitions: referenceDefinitions, + footnoteIds: footnoteDefinitions.map(\.id)), + sourceLine: sourceLine)) blockIndex += 1 i += 1 continue @@ -342,7 +346,10 @@ struct MarkdownBlockParser: Sendable { var preceding = Array(textBuffer[..<start]) textBuffer.removeAll() flushTextBuffer(&preceding, to: &blocks, index: &blockIndex, pending: &pendingRows) - blocks.append(.heading(index: blockIndex, level: level, title: title, sourceLine: headingLine)) + blocks.append(.heading(index: blockIndex, level: level, + title: ReferenceLinks.inlinedHeading(title, definitions: referenceDefinitions, + footnoteIds: footnoteDefinitions.map(\.id)), + sourceLine: headingLine)) blockIndex += 1 i += 2 continue diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index e77c185..5ad0fce 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -489,8 +489,20 @@ struct MarkdownRenderer: Sendable { /// The text GitHub takes a heading's id from: the rendered title with a /// Setext title's line breaks KEPT — the slug drops them (`Alpha⏎Beta` /// is `alphabeta`), where the space shown would be a `-`. + /// A footnote reference counts as its number in ASCII digits: GitHub's + /// slug reads the heading's text, `Note<sup>1</sup>` is `note1`, and the + /// superscript digit drawn here is not a word character for the slug. func headingAnchorText(_ title: String) -> String { - String(Self.formulaSourceWithEscapes(renderInlineFormatting(title)).characters) + let attributed = Self.formulaSourceWithEscapes(renderInlineFormatting(title)) + var text = "" + for run in attributed.runs { + if let number = run[FootnoteNumberAttribute.self] { + text += String(number) + } else { + text += String(attributed[run.range].characters) + } + } + return text } func renderHeader(_ text: String, level: Int) -> AttributedString { @@ -1261,9 +1273,19 @@ struct MarkdownRenderer: Sendable { let superscript = String(String(number).map { superscriptDigits[$0] ?? $0 }) var attr = AttributedString(superscript) + attr[FootnoteNumberAttribute.self] = number attr.setDualFont(size: scaled(11), bold: true, fonts: theme.fonts) attr.setDualForeground(theme.linkColor) return (attr, afterBracket[afterBracket.index(after: closeIndex)...]) } } + +/// The number a footnote reference draws as superscript digits, for +/// `headingAnchorText` (GitHub's heading id reads it as plain digits). Not +/// part of the AppKit scope, so an installed NSAttributedString never +/// carries it. +enum FootnoteNumberAttribute: AttributedStringKey { + typealias Value = Int + static let name = "qmdFootnoteNumber" +} diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index 2a603e8..4439c6a 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -5,7 +5,8 @@ import Foundation /// Rewrites reference-style links and images (`[text][id]`, `[text][]`, /// `[text]`, `![alt][id]`, `![alt][]`, `![alt]`) whose definition exists into /// their inline form (`[text](<url>)`), for the block kinds whose RAW markdown -/// is stored and rendered later: blockquotes, alerts and table cells. +/// is stored and rendered later: blockquotes, alerts, table cells and +/// headings. /// /// Why rewrite instead of handing those renderers the definitions: paragraphs /// are rendered inside the parser with the document's definitions, but a @@ -73,6 +74,20 @@ enum ReferenceLinks { return out.joined(separator: "\n") } + /// A heading's title with every resolvable reference inlined. Headings + /// are stored as raw Markdown and rendered by definition-less renderers + /// (the view, measurer, anchors, search, Contents, print), like a quote. + /// The title is scanned as ONE run, line breaks included, because that + /// is how `renderHeader` / `headingAnchorText` read a Setext title — a + /// link across its break (`[Link⏎text][r]`) is one link — and the breaks + /// stay, since the anchor drops them where the shown title has a space. + static func inlinedHeading(_ title: String, definitions: [String: String], + footnoteIds: [String] = []) -> String { + guard !definitions.isEmpty || !footnoteIds.isEmpty, title.contains("[") else { return title } + return inlinedLine(title, sources: Sources(definitions: definitions, footnoteIds: footnoteIds), + linksDisabled: false) + } + private static func inlinedLine(_ line: String, sources: Sources, linksDisabled: Bool) -> String { let definitions = sources.definitions guard line.contains("[") else { return line } diff --git a/QuickMD/QuickMDTests/SectionExtractorTests.swift b/QuickMD/QuickMDTests/SectionExtractorTests.swift index bf0a661..fd18069 100644 --- a/QuickMD/QuickMDTests/SectionExtractorTests.swift +++ b/QuickMD/QuickMDTests/SectionExtractorTests.swift @@ -268,6 +268,61 @@ final class SectionExtractorTests: XCTestCase { XCTAssertEqual(headings(of: md).map { $0.shownTitle(renderer: renderer) }, shown) } + /// R18: a FULL reference link in a heading (`# [Link text][r]`) showed + /// its source and its id kept the label (`link-textr`). Headings are + /// stored raw and drawn by definition-less renderers, so references are + /// inlined at parse time (`ReferenceLinks.inlinedHeading`) — the view, + /// a click, the id, search, Contents and print all read the same title. + /// GitHub (`gh api markdown`, 2026-11-04) links every form below, keeps + /// the code span, and its id reads the footnote's `<sup>1</sup>` as `1`. + func testReferenceLinksInHeadingsResolveLikeGitHub() { + let md = """ + # [Link text][r] + + # [Coll][] + + # [Short] + + [Link + text][r] + === + + # Note[^1] here + + # **[Bold][r]** and `[c][r]` + + # [A & B][r] + + [r]: https://e.x/r + [coll]: https://e.x/c + [short]: https://e.x/s + [^1]: Foot. + """ + let theme = MarkdownTheme.cached(for: .light) + let renderer = MarkdownRenderer(theme: theme) + let blocks = MarkdownBlockParser(theme: theme).parse(md) + let rendered = blocks.compactMap { block -> AttributedString? in + guard case .heading(let level, let title, _) = block.content else { return nil } + return renderer.renderHeader(title, level: level) + } + let shown = rendered.map { String($0.characters) } + XCTAssertEqual(shown, ["Link text", "Coll", "Short", "Link text", "Note\u{00B9} here", + "Bold and [c][r]", "A & B"]) + XCTAssertEqual(rendered.map { heading in heading.runs.compactMap { $0.link?.absoluteString } }, [ + ["https://e.x/r"], ["https://e.x/c"], ["https://e.x/s"], ["https://e.x/r"], [], + ["https://e.x/r"], ["https://e.x/r"], + ]) + + let anchors = anchorHeadings(of: md) + XCTAssertEqual(HeadingAnchors.ids(for: anchors.map(\.text)), [ + "link-text", "coll", "short", "linktext", "note1-here", "bold-and-cr", "a--b", + ]) + XCTAssertEqual(headings(of: md).map { $0.shownTitle(renderer: renderer) }, shown) + XCTAssertEqual(SectionExtractor.extractSection(from: md, entry: headings(of: md)[0], + headings: headings(of: md)), "# [Link text][r]", + "copy is the source") + } + func testFragmentFindsItsHeading() { let headings = anchorHeadings(of: """ # Guide From 3e7ed3cd0cc1ae25a6a4095d87fdeefb5101efdf Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:41:51 +0100 Subject: [PATCH 092/105] fix(parser): a footnote definition in a quote is a footnote The definition pre-pass tried the footnote pattern only on the whole line, and on a quote line the `>` comes first. What follows the markers was tried only against the link pattern, so `> [^m]: two` became a reference `^m` to `two` and `[^m]` a link. On GitHub it is footnote 1, "two", in an empty quote. The footnote pattern now runs first on a quote line's text too (DefinitionPlacement.footnoteDefinitionText). A footnote definition there may interrupt a paragraph, as at the top level, but not inside the quote's fence. Its line stays as a marker without text, as a link definition's does (definitionTaken), so a lazy line after it stays in the quote. A footnote definition starts a block (startsNoBlock), so at fewer markers it ends a nested quote rather than continuing it lazily, as on GitHub. `[^]` is no footnote label and remains a link, as on GitHub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 52 +++++++++++++++---- .../QuickMDTests/VerticalRhythmTests.swift | 30 +++++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 7b8a681..96933d6 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -84,9 +84,9 @@ struct MarkdownBlockParser: Sendable { // Headings carry the original line number so section copy can slice the // raw text without re-detecting headings itself. var lineMap: [Int] = [] - // Indices in `lines` of quote lines a link definition was taken out - // of: kept as markers without text, so the block loop knows the - // quote's paragraph they open (`DefinitionPlacement.linkDefinition`). + // Indices in `lines` of quote lines a link or footnote definition was + // taken out of: kept as markers without text, so the block loop knows + // the quote's paragraph they open (`DefinitionPlacement.definitionTaken`). var definitionLines = Set<Int>() // Fenced code and `$$` blocks, read as the block loop reads them: a // definition inside one is that block's text (GitHub). @@ -113,12 +113,24 @@ struct MarkdownBlockParser: Sendable { footnoteDefinitions.append((id: fnId, content: fnContent)) if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } placement.definition() + } else if let text = placement.footnoteDefinitionText(trimmed), + let match = Self.footnoteDefRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), + let idRange = Range(match.range(at: 1), in: text), + let contentRange = Range(match.range(at: 2), in: text) { + // A quote's footnote: before the link pattern here too. + let fnId = String(text[idRange]) + footnoteDefinitions.append((id: fnId, content: String(text[contentRange]))) + if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } + _ = placement.definitionTaken(trimmed) + lines.append(line) + lineMap.append(originalIndex) + definitionLines.insert(lines.count - 1) } else if let text = placement.linkDefinitionText(trimmed), let match = Self.refLinkDefRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), let idRange = Range(match.range(at: 1), in: text), let urlRange = Range(match.range(at: 2), in: text) { referenceDefinitions[String(text[idRange]).lowercased()] = String(text[urlRange]) - if placement.linkDefinition(trimmed) { + if placement.definitionTaken(trimmed) { lines.append(line) lineMap.append(originalIndex) definitionLines.insert(lines.count - 1) @@ -1329,6 +1341,11 @@ struct MarkdownBlockParser: Sendable { if MarkdownRenderer.isThematicBreak(line) || MarkdownRenderer.isListItem(line) { return false } if ["-", "*", "+"].contains(marker) { return false } // an empty item if startsHTMLBlock(marker) { return false } + // A footnote definition starts a block (GFM): `> > a` then + // `> [^m]: note` is the outer quote's footnote, not lazy text. + if footnoteDefRegex.firstMatch(in: marker, range: NSRange(marker.startIndex..., in: marker)) != nil { + return false + } return true } @@ -1390,13 +1407,26 @@ struct MarkdownBlockParser: Sendable { } } - /// A LINK definition taken out of `marker` (a `markerLine`). Out of a - /// quote line, it opens a paragraph of that quote — of definitions - /// only, which draws nothing and which a lazy line continues in the - /// quote (GitHub) — and the line stays for the block loop as a - /// marker: true. A lazy definition after such a paragraph goes on - /// with it; any other definition ends what came before. - mutating func linkDefinition(_ marker: String) -> Bool { + /// What a FOOTNOTE definition on a quote line (its `markerLine`) + /// would be read from: what follows its markers. A footnote + /// definition may interrupt a paragraph (GitHub), so anywhere in the + /// quote but in its fence. Nil for any other line: the pre-pass + /// reads a line without markers whole. + func footnoteDefinitionText(_ marker: String) -> String? { + guard marker.hasPrefix(">") else { return nil } + let (level, content) = Self.quoteMarkers(of: marker) + if let tip = quote, tip.inFence, level >= quoteLevel { return nil } + return MarkdownBlockParser.markerLine(String(content)) + } + + /// A link or footnote definition taken out of `marker` (a + /// `markerLine`). Out of a quote line, it opens a paragraph of that + /// quote — of definitions only, which draws nothing and which a lazy + /// line continues in the quote (GitHub) — and the line stays for the + /// block loop as a marker: true. A lazy link definition after such a + /// paragraph goes on with it; any other definition ends what came + /// before. + mutating func definitionTaken(_ marker: String) -> Bool { if marker.hasPrefix(">") { let (level, content) = Self.quoteMarkers(of: marker) var tip = quote ?? QuoteTip() diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index fa764e8..89cec7f 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -873,6 +873,36 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(blocks.map(\.joinsPrevious), [false, true, false, false]) } + /// A FOOTNOTE definition on a quote line is a footnote (GitHub, + /// gfm/h9_*.html): in a quote, a nested or deeper one, after an alert's + /// body, after the quote's text (it may interrupt a paragraph, as at the + /// top level). The pre-pass tried only the LINK pattern on what follows + /// the markers, so `> [^m]: two` became a reference `^m` to `two` and + /// `[^m]` a link. Like a link definition (H8) it leaves a marker that + /// draws nothing and keeps a lazy line in the quote; at fewer markers it + /// is never lazy (it starts a block); in the quote's fence it is code. + func testAFootnoteDefinitionInAQuoteIsAFootnote() throws { + let note = "t:────────────────────────────────\n1. two in quote" + XCTAssertEqual(try quoteShape("x [^m]\n\n> [^m]: two in quote\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> > [^m]: two in quote\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> a\n>> [^m]: two in quote\n"), ["t:x ¹", "q1:a", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> [!NOTE]\n> body\n>\n> [^m]: two in quote\n"), + ["t:x ¹", "alert:note:body", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> text\n> [^m]: two in quote\n"), ["t:x ¹", "q1:text", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> [^m]: two in quote\nlazy\n"), ["t:x ¹", "q1:lazy", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> [^m]: two in quote\n>\n> after\n"), ["t:x ¹", "q1:after", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> > a\n> [^m]: two in quote\nlazy\n"), + ["t:x ¹", "q2:a", "q1:lazy", note]) + // In the quote's fence it is code. + XCTAssertEqual(try quoteShape("x [^m]\n\n> ```\n> [^m]: two in quote\n> ```\n"), + ["t:x [^m]", "q1:```\n[^m]: two in quote\n```"]) + // `[^]` is no footnote label: a link, as on GitHub (h9_caret_empty). + XCTAssertEqual(try quoteShape("[^]: http://e.com\n\n[^]\n"), ["t:^"]) + // The quote's first drawn line is its source line; the marker is the footnote's. + let blocks = try XCTUnwrap(parseReturning("x [^m]\n\n> [^m]: two in quote\nlazy\n")) + XCTAssertEqual(blocks.map(\.sourceLine).prefix(2), [0, 3]) + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) From 12f9b33c7f2196f3f327e73581cd2c6d69cf247e Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:41:55 +0100 Subject: [PATCH 093/105] fix(parser): reference labels match as CommonMark's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A label across a Setext break (`[link⏎text]`), with extra spaces or a tab (`[a b]`, `[A⇥B]`) or in another case form (`[ẞ]` for `[SS]:`, `[straße]` for `[STRASSE]:`) stayed literal; GitHub links them all. ReferenceLabel.normalized is now the one key every lookup uses (the definition pre-pass, the renderer, InlineSyntaxScanner, ReferenceLinks): whitespace runs (cmark's set: a no-break space is not one) become one space, the ends are trimmed, and the label is Unicode case-folded. The first definition of a label now wins, as on GitHub; the last one did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 8 ++--- QuickMD/QuickMD/MarkdownBlockParser.swift | 8 +++-- QuickMD/QuickMD/MarkdownRenderer.swift | 4 +-- QuickMD/QuickMD/ReferenceLinks.swift | 35 ++++++++++++++++-- QuickMD/QuickMDTests/ParserTests.swift | 43 +++++++++++++++++++++++ 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index d22c3ee..6c804d7 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -238,9 +238,9 @@ enum InlineSyntaxScanner { guard let labelClose = after.dropFirst().firstIndex(of: "]"), text.index(after: labelClose) == text.endIndex else { return nil } let label = String(after[after.index(after: after.startIndex)..<labelClose]) - guard references[(label.isEmpty ? String(linkText) : label).lowercased()] != nil else { return nil } + guard references[ReferenceLabel.normalized(label.isEmpty ? String(linkText) : label)] != nil else { return nil } } else { - guard after.isEmpty, references[linkText.lowercased()] != nil else { return nil } + guard after.isEmpty, references[ReferenceLabel.normalized(linkText)] != nil else { return nil } } return ImageSyntax(alt: image.alt, url: image.url, end: line.endIndex) } @@ -520,13 +520,13 @@ enum InlineSyntaxScanner { if next < bytes.count, bytes[next] == UInt8(ascii: "["), let labelClose = matchLabel(bytes, open: next) { let labelRange = labelClose == next + 1 ? altRange : (next + 1)..<labelClose - guard let url = references[string(bytes, labelRange).lowercased()] else { return nil } + guard let url = references[ReferenceLabel.normalized(string(bytes, labelRange))] else { return nil } return (string(bytes, altRange), url, labelClose + 1) } // Shortcut reference: ![alt] let alt = string(bytes, altRange) - guard let url = references[alt.lowercased()] else { return nil } + guard let url = references[ReferenceLabel.normalized(alt)] else { return nil } return (alt, url, next) } } diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 20db539..a5bbbe5 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -114,8 +114,12 @@ struct MarkdownBlockParser: Sendable { let idRange = Range(match.range(at: 1), in: text), let urlRange = Range(match.range(at: 2), in: text) { // Character references in the destination are decoded, as in - // an inline link's (`InlineSyntaxScanner.unescaped`). - referenceDefinitions[String(text[idRange]).lowercased()] = HTMLEntities.decode(String(text[urlRange])) + // an inline link's (`InlineSyntaxScanner.unescaped`). The FIRST + // definition of a label wins (CommonMark, GitHub). + let label = ReferenceLabel.normalized(text[idRange]) + if referenceDefinitions[label] == nil { + referenceDefinitions[label] = HTMLEntities.decode(String(text[urlRange])) + } placement.definition() } else { lines.append(line) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 5ad0fce..1bd9934 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1043,7 +1043,7 @@ struct MarkdownRenderer: Sendable { if afterSecondOpen < remaining.endIndex, let closeSecondBracket = remaining[afterSecondOpen...].firstIndex(of: "]") { let refId = String(remaining[afterSecondOpen..<closeSecondBracket]) - let effectiveId = (refId.isEmpty ? linkText : refId).lowercased() + let effectiveId = ReferenceLabel.normalized(refId.isEmpty ? linkText : refId) if let url = referenceDefinitions[effectiveId] { return makeLink(text: linkText, url: url, remaining: remaining[remaining.index(after: closeSecondBracket)...]) @@ -1052,7 +1052,7 @@ struct MarkdownRenderer: Sendable { } // 3. Shortcut reference: [text] where text matches a definition ID - if let url = referenceDefinitions[linkText.lowercased()] { + if let url = referenceDefinitions[ReferenceLabel.normalized(linkText)] { return makeLink(text: linkText, url: url, remaining: remaining[afterBracket...]) } diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index 4439c6a..c7e276e 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -1,5 +1,36 @@ import Foundation +// MARK: - Reference labels + +/// CommonMark's label matching, in ONE place for every lookup (the +/// definition pre-pass that stores, the renderer, `InlineSyntaxScanner`, +/// `ReferenceLinks`): runs of whitespace — line breaks included, as in a +/// Setext title's `[link⏎text]` — become one space, the ends are trimmed, +/// and the label is Unicode case-folded, not lowercased: GitHub (cmark's +/// `utf8proc_case_fold`) matches `[ẞ]` to `[SS]:` and `[straße]` to +/// `[STRASSE]:` (`gh api markdown`, 2026-11-04). Whitespace is cmark's +/// (space, tab, line feed, line tabulation, form feed, return), so a +/// no-break space stays a character of the label. +enum ReferenceLabel { + static func normalized<S: StringProtocol>(_ label: S) -> String { + var collapsed = "" + var pendingSpace = false + var ascii = true + for scalar in label.unicodeScalars { + switch scalar { + case " ", "\t", "\n", "\u{0B}", "\u{0C}", "\r": + pendingSpace = !collapsed.isEmpty + default: + if pendingSpace { collapsed.append(" "); pendingSpace = false } + if !scalar.isASCII { ascii = false } + collapsed.unicodeScalars.append(scalar) + } + } + // ASCII labels (nearly all): lowercasing IS the fold, no bridging. + return ascii ? collapsed.lowercased() : collapsed.folding(options: .caseInsensitive, locale: nil) + } +} + // MARK: - Reference links inside quotes, alerts and table cells /// Rewrites reference-style links and images (`[text][id]`, `[text][]`, @@ -186,7 +217,7 @@ enum ReferenceLinks { if afterSecondOpen < remaining.endIndex, let closeSecondBracket = remaining[afterSecondOpen...].firstIndex(of: "]") { let refId = String(remaining[afterSecondOpen..<closeSecondBracket]) - let effectiveId = (refId.isEmpty ? linkText : refId).lowercased() + let effectiveId = ReferenceLabel.normalized(refId.isEmpty ? linkText : refId) if let url = definitions[effectiveId] { return resolved(url, rest: remaining[remaining.index(after: closeSecondBracket)...]) } @@ -194,7 +225,7 @@ enum ReferenceLinks { } // Shortcut reference: [text] - if let url = definitions[linkText.lowercased()] { + if let url = definitions[ReferenceLabel.normalized(linkText)] { return resolved(url, rest: remaining[afterBracket...]) } return nil diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index ee1f5bc..570266a 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -648,6 +648,49 @@ final class ParserTests: XCTestCase { XCTAssertFalse(quote.contains("\n\n\n"), "one paragraph break per blank run") } + /// R19: reference labels match as CommonMark's (`ReferenceLabel`) in + /// every lookup — whitespace runs (tab, line break) are one space, the + /// ends trimmed, Unicode case folding (`ẞ` = `SS`, `straße` = `STRASSE`, + /// `ΣΑΣ` = `σας`); a no-break space is no whitespace; the first + /// definition of a label wins. Expected links from `gh api markdown` + /// (2026-11-04). Lowercasing alone and exact whitespace left all of + /// these but `[dup]` literal, and `[dup]` took the LAST definition. + func testReferenceLabelsMatchLikeGitHub() { + let md = "Use [link\ntext] shortcut\n---\n\n" + + "[foo bar] [a\u{00A0}b] [dup] [x][ΣΑΣ] [y][a\n B]\n\n" + + "> [straße] and [A\tB]\n\n" + + "| c |\n|---|\n| [a b] |\n\n" + + "![ẞ]\n\n" + + "[ Foo Bar ]: https://fb\n[a b]: https://ab\n[dup]: https://first\n[DUP]: https://second\n" + + "[SS]: https://ss.png\n[STRASSE]: https://strasse\n[σας]: https://sas\n[link text]: https://lt\n" + let blocks = parse(md) + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) + func links(_ text: AttributedString) -> [String] { + text.runs.compactMap { run in + run.link.map { "\(String(text[run.range].characters))=\($0.absoluteString)" } + } + } + guard blocks.count == 5, + case .heading(let level, let title, _) = blocks[0].content, + case .text(let paragraph) = blocks[1].content, + case .blockquote(let quote, _) = blocks[2].content, + case .table(_, let rows, _, _) = blocks[3].content, + case .image(let url, let alt, _) = blocks[4].content else { + return XCTFail("blocks: \(blocks.map(\.content))") + } + XCTAssertEqual(links(renderer.renderHeader(title, level: level)), ["link text=https://lt"]) + XCTAssertEqual(links(paragraph), ["foo bar=https://fb", "dup=https://first", "x=https://sas", + "y=https://ab"]) + XCTAssertTrue(String(paragraph.characters).contains("[a\u{00A0}b]"), "a no-break space is no whitespace") + XCTAssertEqual(links(renderer.renderQuotedBody(quote)), ["straße=https://strasse", "A\tB=https://ab"]) + XCTAssertEqual(links(renderer.renderInline(rows[0][0])), ["a b=https://ab"]) + XCTAssertEqual(url, "https://ss.png") + XCTAssertEqual(alt, "ẞ") + + XCTAssertEqual(ReferenceLabel.normalized(" \tFoo \n\u{0C} BAR\r "), "foo bar") + XCTAssertEqual(ReferenceLabel.normalized("ff İ DŽ"), ReferenceLabel.normalized("FF i\u{0307} dž")) + } + /// The rewrite must draw exactly what the paragraph renderer (which HAS the /// definitions) draws for the same line: characters and links. func testReferenceInliningMatchesTheDefinitionAwareRenderer() { From cc694e678a297f000283c55ae96c77bef0b21699 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:50:33 +0100 Subject: [PATCH 094/105] fix(parser): an alert needs a body On GitHub `> [!NOTE]` alone is a plain quote "[!NOTE]", and so is a marker followed only by blank `>` lines or by definitions the pre-pass takes out. We drew an alert with an empty box. A body is any block after the marker: text, a lazy line, a list, a heading, or a nested quote right after it, even an empty one. The quote branch now makes a level-1 group an alert only with a body or a nested quote following; otherwise it is a quote. ParserTests and BlockHeightMeasurerTests pinned the header-only alert. Their fixtures now give the alert an empty nested quote, and the marker alone is asserted to be a quote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 14 +++++++--- .../BlockHeightMeasurerTests.swift | 6 +++-- QuickMD/QuickMDTests/ParserTests.swift | 11 ++++++-- .../QuickMDTests/VerticalRhythmTests.swift | 26 ++++++++++++++++++- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 96933d6..d413131 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -492,10 +492,16 @@ struct MarkdownBlockParser: Sendable { if !content.isEmpty, let groupSourceLine { // GFM alert: level-1 quote whose first line is exactly // `[!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / - // `[!CAUTION]` (marker alone on the line, per GitHub spec). - if groupLevel == 1, let kind = AlertKind(markerLine: groupLines[0]) { - let body = groupLines.dropFirst().joined(separator: "\n") - .trimmingCharacters(in: .whitespacesAndNewlines) + // `[!CAUTION]` (marker alone on the line, per GitHub spec) + // — and that has a body (GitHub): text after the marker, + // or a nested quote right after it, even an empty one. + // The marker alone (blank lines, definitions taken out) + // is a plain quote "[!NOTE]". + let alertKind = groupLevel == 1 ? AlertKind(markerLine: groupLines[0]) : nil + let body = groupLines.dropFirst().joined(separator: "\n") + .trimmingCharacters(in: .whitespacesAndNewlines) + let nestedQuoteFollows = i < lines.count && Self.markerLine(lines[i]).hasPrefix(">") + if let kind = alertKind, !body.isEmpty || nestedQuoteFollows { blocks.append(.alert(index: blockIndex, kind: kind, content: body, sourceLine: groupSourceLine)) previousGroupWasQuote = false } else { diff --git a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift index d2230f5..b3d38b9 100644 --- a/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift +++ b/QuickMD/QuickMDTests/BlockHeightMeasurerTests.swift @@ -205,9 +205,11 @@ final class BlockHeightMeasurerTests: XCTestCase { } } - /// An alert with a marker but no body skips the body and its VStack spacing. + /// An alert with a marker but no body text skips the body and its VStack + /// spacing. (Its body is an empty nested quote: the marker alone is a + /// plain quote, as on GitHub.) func testAlertWithoutBodyHasNoBodyChrome() { - let blocks = parse("> [!NOTE]") + let blocks = parse("> [!NOTE]\n> >") let measured = measure(blocks, width: 600) guard let index = blocks.firstIndex(where: { if case .alert = $0.content { return true } else { return false } }) else { return XCTFail("fixture did not parse as an alert") diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 2b0f9e3..6f06fab 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -665,10 +665,17 @@ final class ParserTests: XCTestCase { XCTAssertEqual(level, 2) } + /// An alert needs a body (GitHub): the marker alone is a plain quote; a + /// nested quote right after it is a body, even an empty one, and the + /// alert's own body is then empty. func testAlertWithEmptyBodyParses() { - let blocks = parse("> [!WARNING]") + guard case .blockquote(let marker, 1) = parse("> [!WARNING]").first?.content else { + return XCTFail("a marker alone is a plain quote") + } + XCTAssertEqual(marker, "[!WARNING]") + let blocks = parse("> [!WARNING]\n> >") guard case .alert(let kind, let content) = blocks.first?.content else { - return XCTFail("header-only alert should still parse") + return XCTFail("an alert whose body is a nested quote should still parse") } XCTAssertEqual(kind, .warning) XCTAssertTrue(content.isEmpty) diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 89cec7f..11d0370 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -903,6 +903,30 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(blocks.map(\.sourceLine).prefix(2), [0, 3]) } + /// An alert needs a body (GitHub, gfm/h10_*.html): `> [!NOTE]` alone, + /// or followed only by blank `>` lines or definitions the pre-pass took + /// out, is a plain quote "[!NOTE]" — it was an alert with an empty box. + /// Any block after the marker is a body: text, a lazy line, a list, a + /// heading, a nested quote (even an empty one). + func testAnAlertNeedsABody() throws { + XCTAssertEqual(try quoteShape("> [!NOTE]\n"), ["q1:[!NOTE]"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n>\n"), ["q1:[!NOTE]"]) + XCTAssertEqual(try quoteShape("> [!TIP]\n\ntext\n"), ["q1:[!TIP]", "t:text"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n>\n> [x]: http://e.com\n\n[x]\n"), ["q1:[!NOTE]", "t:x"]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> [!NOTE]\n> [^m]: note\n").prefix(2), ["t:x ¹", "q1:[!NOTE]"]) + // A body: an alert. + XCTAssertEqual(try quoteShape("> [!NOTE]\n> text\n"), ["alert:note:text"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n>\n> text\n"), ["alert:note:text"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\nlazy\n"), ["alert:note:lazy"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> - item\n"), ["alert:note:- item"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> # H\n"), ["alert:note:# H"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> [x]: http://e.com\n\n[x]\n"), + ["alert:note:[x]: http://e.com", "t:[x]"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> > nested\n"), ["alert:note:", "q2:nested"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n> >\n"), ["alert:note:"]) + XCTAssertEqual(try quoteShape("> [!NOTE]\n>> [x]: http://e.com\n\n[x]\n"), ["alert:note:", "t:x"]) + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) @@ -1028,7 +1052,7 @@ final class VerticalRhythmTests: XCTestCase { func testAWidthChangeLaysAQuoteOutAgainWithoutRenderingIt() throws { let long = "words that wrap at a narrow column but not at a wide one, and a few more of them" let markdown = "Para\n\n> \(long)\nand a lazy line\n\n> > nested \(long)\n\n" - + "> [!NOTE]\n> \(long)\n\n> [!TIP]\n\nEnd\n" + + "> [!NOTE]\n> \(long)\n\n> [!TIP]\n> >\n\nEnd\n" let blocks = try XCTUnwrap(parseReturning(markdown)) let theme = MarkdownTheme.cached(for: .light) let first = BlockHeightMeasurer.measure(blocks: blocks, theme: theme, fontScale: 1, From ec52f1388e469eee009daa25c30b1b4c8f27d2a3 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Wed, 4 Nov 2026 23:52:23 +0100 Subject: [PATCH 095/105] fix(parser): a footnote label is GitHub's The footnote definition pattern took any label and wanted a space after the colon. cmark-gfm's label is one or more characters other than `]`, spaces, tabs and line breaks. A no-break space, `^`, `[`, `\` and other punctuation are allowed. The text may follow the colon directly, or be empty. So on GitHub `[^ m]: url` and `[^a b]: url` are LINK definitions, which we read as footnotes, and `[^m]:note` is a footnote, which we read as neither. One pattern serves every level (top level, quotes, startsNoBlock), and the footnote pattern is still tried before the link pattern (constraint 6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownTheme.swift | 5 +++- .../QuickMDTests/VerticalRhythmTests.swift | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/MarkdownTheme.swift b/QuickMD/QuickMD/MarkdownTheme.swift index 433d48c..a1b7a91 100644 --- a/QuickMD/QuickMD/MarkdownTheme.swift +++ b/QuickMD/QuickMD/MarkdownTheme.swift @@ -318,7 +318,10 @@ struct MarkdownTheme: Sendable, Identifiable { /// Matches footnote definitions: [^id]: text /// Captures: (1) footnote ID, (2) content - static let footnoteDefinitionPattern = #"^[ \t]*\[\^([^\]]+)\]:[ \t]+(.*)"# + /// The label is cmark-gfm's: one or more characters but `]`, spaces, + /// tabs and line breaks (`[^ m]: url` is a LINK definition on GitHub); + /// the text may follow the colon directly, or be empty. + static let footnoteDefinitionPattern = #"^[ \t]*\[\^([^\] \t\r\n]+)\]:[ \t]*(.*)"# // MARK: - URL Autolinking diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 11d0370..3e06ed3 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -927,6 +927,32 @@ final class VerticalRhythmTests: XCTestCase { XCTAssertEqual(try quoteShape("> [!NOTE]\n>> [x]: http://e.com\n\n[x]\n"), ["alert:note:", "t:x"]) } + /// A footnote label is cmark-gfm's: one or more characters but `]`, + /// spaces, tabs and line breaks (a no-break space, `^`, `[`, `\` and + /// punctuation are fine); the text may follow the colon directly, or be + /// empty (GitHub, gfm/h11_*.html). The pattern took any label, so + /// `[^ m]: url` was a footnote where GitHub reads a LINK definition, and + /// it needed a space after the colon, so `[^m]:note` was neither. At + /// every level, footnote before link (constraint 6). + func testAFootnoteLabelIsGitHubs() throws { + let note = "t:────────────────────────────────\n1. note" + // Whitespace in the label: a link definition. + XCTAssertEqual(try quoteShape("x [^ m]\n\n[^ m]: http://e.com\n"), ["t:x ^ m"]) + XCTAssertEqual(try quoteShape("x [^a b]\n\n[^a b]: http://e.com\n"), ["t:x ^a b"]) + XCTAssertEqual(try quoteShape("x [^a\tb]\n\n[^a\tb]: http://e.com\n"), ["t:x ^a\tb"]) + XCTAssertEqual(try quoteShape("x [^ m]\n\n> [^ m]: http://e.com\n"), ["t:x ^ m"]) + XCTAssertEqual(try quoteShape("x [^]\n\n[^]: http://e.com\n"), ["t:x ^"]) + // Footnotes. + XCTAssertEqual(try quoteShape("x [^m]\n\n[^m]:note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n> [^m]:note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^m]\n\n[^m]:\n").first, "t:x ¹") + XCTAssertEqual(try quoteShape("x [^a\u{00A0}b]\n\n[^a\u{00A0}b]: note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^a.b-c_d]\n\n[^a.b-c_d]: note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^a^b]\n\n[^a^b]: note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^a\\b]\n\n[^a\\b]: note\n"), ["t:x ¹", note]) + XCTAssertEqual(try quoteShape("x [^ä€]\n\n[^ä€]: note\n"), ["t:x ¹", note]) + } + func testALinkDefinitionCannotInterruptAParagraph() throws { XCTAssertEqual(try quoteShape("> a\n[x]: http://e.com\n\n[x]\n"), ["q1:a\n[x]: http://e.com", "t:[x]"]) XCTAssertEqual(try quoteShape("Para\n[x]: http://e.com\n\n[x]\n"), ["t:Para [x]: http://e.com", "t:[x]"]) From 4d00241a721c11466105f5dad8c634eca42c9b9e Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:01:01 +0100 Subject: [PATCH 096/105] fix(parser): footnote labels match as reference labels do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[^Note]` and `[^NOTE]` stayed literal with only `[^note]:` defined, and `[^ẞ]` did not find `[^ss]:`; GitHub links them all to the footnote. Footnote ids are now stored as ReferenceLabel.normalized and looked up the same way by the renderer and ReferenceLinks (quotes, alerts, cells, headings). The label grammar stays cmark-gfm's (H11). The first definition of an id wins and a later one is not listed, as on GitHub; both were listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 20 +++++++----- QuickMD/QuickMD/MarkdownRenderer.swift | 3 +- QuickMD/QuickMD/ReferenceLinks.swift | 3 +- QuickMD/QuickMDTests/ParserTests.swift | 38 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 706fe43..37f63b7 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -108,19 +108,25 @@ struct MarkdownBlockParser: Sendable { if let match = Self.footnoteDefRegex.firstMatch(in: trimmed, range: nsRange), let idRange = Range(match.range(at: 1), in: trimmed), let contentRange = Range(match.range(at: 2), in: trimmed) { - let fnId = String(trimmed[idRange]) - let fnContent = String(trimmed[contentRange]) - footnoteDefinitions.append((id: fnId, content: fnContent)) - if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } + // Ids are normalised labels (`ReferenceLabel`): `[^Note]` finds + // `[^note]:`. The FIRST definition of an id wins (GitHub + // drops a later one from the list too). + let fnId = ReferenceLabel.normalized(trimmed[idRange]) + if !footnoteIds.contains(fnId) { + footnoteDefinitions.append((id: fnId, content: String(trimmed[contentRange]))) + footnoteIds.append(fnId) + } placement.definition() } else if let text = placement.footnoteDefinitionText(trimmed), let match = Self.footnoteDefRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), let idRange = Range(match.range(at: 1), in: text), let contentRange = Range(match.range(at: 2), in: text) { // A quote's footnote: before the link pattern here too. - let fnId = String(text[idRange]) - footnoteDefinitions.append((id: fnId, content: String(text[contentRange]))) - if !footnoteIds.contains(fnId) { footnoteIds.append(fnId) } + let fnId = ReferenceLabel.normalized(text[idRange]) + if !footnoteIds.contains(fnId) { + footnoteDefinitions.append((id: fnId, content: String(text[contentRange]))) + footnoteIds.append(fnId) + } _ = placement.definitionTaken(trimmed) lines.append(line) lineMap.append(originalIndex) diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 1bd9934..0708988 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1255,7 +1255,8 @@ struct MarkdownRenderer: Sendable { // the number a quote / alert / table cell already carries // (`ReferenceLinks`: those are rendered without the definitions). let number: Int - if let idx = footnoteDefinitions.firstIndex(where: { $0.id == fnId }) { + let key = ReferenceLabel.normalized(fnId) // ids are stored normalised + if let idx = footnoteDefinitions.firstIndex(where: { $0.id == key }) { number = idx + 1 } else if fnId.unicodeScalars.first == Self.numberedFootnoteMarker, let numbered = Int(String(fnId.unicodeScalars.dropFirst())), numbered > 0 { diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index c7e276e..f851f36 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -237,7 +237,8 @@ enum ReferenceLinks { let afterBracket = remaining.dropFirst(2) guard let close = afterBracket.firstIndex(of: "]") else { return nil } let id = String(afterBracket[..<close]) - guard !id.isEmpty, let index = sources.footnoteIds.firstIndex(of: id) else { return nil } + guard !id.isEmpty, + let index = sources.footnoteIds.firstIndex(of: ReferenceLabel.normalized(id)) else { return nil } return ("[^\(MarkdownRenderer.numberedFootnoteMarker)\(index + 1)]", afterBracket[afterBracket.index(after: close)...]) } diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index b8efe51..46a01d7 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -679,6 +679,44 @@ final class ParserTests: XCTestCase { XCTAssertTrue(String(notes.characters).contains("1. Quoted note.\n2. Second."), String(notes.characters)) } + /// R21a: footnote labels match like link labels (`ReferenceLabel`): + /// `[^Note]`, `[^NOTE]` and `[^note]` are all footnote 1, `[^ẞ]` finds + /// `[^ss]:`, and of `[^dup]:` / `[^DUP]:` the first wins — the second is + /// not listed either. GitHub (`gh api markdown`, 2026-11-04). They stayed + /// literal (an exact `==`), and both duplicates were listed. + func testFootnoteLabelsMatchLikeGitHub() { + let md = """ + # H[^Note] + + A[^Note] B[^NOTE] C[^note] D[^Dup] E[^ẞ] + + > Q[^NOTE] [^DUP] + + | c | + |---| + | T[^NoTe] | + + [^note]: First. + [^dup]: Dup one. + [^DUP]: Dup two. + [^ss]: Fold. + """ + let blocks = parse(md) + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) + guard case .heading(let level, let title, _) = blocks[0].content, + case .text(let paragraph) = blocks[1].content, + case .blockquote(let quote, _) = blocks[2].content, + case .table(_, let rows, _, _) = blocks[3].content, + case .text(let notes) = blocks.last?.content else { return XCTFail("blocks: \(blocks.map(\.content))") } + XCTAssertEqual(String(renderer.renderHeader(title, level: level).characters), "H\u{00B9}") + XCTAssertEqual(String(paragraph.characters), "A\u{00B9} B\u{00B9} C\u{00B9} D\u{00B2} E\u{00B3}") + XCTAssertEqual(String(renderer.renderQuotedBody(quote).characters), "Q\u{00B9} \u{00B2}") + XCTAssertEqual(String(renderer.renderInline(rows[0][0]).characters), "T\u{00B9}") + let listed = String(notes.characters) + XCTAssertTrue(listed.hasSuffix("1. First.\n2. Dup one.\n3. Fold."), listed) + XCTAssertFalse(listed.contains("Dup two"), listed) + } + /// R19: reference labels match as CommonMark's (`ReferenceLabel`) in /// every lookup — whitespace runs (tab, line break) are one space, the /// ends trimmed, Unicode case folding (`ẞ` = `SS`, `straße` = `STRASSE`, From 002ce92763ae62e2bee1b33dc641693eb8aa7620 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:02:16 +0100 Subject: [PATCH 097/105] fix(parser): a whitespace-only label defines nothing `[ ]: https://empty` was taken out as a definition of the empty label; CommonMark and GitHub keep the line as paragraph text, and `[ ]` / `[x][ ]` stay literal. The pre-pass now rejects a label that ReferenceLabel.normalized makes empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 3 +++ QuickMD/QuickMDTests/ParserTests.swift | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 37f63b7..e36d147 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -134,6 +134,9 @@ struct MarkdownBlockParser: Sendable { } else if let text = placement.linkDefinitionText(trimmed), let match = Self.refLinkDefRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)), let idRange = Range(match.range(at: 1), in: text), + // A label of whitespace only defines nothing (CommonMark): + // the line is text. + !ReferenceLabel.normalized(text[idRange]).isEmpty, let urlRange = Range(match.range(at: 2), in: text) { // Character references in the destination are decoded, as in // an inline link's (`InlineSyntaxScanner.unescaped`). The FIRST diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 46a01d7..acc9cc2 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -717,6 +717,22 @@ final class ParserTests: XCTestCase { XCTAssertFalse(listed.contains("Dup two"), listed) } + /// R21b: a label of whitespace only (`[ ]:`, `[\t]:`) defines nothing — + /// the line is paragraph text and `[ ]` / `[x][ ]` stay literal (GitHub, + /// `gh api markdown`, 2026-11-04). It was taken out as a definition of "". + func testWhitespaceOnlyLabelDefinesNothing() { + let blocks = parse("[ ] and [x][ ] and [y][\t]\n\n[ ]: https://empty\n\n[\t]: https://tab\n") + let texts = blocks.compactMap { block -> AttributedString? in + guard case .text(let text) = block.content else { return nil } + return text + } + XCTAssertEqual(texts.map { String($0.characters) }, + ["[ ] and [x][ ] and [y][\t]", "[ ]: https://empty", "[\t]: https://tab"]) + // Only the bare URLs link (autolinks, as on GitHub). + XCTAssertEqual(texts.map { text in text.runs.compactMap { $0.link?.absoluteString } }, + [[], ["https://empty"], ["https://tab"]]) + } + /// R19: reference labels match as CommonMark's (`ReferenceLabel`) in /// every lookup — whitespace runs (tab, line break) are one space, the /// ends trimmed, Unicode case folding (`ẞ` = `SS`, `straße` = `STRASSE`, From 54d8463ed1f6923e87a24fea5be94ab6edd211be Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:06:18 +0100 Subject: [PATCH 098/105] fix(parser): escaped brackets in reference labels `[a\]b]: https://esc` was not a definition (the pattern stopped at the first `]`), and `[y][a\]b]` read the label `a\`. GitHub links both, the label matched as written. The definition pattern now takes escaped characters and no unescaped bracket (`[a[b]:` stays text, as on GitHub), and the full form's label ends at the first unescaped `]` with no unescaped `[` inside (ReferenceLabel.closingBracket) in the renderer, ReferenceLinks and the linked-image line, the rule InlineSyntaxScanner.matchLabel already used for images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/InlineSyntaxScanner.swift | 2 +- QuickMD/QuickMD/MarkdownRenderer.swift | 2 +- QuickMD/QuickMD/MarkdownTheme.swift | 4 +- QuickMD/QuickMD/ReferenceLinks.swift | 28 ++++++++++++- QuickMD/QuickMDTests/ParserTests.swift | 51 +++++++++++++++++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) diff --git a/QuickMD/QuickMD/InlineSyntaxScanner.swift b/QuickMD/QuickMD/InlineSyntaxScanner.swift index 6c804d7..ad77c65 100644 --- a/QuickMD/QuickMD/InlineSyntaxScanner.swift +++ b/QuickMD/QuickMD/InlineSyntaxScanner.swift @@ -235,7 +235,7 @@ enum InlineSyntaxScanner { guard let destination = parenthesizedDestination(after, pairs: pairs), destination.end == text.endIndex else { return nil } } else if after.hasPrefix("[") { - guard let labelClose = after.dropFirst().firstIndex(of: "]"), + guard let labelClose = ReferenceLabel.closingBracket(in: after, from: after.index(after: after.startIndex)), text.index(after: labelClose) == text.endIndex else { return nil } let label = String(after[after.index(after: after.startIndex)..<labelClose]) guard references[ReferenceLabel.normalized(label.isEmpty ? String(linkText) : label)] != nil else { return nil } diff --git a/QuickMD/QuickMD/MarkdownRenderer.swift b/QuickMD/QuickMD/MarkdownRenderer.swift index 0708988..5a668cb 100644 --- a/QuickMD/QuickMD/MarkdownRenderer.swift +++ b/QuickMD/QuickMD/MarkdownRenderer.swift @@ -1041,7 +1041,7 @@ struct MarkdownRenderer: Sendable { if remaining[afterBracket...].hasPrefix("[") { let afterSecondOpen = remaining.index(after: afterBracket) if afterSecondOpen < remaining.endIndex, - let closeSecondBracket = remaining[afterSecondOpen...].firstIndex(of: "]") { + let closeSecondBracket = ReferenceLabel.closingBracket(in: remaining, from: afterSecondOpen) { let refId = String(remaining[afterSecondOpen..<closeSecondBracket]) let effectiveId = ReferenceLabel.normalized(refId.isEmpty ? linkText : refId) if let url = referenceDefinitions[effectiveId] { diff --git a/QuickMD/QuickMD/MarkdownTheme.swift b/QuickMD/QuickMD/MarkdownTheme.swift index 3b85fb7..e5536f9 100644 --- a/QuickMD/QuickMD/MarkdownTheme.swift +++ b/QuickMD/QuickMD/MarkdownTheme.swift @@ -321,7 +321,9 @@ struct MarkdownTheme: Sendable, Identifiable { /// Matches reference link definitions: [id]: url or [id]: <url> with optional title /// Captures: (1) reference ID, (2) URL - static let referenceLinkDefinitionPattern = #"^[ \t]*\[([^\]]+)\]:[ \t]+<?([^\s>]+)>?"# + /// The label: no unescaped bracket, backslash escapes kept as written + /// (`[a\]b]:` defines `a\]b`, `[a[b]:` is text — CommonMark, GitHub). + static let referenceLinkDefinitionPattern = #"^[ \t]*\[((?:[^\[\]\\]|\\.)+)\]:[ \t]+<?([^\s>]+)>?"# /// Matches footnote definitions: [^id]: text /// Captures: (1) footnote ID, (2) content diff --git a/QuickMD/QuickMD/ReferenceLinks.swift b/QuickMD/QuickMD/ReferenceLinks.swift index f851f36..af3c739 100644 --- a/QuickMD/QuickMD/ReferenceLinks.swift +++ b/QuickMD/QuickMD/ReferenceLinks.swift @@ -29,6 +29,32 @@ enum ReferenceLabel { // ASCII labels (nearly all): lowercasing IS the fold, no bridging. return ascii ? collapsed.lowercased() : collapsed.folding(options: .caseInsensitive, locale: nil) } + + /// The `]` closing a label whose text starts at `start` (just past its + /// `[`): the first UNESCAPED one, so `[x][a\]b]` reads the label `a\]b`. + /// An unescaped `[` inside makes it no label (CommonMark, GitHub) → nil. + /// The label is matched as written, backslashes included + /// (`[a\]b]: url` defines `a\]b`). `InlineSyntaxScanner.matchLabel` is + /// the same rule over bytes. + static func closingBracket(in text: Substring, from start: Substring.Index) -> Substring.Index? { + var index = start + while index < text.endIndex { + let character = text[index] + if character == "\\" { + let next = text.index(after: index) + if next < text.endIndex, MarkdownTheme.escapableChars.contains(text[next]) { + index = text.index(after: next) + continue + } + } else if character == "]" { + return index + } else if character == "[" { + return nil + } + index = text.index(after: index) + } + return nil + } } // MARK: - Reference links inside quotes, alerts and table cells @@ -215,7 +241,7 @@ enum ReferenceLinks { if remaining[afterBracket...].hasPrefix("[") { let afterSecondOpen = remaining.index(after: afterBracket) if afterSecondOpen < remaining.endIndex, - let closeSecondBracket = remaining[afterSecondOpen...].firstIndex(of: "]") { + let closeSecondBracket = ReferenceLabel.closingBracket(in: remaining, from: afterSecondOpen) { let refId = String(remaining[afterSecondOpen..<closeSecondBracket]) let effectiveId = ReferenceLabel.normalized(refId.isEmpty ? linkText : refId) if let url = definitions[effectiveId] { diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index acc9cc2..412fa62 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -733,6 +733,57 @@ final class ParserTests: XCTestCase { [[], ["https://empty"], ["https://tab"]]) } + /// R21c: a label may hold backslash-escaped brackets, matched as + /// written: `[a\]b]: url` is a definition, found by `[a\]b]`, + /// `[y][a\]b]`, `[w][A\]B]`, `![a\]b]`, in a quote and a heading; an + /// UNESCAPED `[` makes no label (`[a[b]:` is text, `[v][a[b]` literal). + /// GitHub (`gh api markdown`, 2026-11-04). The definition pattern + /// stopped at the first `]` and the full form's label too. + func testEscapedBracketsInLabelsLikeGitHub() { + let md = #""" + # [w][A\]B] + + [a\]b] and [y][a\]b] and [a\[c] and [z][a\\] and [w][A\]B] + + > Quoted [q][a\]b]. + + ![a\]b] + + [v][a[b] + + [a\]b]: https://esc + [a\[c]: https://open + [a\\]: https://bs + + [a[b]: https://ab + """# + let blocks = parse(md) + let renderer = MarkdownRenderer(theme: MarkdownTheme.cached(for: .light)) + // One entry per link: an escape splits a link's text into runs. + func links(_ text: AttributedString) -> [String] { + text.runs[\.link].compactMap { link, range in + link.map { "\(String(text[range].characters))=\($0.absoluteString)" } + } + } + guard blocks.count == 6, + case .heading(let level, let title, _) = blocks[0].content, + case .text(let paragraph) = blocks[1].content, + case .blockquote(let quote, _) = blocks[2].content, + case .image(let url, _, _) = blocks[3].content, + case .text(let literal) = blocks[4].content, + case .text(let notDefinition) = blocks[5].content else { + return XCTFail("blocks: \(blocks.map(\.content))") + } + XCTAssertEqual(links(renderer.renderHeader(title, level: level)), ["w=https://esc"]) + XCTAssertEqual(links(paragraph), ["a]b=https://esc", "y=https://esc", "a[c=https://open", "z=https://bs", + "w=https://esc"]) + XCTAssertEqual(links(renderer.renderQuotedBody(quote)), ["q=https://esc"]) + XCTAssertEqual(url, "https://esc") + XCTAssertEqual(String(literal.characters), "[v][a[b]") + XCTAssertEqual(links(literal), []) + XCTAssertEqual(String(notDefinition.characters), "[a[b]: https://ab") + } + /// R19: reference labels match as CommonMark's (`ReferenceLabel`) in /// every lookup — whitespace runs (tab, line break) are one space, the /// ends trimmed, Unicode case folding (`ẞ` = `SS`, `straße` = `STRASSE`, From 058a1c5cd06082be792a99ebb58c00cb1fbb4630 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:29:07 +0100 Subject: [PATCH 099/105] test: comments no longer point at local scratch files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMDTests/TableTextTests.swift | 3 +-- QuickMD/QuickMDTests/VerticalRhythmTests.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/QuickMD/QuickMDTests/TableTextTests.swift b/QuickMD/QuickMDTests/TableTextTests.swift index 3347ac4..58b33ff 100644 --- a/QuickMD/QuickMDTests/TableTextTests.swift +++ b/QuickMD/QuickMDTests/TableTextTests.swift @@ -20,8 +20,7 @@ final class TableTextTests: XCTestCase { private var theme: MarkdownTheme { MarkdownTheme.cached(for: .light) } - /// The prototype's fixtures (scratchpad/table-proto, 2026-10-22) plus a - /// header-only table. + /// The table prototype's fixtures (2026-10-22) plus a header-only table. private static let fixtures: [(name: String, markdown: String)] = [ ("simple", """ | Name | Role | Since | diff --git a/QuickMD/QuickMDTests/VerticalRhythmTests.swift b/QuickMD/QuickMDTests/VerticalRhythmTests.swift index 3e06ed3..2886ccd 100644 --- a/QuickMD/QuickMDTests/VerticalRhythmTests.swift +++ b/QuickMD/QuickMDTests/VerticalRhythmTests.swift @@ -666,7 +666,7 @@ final class VerticalRhythmTests: XCTestCase { /// in a nested quote, a list item or an alert too, and on a line with /// fewer markers than the quote. Our parser ended the quote and made the /// line a paragraph of its own. Every expected shape is GitHub's - /// (`gh api markdown`, scratchpad/audit/gfm/lazy_*.html). + /// (`gh api markdown`). func testALazyLineContinuesTheParagraphAQuoteEndsIn() throws { // Continues: a plain line, a hard-wrapped quote, `===`, an indented // line, an item that may not start a list here (none may: a lazy From 1154df2a332ec3ef11ec4149b3d819555e668746 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:33:41 +0100 Subject: [PATCH 100/105] chore: drop an unused loop index in the height measurer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/BlockHeightMeasurer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift index ea45f50..796ee21 100644 --- a/QuickMD/QuickMD/BlockHeightMeasurer.swift +++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift @@ -1278,7 +1278,7 @@ enum BlockHeightMeasurer { kinds.reserveCapacity(blocks.count) rhythm.reserveCapacity(blocks.count) - for (row, block) in blocks.enumerated() { + for block in blocks { // Text and headings have their edges measured below; every other // kind's are fixed by its view (`boxRhythm`) — a quote's from its // text's ink. From 07dca92d21ee9bbcb445789d6f4e5a98902f4d97 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 00:58:39 +0100 Subject: [PATCH 101/105] fix(images): a linked image is a picture only when it is its own paragraph Badges one per line ("[![Platform](...)](...)" on four lines, this project's README among them) are ONE paragraph on GitHub, the same as a badge row on one line. Each line became its own image block, so the badges stacked into a column of pictures with captions. The linked-image rule now applies only when no paragraph is open before the line (the buffer is empty or ends in a blank line or a rule) and the paragraph ends with it: the last line, or a blank line or a block that may interrupt a paragraph follows (fence, $$, ATX heading, quote, rule, a bullet or an item numbered 1 with text, HTML kinds 1-6, a table). A setext underline makes the line a heading. A logo or single badge standing alone is still the picture; a badge paragraph keeps its linked placeholders until badge rows (1.15). Every case checked with gh api markdown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/MarkdownBlockParser.swift | 51 ++++++++++++++++--- QuickMD/QuickMDTests/ParserTests.swift | 60 ++++++++++++++++++++--- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index e36d147..641780b 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -304,11 +304,17 @@ struct MarkdownBlockParser: Sendable { continue } - // A line that is only ONE linked image (`[![alt](img)](url)`, the - // README logo/badge form): the same `.image` block as `![alt](img)` - // — drawn on screen and in PDF, click = preview. The link target is - // dropped, as it is for `<a href><img></a>` (links on images: 1.15). - if let image = InlineSyntaxScanner.linkedImage(in: line[...], references: referenceDefinitions) { + // A PARAGRAPH that is only ONE linked image (`[![alt](img)](url)`, + // the README logo/badge form): the same `.image` block as + // `![alt](img)` — drawn on screen and in PDF, click = preview. The + // link target is dropped, as it is for `<a href><img></a>` (links + // on images: 1.15). Not a line of a longer paragraph: badges one + // per line are ONE paragraph on GitHub, a badge row like the one + // on a single line, and stay its linked placeholders (badge rows: + // 1.15) instead of a column of pictures. + if Self.noParagraphOpen(textBuffer), + let image = InlineSyntaxScanner.linkedImage(in: line[...], references: referenceDefinitions), + paragraphEnds(after: i, in: lines) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, pending: &pendingRows) blocks.append(.image(index: blockIndex, url: image.url, alt: HTMLEntities.decode(image.alt), width: nil, sourceLine: sourceLine)) @@ -804,6 +810,37 @@ struct MarkdownBlockParser: Sendable { MarkdownRenderer.isBlankLine(line) } + /// No paragraph goes on into the next line from the text buffered so + /// far: nothing is buffered, or it ends in a blank line or a rule (a + /// `---` under text never gets here: the setext branch takes it). + private static func noParagraphOpen(_ buffer: [(line: String, originalIndex: Int)]) -> Bool { + guard let last = buffer.last else { return true } + return isBlank(last.line) || MarkdownRenderer.isThematicBreak(last.line) + } + + /// The paragraph line `index` is in ends with it (CommonMark, GFM): it + /// is the last line, or the next line is blank or starts a block that + /// may interrupt a paragraph — a fence or `$$`, an ATX heading, a quote, + /// a rule, a bullet or an item numbered 1 with text + /// (`canInterruptParagraph`), an HTML block of kinds 1-6, a table (row + /// and separator). A setext underline makes the line a heading instead. + private func paragraphEnds(after index: Int, in lines: [String]) -> Bool { + guard index + 1 < lines.count else { return true } + let next = lines[index + 1] + if Self.isBlank(next) { return true } + let marker = Self.markerLine(next) + if marker.range(of: MarkdownTheme.setextH1Pattern, options: .regularExpression) != nil + || marker.range(of: MarkdownTheme.setextH2Pattern, options: .regularExpression) != nil { + return false + } + if Self.fenceOpening(marker) != nil || marker.hasPrefix("$$") || marker.hasPrefix(">") { return true } + if Self.headerRegex.firstMatch(in: next, range: NSRange(next.startIndex..., in: next)) != nil { return true } + if MarkdownRenderer.isThematicBreak(next) { return true } + if MarkdownRenderer.isListItem(next) { return MarkdownRenderer.canInterruptParagraph(next) } + if Self.startsHTMLBlock(marker, interruptingParagraph: true) { return true } + return Self.pipeCount(next) >= 2 && index + 2 < lines.count && isTableSeparator(lines[index + 2]) + } + /// Can this line be a definition-list TERM? Non-blank, not a definition /// line, and not the start of any other block this parser recognises /// (heading, list item, table row, fence, quote, image — Markdown or @@ -1585,7 +1622,7 @@ struct MarkdownBlockParser: Sendable { /// CommonMark's HTML block starts (kinds 1-7), as far as a lazy line needs /// them: `<!…`/`<?…`, a block-level or raw tag (`<div`, `</p>`, `<pre`), /// or a line that is one complete tag. `<span>text` is inline HTML. - static func startsHTMLBlock(_ marker: String) -> Bool { + static func startsHTMLBlock(_ marker: String, interruptingParagraph: Bool = false) -> Bool { guard marker.hasPrefix("<") else { return false } if marker.hasPrefix("<!") || marker.hasPrefix("<?") { return true } let afterBracket = marker.dropFirst().drop(while: { $0 == "/" }) @@ -1594,6 +1631,8 @@ struct MarkdownBlockParser: Sendable { let rest = afterBracket.dropFirst(name.count) let ends = rest.isEmpty || rest.first == " " || rest.first == "\t" || rest.first == ">" || rest.hasPrefix("/>") if ends, htmlBlockTags.contains(name) { return true } + // Kind 7 (a line that is one complete tag) cannot interrupt a paragraph. + if interruptingParagraph { return false } return marker.range(of: #"^</?[A-Za-z][A-Za-z0-9-]*(\s[^<>]*)?/?>[ \t]*$"#, options: .regularExpression) != nil } diff --git a/QuickMD/QuickMDTests/ParserTests.swift b/QuickMD/QuickMDTests/ParserTests.swift index 412fa62..85a788f 100644 --- a/QuickMD/QuickMDTests/ParserTests.swift +++ b/QuickMD/QuickMDTests/ParserTests.swift @@ -416,17 +416,20 @@ final class ParserTests: XCTestCase { // MARK: - A line that is only one linked image (B16) - /// `[![alt](img)](url)` alone on a line is the picture, like `![alt](img)` - /// and `<a href><img></a>` — an `.image` block on that line (Section copy, - /// search of the alt and copy work as for a plain image line). The link - /// target is not kept (as for `<a><img>`). Text beside it, or two linked - /// images (a badge row), stay a paragraph. + /// `[![alt](img)](url)` alone in its paragraph is the picture, like + /// `![alt](img)` and `<a href><img></a>` — an `.image` block on that line + /// (Section copy, search of the alt and copy work as for a plain image + /// line). The link target is not kept (as for `<a><img>`). Text beside + /// it, or two linked images (a badge row), stay a paragraph. func testLineOfOneLinkedImageIsAnImageBlock() { let md = """ # Title [![Logo](img/logo.png)](https://example.com) + [![Badge][shield]][site]\u{0020}\u{0020} + ![Plain](img/plain.png) + text before [![a](a.png)](https://a.example) [![a](a.png)](https://a.example) text after @@ -445,7 +448,7 @@ final class ParserTests: XCTestCase { } XCTAssertEqual(images.map(\.url), ["img/logo.png", "https://img.shields.io/x.svg", "img/plain.png"]) XCTAssertEqual(images.map(\.alt), ["Logo", "Badge", "Plain"]) - XCTAssertEqual(images.map(\.line), [1, 2, 3], "each on its own source line") + XCTAssertEqual(images.map(\.line), [1, 3, 5], "each on its own source line") let paragraphs = blocks.compactMap(plainText) XCTAssertEqual(paragraphs.count, 4, "\(paragraphs)") @@ -516,6 +519,51 @@ final class ParserTests: XCTestCase { XCTAssertLessThan(large, max(small, 0.005) * 24, "5k: \(small) s, 40k: \(large) s (quadratic would be ~64×)") } + /// Badges one per line are ONE paragraph on GitHub (`gh api markdown`: + /// `<p><a><img></a>⏎<a><img></a>…</p>`), the same as a badge row on one + /// line: linked placeholders, not a column of pictures (badge rows: 1.15). + /// So is a linked image with text on the line before or after it. One + /// that a blank line, a block start or the document's end closes is the + /// picture. + func testLinkedImagesOnePerLineStayOneParagraph() { + let readme = """ + [![Platform](https://img.shields.io/platform.svg)](https://www.apple.com/macos) + [![Swift](https://img.shields.io/swift.svg)](https://swift.org) + [![License](https://img.shields.io/license.svg)](LICENSE) + + [![Donation goal](https://qmd.app/goal.svg)](https://qmd.app/#goal) + """ + let blocks = parse(readme) + XCTAssertEqual(images(blocks).map(\.alt), ["Donation goal"]) + let paragraphs = blocks.compactMap(plainText) + XCTAssertEqual(paragraphs.count, 1, "\(paragraphs)") + XCTAssertEqual(paragraphs.first, "[Image: Platform] [Image: Swift] [Image: License]") + guard case .text(let row) = blocks[0].content else { return XCTFail("text") } + XCTAssertEqual(Set(row.runs.compactMap(\.link).map(\.absoluteString)), + ["https://www.apple.com/macos", "https://swift.org", "LICENSE"]) + + // Text right before or after (no blank line): one paragraph with it. + XCTAssertTrue(images(parse("Built with:\n[![Swift](s.svg)](https://swift.org)")).isEmpty) + XCTAssertTrue(images(parse("[![Swift](s.svg)](https://swift.org)\nand more")).isEmpty) + // A block that may interrupt a paragraph closes it: a heading, a + // bullet, an item numbered 1, a fence, a quote, HTML of kinds 1-6, a + // rule, a table (GitHub: `<p><img></p>` and then that block). + for next in ["# Next", "- item", "1. item", "```", "> q", "<div>", "***", "| a | b |\n|---|---|\n| 1 | 2 |"] { + XCTAssertEqual(images(parse("[![Logo](l.png)](https://x)\n\(next)")).map(\.alt), ["Logo"], next) + } + // Lines that cannot interrupt a paragraph go on with it (GitHub: one + // `<p>`): an item not numbered 1, an empty item, a lone inline tag; + // a setext underline makes the line a heading. + for next in ["2. item", "*", "<br>", "---", "==="] { + XCTAssertTrue(images(parse("[![Logo](l.png)](https://x)\n\(next)")).isEmpty, next) + } + // At the end of the document, after a blank line, a heading or a rule. + XCTAssertEqual(images(parse("text\n\n[![Logo](l.png)](https://x)")).map(\.alt), ["Logo"]) + XCTAssertEqual(images(parse("# Title\n[![Logo](l.png)](https://x)\n\ntext")).map(\.alt), ["Logo"]) + XCTAssertEqual(images(parse("***\n[![Logo](l.png)](https://x)")).map(\.alt), ["Logo"]) + XCTAssertEqual(images(parse("text\n\n---\n[![Logo](l.png)](https://x)")).map(\.alt), ["Logo"]) + } + // MARK: - Reference links in quotes, alerts and table cells /// Quotes, alerts and table cells keep their markdown and are rendered From fb6d4ad73c7411ea8c5e142acc6c87dd9c090686 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 01:06:07 +0100 Subject: [PATCH 102/105] fix(hover): block buttons show only while the pointer is over the document With the pointer resting just below the window, every few headings the document scrolled past that point kept its copy icon (a search jump, a keyboard or scroller scroll): SwiftUI reports a hover for a row that lies outside the clip view, and no mouse move ever ends it. 1.14.3 did not show it; the 1.14.4 rows and refreshes do. The heading copy, code copy and diagram zoom buttons now use DocumentHover: a continuous hover that counts only while the pointer is over the clip view of this list's window with no other window above it (SelectionController.pointerIsOverVisibleDocument), checked on every move, so a real hover after such a report still shows the button. Checked end to end: twelve scroll positions and the search flow with the pointer below the window show no icon; hovering a heading, a code block and a diagram shows its button and leaving hides it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/QuickMD/Views/ChromeButtons.swift | 2 +- QuickMD/QuickMD/Views/CodeBlockView.swift | 2 +- QuickMD/QuickMD/Views/MermaidBlockView.swift | 2 +- QuickMD/QuickMD/Views/VirtualBlockList.swift | 53 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/QuickMD/QuickMD/Views/ChromeButtons.swift b/QuickMD/QuickMD/Views/ChromeButtons.swift index 835482f..e7eb729 100644 --- a/QuickMD/QuickMD/Views/ChromeButtons.swift +++ b/QuickMD/QuickMD/Views/ChromeButtons.swift @@ -101,7 +101,7 @@ struct HeadingBlockView: View { } } .contentShape(Rectangle()) - .onHover { hovering in + .documentHover { hovering in hideWorkItem?.cancel() if hovering { isHovered = true diff --git a/QuickMD/QuickMD/Views/CodeBlockView.swift b/QuickMD/QuickMD/Views/CodeBlockView.swift index 0ed2e68..19f7cef 100644 --- a/QuickMD/QuickMD/Views/CodeBlockView.swift +++ b/QuickMD/QuickMD/Views/CodeBlockView.swift @@ -137,7 +137,7 @@ struct CodeBlockView: View { .opacity(isHovered || justCopied ? 1 : 0) .padding(Metrics.copyButtonOuterPadding) } - .onHover { hovering in + .documentHover { hovering in isHovered = hovering } .task(id: cacheKey) { diff --git a/QuickMD/QuickMD/Views/MermaidBlockView.swift b/QuickMD/QuickMD/Views/MermaidBlockView.swift index 634bd63..fd9d3b3 100644 --- a/QuickMD/QuickMD/Views/MermaidBlockView.swift +++ b/QuickMD/QuickMD/Views/MermaidBlockView.swift @@ -146,7 +146,7 @@ struct MermaidBlockView: View { .padding(6) } .padding(.vertical, Metrics.verticalPadding) - .onHover { hovering in + .documentHover { hovering in isHovered = hovering } } diff --git a/QuickMD/QuickMD/Views/VirtualBlockList.swift b/QuickMD/QuickMD/Views/VirtualBlockList.swift index 8f5926e..3ecf2f6 100644 --- a/QuickMD/QuickMD/Views/VirtualBlockList.swift +++ b/QuickMD/QuickMD/Views/VirtualBlockList.swift @@ -1914,6 +1914,44 @@ extension EnvironmentValues { } } +/// Hover for the buttons a block shows under the pointer (heading copy, code +/// copy, diagram zoom). SwiftUI also reports a hover for a row that lies +/// OUTSIDE the clip view: with the pointer resting just below the window, the +/// rows the document scrolls past that point became "hovered", and no mouse +/// move ever ended it — copy icons stayed on every few headings after a +/// search jump or a keyboard scroll. So the hover counts only while the +/// pointer is really over the visible document +/// (`SelectionController.pointerIsOverVisibleDocument`), checked on every +/// move (continuous hover), so a real hover after such a report still shows. +/// Outside the document list (no `blockSelection`) it is a plain hover. +struct DocumentHover: ViewModifier { + @Environment(\.blockSelection) private var blockSelection + let onChange: (Bool) -> Void + @State private var reported = false + + func body(content: Content) -> some View { + content.onContinuousHover { phase in + let hovering: Bool + switch phase { + case .active: + hovering = blockSelection?.controller?.pointerIsOverVisibleDocument() ?? true + case .ended: + hovering = false + } + guard hovering != reported else { return } + reported = hovering + onChange(hovering) + } + } +} + +extension View { + /// `onHover` for a block's hover buttons (`DocumentHover`). + func documentHover(perform action: @escaping (Bool) -> Void) -> some View { + modifier(DocumentHover(onChange: action)) + } +} + /// The document's ONE selection and everything that edits, draws and copies it /// (S-D4). Owned by `VirtualBlockList.Coordinator`; main-thread only (every /// entry point is an AppKit event, a menu action or a representable update). @@ -1969,6 +2007,21 @@ final class SelectionController: NSObject { keyObservers.forEach { NotificationCenter.default.removeObserver($0) } } + /// Whether the pointer is over the part of the document the reader sees + /// (the clip view's bounds) in this list's window, with no other window + /// above it there. `DocumentHover` asks: SwiftUI also reports a hover for + /// a row OUTSIDE the clip view, under a pointer resting just below the + /// window. + func pointerIsOverVisibleDocument() -> Bool { + guard let tableView, let window = tableView.window, + let clipView = tableView.enclosingScrollView?.contentView else { return false } + let screenPoint = NSEvent.mouseLocation + guard NSWindow.windowNumber(at: screenPoint, belowWindowWithWindowNumber: 0) + == window.windowNumber else { return false } + let point = clipView.convert(window.convertPoint(fromScreen: screenPoint), from: nil) + return clipView.bounds.contains(point) + } + func attach(tableView: BlockTableView) { self.tableView = tableView guard keyObservers.isEmpty else { return } From fe0f95412651f7e74913de32f6e7e99427aa9d44 Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 05:11:21 +0100 Subject: [PATCH 103/105] release: v1.14.4 (build 31) One vertical rhythm for every kind of block, replacing the cramped spacing of 1.14.3, and Markdown read as on GitHub. CHANGELOG entry rewritten for the shipped content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/CHANGELOG.md | 25 ++++++++++++++++++++--- QuickMD/QuickMD.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/QuickMD/CHANGELOG.md b/QuickMD/CHANGELOG.md index 72ef356..4541a99 100644 --- a/QuickMD/CHANGELOG.md +++ b/QuickMD/CHANGELOG.md @@ -5,12 +5,31 @@ All notable changes to QuickMD will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.14.4] - 2026-11-04 +## [1.14.4] - 2026-11-05 -Spacing around headings is back. +Comfortable spacing, done properly, and Markdown that reads like it does on GitHub. + +### Changed +- **Spacing:** one consistent rhythm between every kind of block. Headings get clear space above and stay close to their own text; paragraphs, lists, quotes, tables, code and diagrams are evenly spaced, with or without blank lines in the source. This replaces the cramped spacing of 1.14.3. PDF export follows the same rhythm and keeps a heading on the page with its text. +- **Reading Mode:** the column grows with zoom, so a line holds the same amount of text at any size (720 pt at 100%). The heading copy buttons are hidden there. ### Fixed -- Headings and lists have the spacing they had before 1.14.3 again. The 1.14.3 change that dropped the blank lines around blocks left headings too close to their text, so documents looked cramped; it is withdrawn. The other 1.14.3 changes stay. +- Read as on GitHub: + - character references such as `&`, `©` and ` ` show as characters, also in link addresses; + - formatting inside link text (`[**Docs**](...)`, ``[`code`](...)``) renders; + - a linked image that stands alone in its paragraph (a README logo or a single badge) shows the picture; + - reference links and footnotes work inside quotes, alerts and table cells; + - a quote wrapped over several lines without `>` on each line stays one quote; + - an alert marker with nothing under it (`> [!NOTE]` alone) is a plain quote; + - `1)` lists, a year like `1984.` inside a paragraph, a heading underlined over several lines, `\$5` amounts, and link definitions inside code; + - a fenced code block indented under a list step is no longer drawn shifted right. +- Formulas keep their LaTeX escapes (`$50\%$`), and a formula that cannot be drawn shows its source. +- Zoom and Reading Mode keep your reading position exactly; images and diagrams keep their size once shown, and a diagram no longer flashes empty when it scrolls back into view. +- Search centres the match inside a tall table, code block or quote. +- A link to a section of another document (`other.md#section`) scrolls to that section. +- A file that cannot be read says so, instead of reporting it as missing. +- Settings reopens on the tab you used last. +- Faster copy and search in documents with many headings; long quotes no longer slow down scrolling and search. ## [1.14.3] - 2026-11-04 diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj index 7696463..d3c1d00 100644 --- a/QuickMD/QuickMD.xcodeproj/project.pbxproj +++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj @@ -939,7 +939,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 31; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VJ92852M32; ENABLE_APP_SANDBOX = YES; @@ -983,7 +983,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 31; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VJ92852M32; ENABLE_APP_SANDBOX = YES; From 08d7f03117fa3b4bb16bcdf02ceee773c140d49f Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 05:28:45 +0100 Subject: [PATCH 104/105] docs(readme): screenshots for 1.14.4 The same eight views as for 1.14.3, captured again from 1.14.4 so the README shows its spacing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- QuickMD/Screenshots/screenshot-1.png | Bin 56553 -> 49014 bytes QuickMD/Screenshots/screenshot-2.png | Bin 60372 -> 53007 bytes QuickMD/Screenshots/screenshot-3.png | Bin 65745 -> 57372 bytes QuickMD/Screenshots/screenshot-4.png | Bin 53193 -> 44724 bytes QuickMD/Screenshots/screenshot-5.png | Bin 51901 -> 43809 bytes QuickMD/Screenshots/screenshot-6.png | Bin 39105 -> 40854 bytes QuickMD/Screenshots/screenshot-7.png | Bin 75424 -> 71498 bytes 7 files changed, 0 insertions(+), 0 deletions(-) diff --git a/QuickMD/Screenshots/screenshot-1.png b/QuickMD/Screenshots/screenshot-1.png index b28f78a6a0983ce2daddbbb85ff3eb37105793c9..2a15800d891561ee573c0249d91ed169cd49e945 100644 GIT binary patch literal 49014 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 Q1pxKZ(J)kRP=&<&A8zL?ZU6uP literal 56553 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 b*>g#z;G!W*^vRZu<PP9u?`p@i#S{Mz)jJlX diff --git a/QuickMD/Screenshots/screenshot-2.png b/QuickMD/Screenshots/screenshot-2.png index 5d6ebd177d2645daa58e96d4dfb6cffd4ce3c7e3..feee2beff7972cbe47abc264ed0357cf935297c2 100644 GIT binary patch literal 53007 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 literal 60372 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 diff --git a/QuickMD/Screenshots/screenshot-3.png b/QuickMD/Screenshots/screenshot-3.png index fb43ded8320423cd525a2ee667d900ae0abde8d9..a889f6c84e66dfc24405d2bb648de9bd1cef84f5 100644 GIT binary patch literal 57372 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%0A3ngqC(?`xn&83Oj_=XyS_( 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 literal 65745 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 diff --git a/QuickMD/Screenshots/screenshot-4.png b/QuickMD/Screenshots/screenshot-4.png index 9444425cf745630d548c48a7aed14e302d7408a6..a23192df7341279ce13d50beb59dabadba57d84b 100644 GIT binary patch literal 44724 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 literal 53193 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 IM6N<$f(W_Z9smFU diff --git a/QuickMD/Screenshots/screenshot-5.png b/QuickMD/Screenshots/screenshot-5.png index 063dec9ea81a9c469d26d27c6a8f16600ec7e2eb..59c2379af026be0b1027b412cb51efec522a37c8 100644 GIT binary patch literal 43809 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 VwQfh6<`V~lhPtLY)t6mj{tsh1@hkuU literal 51901 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 Mlv9%}mxcuY5BTQxY5)KL diff --git a/QuickMD/Screenshots/screenshot-6.png b/QuickMD/Screenshots/screenshot-6.png index 07faee5508e10b30530fc567f725b0cf03f12bf8..05f3c2a526342b3e756763349811433c2bf73cf2 100644 GIT binary patch literal 40854 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 -7Ti4mr{tb+HL`6jOq5JYO<Go@M#^Ur``e8?<aJ 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%0Ae7~vsVeIyiMc&q4#JY! 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 literal 39105 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�MMG@P6zCX7G_W3o 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 diff --git a/QuickMD/Screenshots/screenshot-7.png b/QuickMD/Screenshots/screenshot-7.png index 4b164b3d67b3997eaae2398c5fb0608a5a7fdacd..1dd25e952996978b8a289ed024097fd9801b0de4 100644 GIT binary patch literal 71498 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 literal 75424 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 From 3870506c10a9efb498ff3dd8a46d092226f75f8c Mon Sep 17 00:00:00 2001 From: Bartosz Sroczynski <b.sroczynski@pm.me> Date: Thu, 5 Nov 2026 05:29:16 +0100 Subject: [PATCH 105/105] chore: sync local Casks/quickmd.rb to v1.14.4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- Casks/quickmd.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Casks/quickmd.rb b/Casks/quickmd.rb index 24a8d01..a43b4b1 100644 --- a/Casks/quickmd.rb +++ b/Casks/quickmd.rb @@ -1,6 +1,6 @@ cask "quickmd" do - version "1.14.3" - sha256 "a9d00b75f01778d2e5309358f847f74076734bf51a639873ef197c6756114b78" + version "1.14.4" + sha256 "17677468c385bbe190c8770fe35843c41485bfe02ccbeca3210d8ee2ca4f0104" url "https://github.com/b451c/quickmd/releases/download/v#{version}/QuickMD-v#{version}.zip" name "QuickMD"