From 6087ac387f4d52cc2cda4f13c58d586f29f9068b Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 7 Mar 2025 13:10:10 -0600 Subject: [PATCH 1/2] Fixes some issues with line wrapping around inline code - Inline code that would have wrapped at the beginning of a line no longer gets an extra blank line before it. - Punctuation immediately following inline code is no longer pushed to the next line. - A line break is inserted before a symbolic link that would overflow the preferred line length. --- .../Walker/Walkers/MarkupFormatter.swift | 27 ++++-- .../Visitors/MarkupFormatterTests.swift | 90 ++++++++++++++++++- 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift index bee884b3..62a98ed0 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift @@ -367,11 +367,11 @@ public struct MarkupFormatter: MarkupWalker { // MARK: Formatter Utilities /// True if the current line length is over the preferred line limit. - var isOverPreferredLineLimit: Bool { + func isOverPreferredLineLimit(with addition: String = "") -> Bool { guard let lineLimit = formattingOptions.preferredLineLimit else { return false } - return state.lastLineLength >= lineLimit.maxLength + return state.lastLineLength + addition.count >= lineLimit.maxLength } /** @@ -590,8 +590,12 @@ public struct MarkupFormatter: MarkupWalker { // However, there is one exception: // we might already be right at the edge of a line when // this method was called. - if state.lastLineLength + word.count >= lineLimit.maxLength { - queueNewline() + if isOverPreferredLineLimit(with: word) && state.queuedNewlines == 0 { + // An exception to the exception: don't push punctuation to the + // next line, even if this line is at or longer than the limit. + if !(word.allSatisfy(\.isPunctuation) && word.count <= 3) { + queueNewline() + } } print(word, for: element) wordsThisLine += 1 @@ -776,13 +780,14 @@ public struct MarkupFormatter: MarkupWalker { public mutating func visitInlineCode(_ inlineCode: InlineCode) { let savedState = state + let atLineStart = state.lastLineLength == 0 softWrapPrint("`\(inlineCode.code)`", for: inlineCode) // Splitting inline code elements is allowed if it contains spaces. // If printing with automatic wrapping still put us over the line, // prefer to print it on the next line to give as much opportunity // to keep the contents on one line. - if inlineCode.indexInParent > 0 && (isOverPreferredLineLimit || state.effectiveLineNumber > savedState.effectiveLineNumber) { + if !atLineStart && inlineCode.indexInParent > 0 && (isOverPreferredLineLimit() || state.effectiveLineNumber > savedState.effectiveLineNumber) { restoreState(to: savedState) queueNewline() softWrapPrint("`\(inlineCode.code)`", for: inlineCode) @@ -813,7 +818,7 @@ public struct MarkupFormatter: MarkupWalker { // Image elements' source URLs can't be split. If wrapping the alt text // of an image still put us over the line, prefer to print it on the // next line to give as much opportunity to keep the alt text contents on one line. - if image.indexInParent > 0 && (isOverPreferredLineLimit || state.effectiveLineNumber > savedState.effectiveLineNumber) { + if image.indexInParent > 0 && (isOverPreferredLineLimit() || state.effectiveLineNumber > savedState.effectiveLineNumber) { restoreState(to: savedState) queueNewline() printImage() @@ -850,7 +855,7 @@ public struct MarkupFormatter: MarkupWalker { // Link elements' destination URLs can't be split. If wrapping the link text // of a link still put us over the line, prefer to print it on the // next line to give as much opportunity to keep the link text contents on one line. - if link.indexInParent > 0 && (isOverPreferredLineLimit || state.effectiveLineNumber > savedState.effectiveLineNumber) { + if link.indexInParent > 0 && (isOverPreferredLineLimit() || state.effectiveLineNumber > savedState.effectiveLineNumber) { restoreState(to: savedState) queueNewline() printRegularLink() @@ -1140,6 +1145,12 @@ public struct MarkupFormatter: MarkupWalker { } public mutating func visitSymbolLink(_ symbolLink: SymbolLink) { + let atLineStart = state.lastLineLength == 0 + let composited = "``\(symbolLink.destination ?? "")``" + + if !atLineStart && isOverPreferredLineLimit(with: composited) { + queueNewline() + } print("``", for: symbolLink) print(symbolLink.destination ?? "", for: symbolLink) print("``", for: symbolLink) @@ -1162,7 +1173,7 @@ public struct MarkupFormatter: MarkupWalker { // gets into the realm of JSON formatting which might be out of scope of // this formatter. Therefore if exceeded, prefer to print it on the next // line to give as much opportunity to keep the attributes on one line. - if attributes.indexInParent > 0 && (isOverPreferredLineLimit || state.effectiveLineNumber > savedState.effectiveLineNumber) { + if attributes.indexInParent > 0 && (isOverPreferredLineLimit() || state.effectiveLineNumber > savedState.effectiveLineNumber) { restoreState(to: savedState) queueNewline() printInlineAttributes() diff --git a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift index 37750e62..ae9604b4 100644 --- a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift +++ b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift @@ -947,6 +947,57 @@ class MarkupFormatterLineSplittingTests: XCTestCase { } } + func testParagraphWithLongSymbolicLinks() { + let source = """ + Because options are parsed before arguments, an option that consumes or + suppresses the `--` terminator can prevent a `postTerminator` argument + array from capturing any input. In particular, the + ``SingleValueParsingStrategy/unconditional``, + ``ArrayParsingStrategy/unconditionalSingleValue``, and + ``ArrayParsingStrategy/remaining`` parsing strategies can all consume + the terminator as part of their values. + """ + let expected = """ + Because options are parsed before arguments, an option that consumes or + suppresses the `--` terminator can prevent a `postTerminator` argument + array from capturing any input. In particular, the + ``SingleValueParsingStrategy/unconditional``, + ``ArrayParsingStrategy/unconditionalSingleValue``, and + ``ArrayParsingStrategy/remaining`` parsing strategies can all consume the + terminator as part of their values. + """ + let options = MarkupFormatter.Options(preferredLineLimit: PreferredLineLimit(maxLength: 74, breakWith: .softBreak)) + let document = Document(parsing: source, options: [.parseSymbolLinks]) + let printed = document.format(options: options) + XCTAssertEqual(expected, printed) + + let expectedTreeDump = """ + Document + └─ Paragraph + ├─ Text "Because options are parsed before arguments, an option that consumes or" + ├─ SoftBreak + ├─ Text "suppresses the " + ├─ InlineCode `--` + ├─ Text " terminator can prevent a " + ├─ InlineCode `postTerminator` + ├─ Text " argument" + ├─ SoftBreak + ├─ Text "array from capturing any input. In particular, the" + ├─ SoftBreak + ├─ InlineCode `SingleValueParsingStrategy/unconditional` + ├─ Text "," + ├─ SoftBreak + ├─ InlineCode `ArrayParsingStrategy/unconditionalSingleValue` + ├─ Text ", and" + ├─ SoftBreak + ├─ InlineCode `ArrayParsingStrategy/remaining` + ├─ Text " parsing strategies can all consume the" + ├─ SoftBreak + └─ Text "terminator as part of their values." + """ + XCTAssertEqual(expectedTreeDump, Document(parsing: printed).debugDescription()) + } + /** Test that line breaks maintain block structure in a flat, unordered list. */ @@ -1197,7 +1248,6 @@ class MarkupFormatterLineSplittingTests: XCTestCase { let expected = """ > Really really > really long line - > >\u{0020} > > Whoa, really > > really really > > really long @@ -1352,6 +1402,44 @@ class MarkupFormatterLineSplittingTests: XCTestCase { XCTAssertEqual(expected, printed) XCTAssertTrue(document.hasSameStructure(as: Document(parsing: printed))) } + + func testBreakAtLongInlineCode() { + let source = "This is a long line `that contains inline code`." + let document = Document(parsing: source) + let printed = document.format(options: .init(preferredLineLimit: .init(maxLength: 20, breakWith: .softBreak))) + let expected = """ + This is a long line + `that contains + inline code`. + """ + let expectedTreeDump = """ + Document + └─ Paragraph + ├─ Text "This is a long line" + ├─ SoftBreak + ├─ InlineCode `that contains inline code` + └─ Text "." + """ + XCTAssertEqual(expected, printed) + XCTAssertEqual(expectedTreeDump, Document(parsing: printed).debugDescription()) + } + + func testBreakAtShortInlineCode() { + let source = """ + Perform an atomic logical AND operation on the value referenced by + `pointer` and return the original value, applying the specified memory + ordering. + """ + let expected = """ + Perform an atomic logical AND operation on the value referenced by + `pointer` and return the original value, applying the specified memory + ordering. + """ + let document = Document(parsing: source) + let printed = document.format(options: .init(preferredLineLimit: .init(maxLength: 76, breakWith: .softBreak))) + XCTAssertEqual(expected, printed) + XCTAssertTrue(document.hasSameStructure(as: Document(parsing: printed))) + } } class MarkupFormatterTableTests: XCTestCase { From 529def7d344d407d5b6f2956c5a85e94bfcce124 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 7 Mar 2025 13:26:52 -0600 Subject: [PATCH 2/2] Updates a few documentation comments --- .../Walker/Walkers/MarkupFormatter.swift | 3 ++- .../Visitors/MarkupFormatterTests.swift | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift index 62a98ed0..d3c4999e 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift @@ -366,7 +366,8 @@ public struct MarkupFormatter: MarkupWalker { // MARK: Formatter Utilities - /// True if the current line length is over the preferred line limit. + /// Returns `true` if the current line length, plus an optional addition, + /// is over the preferred line limit. func isOverPreferredLineLimit(with addition: String = "") -> Bool { guard let lineLimit = formattingOptions.preferredLineLimit else { return false diff --git a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift index ae9604b4..ff7d64a4 100644 --- a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift +++ b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift @@ -947,10 +947,14 @@ class MarkupFormatterLineSplittingTests: XCTestCase { } } + /** + Test that breaks are inserted before symbolic links when necessary to + honor the preferred line limit. + */ func testParagraphWithLongSymbolicLinks() { let source = """ Because options are parsed before arguments, an option that consumes or - suppresses the `--` terminator can prevent a `postTerminator` argument + suppresses the `--` terminator can prevent a ``postTerminator`` argument array from capturing any input. In particular, the ``SingleValueParsingStrategy/unconditional``, ``ArrayParsingStrategy/unconditionalSingleValue``, and @@ -959,7 +963,7 @@ class MarkupFormatterLineSplittingTests: XCTestCase { """ let expected = """ Because options are parsed before arguments, an option that consumes or - suppresses the `--` terminator can prevent a `postTerminator` argument + suppresses the `--` terminator can prevent a ``postTerminator`` argument array from capturing any input. In particular, the ``SingleValueParsingStrategy/unconditional``, ``ArrayParsingStrategy/unconditionalSingleValue``, and @@ -1403,6 +1407,10 @@ class MarkupFormatterLineSplittingTests: XCTestCase { XCTAssertTrue(document.hasSameStructure(as: Document(parsing: printed))) } + /** + Test that wrapping at the start of a long inline code run doesn't cause + an extra newline. + */ func testBreakAtLongInlineCode() { let source = "This is a long line `that contains inline code`." let document = Document(parsing: source) @@ -1424,6 +1432,10 @@ class MarkupFormatterLineSplittingTests: XCTestCase { XCTAssertEqual(expectedTreeDump, Document(parsing: printed).debugDescription()) } + /** + Test that wrapping at the start of a short inline code run doesn't cause + an extra newline. + */ func testBreakAtShortInlineCode() { let source = """ Perform an atomic logical AND operation on the value referenced by