diff --git a/Sources/Markdown/Base/RawMarkup.swift b/Sources/Markdown/Base/RawMarkup.swift index 3cd1f853..c9051306 100644 --- a/Sources/Markdown/Base/RawMarkup.swift +++ b/Sources/Markdown/Base/RawMarkup.swift @@ -24,7 +24,7 @@ enum RawMarkupData: Equatable { case thematicBreak case htmlBlock(String) case listItem(checkbox: Checkbox?) - case orderedList + case orderedList(startIndex: UInt = 1) case unorderedList case paragraph case blockDirective(name: String, nameLocation: SourceLocation?, arguments: DirectiveArgumentText) @@ -228,8 +228,8 @@ final class RawMarkup: ManagedBuffer { return .create(data: .listItem(checkbox: checkbox), parsedRange: parsedRange, children: children) } - static func orderedList(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { - return .create(data: .orderedList, parsedRange: parsedRange, children: children) + static func orderedList(parsedRange: SourceRange?, _ children: [RawMarkup], startIndex: UInt = 1) -> RawMarkup { + return .create(data: .orderedList(startIndex: startIndex), parsedRange: parsedRange, children: children) } static func unorderedList(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/OrderedList.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/OrderedList.swift index b3fe1884..e52243b8 100644 --- a/Sources/Markdown/Block Nodes/Block Container Blocks/OrderedList.swift +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/OrderedList.swift @@ -32,6 +32,26 @@ public extension OrderedList { try! self.init(.orderedList(parsedRange: nil, items.map { $0.raw.markup })) } + /// The starting index for the list. + /// + /// The default starting index in CommonMark is 1. In this case, clients may use the default + /// ordered-list start index of their desired rendering format. For example, when rendering to + /// HTML, clients may omit the `start` attribute of the rendered list when this returns 1. + var startIndex: UInt { + get { + guard case let .orderedList(start) = _data.raw.markup.data else { + fatalError("\(self) markup wrapped unexpected \(_data.raw)") + } + return start + } + set { + guard startIndex != newValue else { + return + } + _data = _data.replacingSelf(.orderedList(parsedRange: nil, _data.raw.markup.copyChildren(), startIndex: newValue)) + } + } + // MARK: Visitation func accept(_ visitor: inout V) -> V.Result { diff --git a/Sources/Markdown/Parser/CommonMarkConverter.swift b/Sources/Markdown/Parser/CommonMarkConverter.swift index 2819f7c7..06782433 100644 --- a/Sources/Markdown/Parser/CommonMarkConverter.swift +++ b/Sources/Markdown/Parser/CommonMarkConverter.swift @@ -299,7 +299,8 @@ struct MarkupParser { case CMARK_BULLET_LIST: return MarkupConversion(state: childConversion.state.next(), result: .unorderedList(parsedRange: parsedRange, childConversion.result)) case CMARK_ORDERED_LIST: - return MarkupConversion(state: childConversion.state.next(), result: .orderedList(parsedRange: parsedRange, childConversion.result)) + let cmarkStart = UInt(cmark_node_get_list_start(state.node)) + return MarkupConversion(state: childConversion.state.next(), result: .orderedList(parsedRange: parsedRange, childConversion.result, startIndex: cmarkStart)) default: fatalError("cmark reported a list node but said its list type is CMARK_NO_LIST?") } diff --git a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift index f961de98..6c8307fc 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift @@ -496,6 +496,7 @@ public struct MarkupFormatter: MarkupWalker { return nil } let numeral: UInt + // FIXME: allow `orderedListNumerals` to defer to the user-authored starting index (#76, rdar://99970544) switch formattingOptions.orderedListNumerals { case let .allSame(n): numeral = n diff --git a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift index 3b265f17..2acdcd13 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift @@ -189,6 +189,14 @@ struct MarkupTreeDumper: MarkupWalker { dump(heading, customDescription: "level: \(heading.level)") } + mutating func visitOrderedList(_ orderedList: OrderedList) { + if orderedList.startIndex != 1 { + dump(orderedList, customDescription: "startIndex: \(orderedList.startIndex)") + } else { + defaultVisit(orderedList) + } + } + mutating func visitCodeBlock(_ codeBlock: CodeBlock) { let lines = indentLiteralBlock(codeBlock.code, from: codeBlock, countLines: false) dump(codeBlock, customDescription: "language: \(codeBlock.language ?? "none")\n\(lines)") diff --git a/Tests/MarkdownTests/Visitors/Everything.md b/Tests/MarkdownTests/Visitors/Everything.md index bd810706..690fc6af 100644 --- a/Tests/MarkdownTests/Visitors/Everything.md +++ b/Tests/MarkdownTests/Visitors/Everything.md @@ -12,6 +12,9 @@ > BlockQuote +2. flour +2. sugar + ```swift func foo() { let x = 1 diff --git a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift index f599f1e2..5dc30846 100644 --- a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift +++ b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift @@ -144,6 +144,33 @@ class MarkupFormatterSingleElementTests: XCTestCase { } } + func testPrintOrderedListCustomStart() { + let options = MarkupFormatter.Options(orderedListNumerals: .allSame(2)) + do { // no checkbox + let expected = "2. A list item." + var renderedList = OrderedList(ListItem(Paragraph(Text("A list item.")))) + renderedList.startIndex = 2 + let printed = renderedList.format(options: options) + XCTAssertEqual(expected, printed) + } + do { // unchecked + let expected = "2. [ ] A list item." + var renderedList = OrderedList(ListItem(checkbox: .unchecked, + Paragraph(Text("A list item.")))) + renderedList.startIndex = 2 + let printed = renderedList.format(options: options) + XCTAssertEqual(expected, printed) + } + do { // checked + let expected = "2. [x] A list item." + var renderedList = OrderedList(ListItem(checkbox: .checked, + Paragraph(Text("A list item.")))) + renderedList.startIndex = 2 + let printed = renderedList.format(options: options) + XCTAssertEqual(expected, printed) + } + } + func testPrintParagraph() { let expected = "A paragraph." let printed = Paragraph(Text("A paragraph.")).format() @@ -426,13 +453,13 @@ class MarkupFormatterOptionsTests: XCTestCase { 3. C """ let allSame = """ - 0. A - 0. B - 0. C + 1. A + 1. B + 1. C """ do { let document = Document(parsing: incrementing) - let printed = document.format(options: .init(orderedListNumerals: .allSame(0))) + let printed = document.format(options: .init(orderedListNumerals: .allSame(1))) XCTAssertEqual(allSame, printed) } @@ -917,7 +944,7 @@ class MarkupFormatterLineSplittingTests: XCTestCase { let expectedTreeDump = """ Document - └─ OrderedList + └─ OrderedList startIndex: 1000 └─ ListItem └─ Paragraph ├─ Text "Really really" diff --git a/Tests/MarkdownTests/Visitors/MarkupTreeDumperTests.swift b/Tests/MarkdownTests/Visitors/MarkupTreeDumperTests.swift index edf0514f..54de038d 100644 --- a/Tests/MarkdownTests/Visitors/MarkupTreeDumperTests.swift +++ b/Tests/MarkdownTests/Visitors/MarkupTreeDumperTests.swift @@ -14,7 +14,7 @@ import XCTest final class MarkupTreeDumperTests: XCTestCase { func testDumpEverything() { let expectedDump = """ - Document @1:1-39:90 Root #\(everythingDocument.raw.metadata.id.rootId) #0 + Document @1:1-42:90 Root #\(everythingDocument.raw.metadata.id.rootId) #0 ├─ Heading @1:1-1:9 #1 level: 1 │ └─ Text @1:3-1:9 #2 "Header" ├─ Paragraph @3:1-3:65 #3 @@ -55,37 +55,44 @@ final class MarkupTreeDumperTests: XCTestCase { ├─ BlockQuote @13:1-13:13 #38 │ └─ Paragraph @13:3-13:13 #39 │ └─ Text @13:3-13:13 #40 "BlockQuote" - ├─ CodeBlock @15:1-19:4 #41 language: swift + ├─ OrderedList @15:1-17:1 #41 startIndex: 2 + │ ├─ ListItem @15:1-15:9 #42 + │ │ └─ Paragraph @15:4-15:9 #43 + │ │ └─ Text @15:4-15:9 #44 "flour" + │ └─ ListItem @16:1-17:1 #45 + │ └─ Paragraph @16:4-16:9 #46 + │ └─ Text @16:4-16:9 #47 "sugar" + ├─ CodeBlock @18:1-22:4 #48 language: swift │ func foo() { │ let x = 1 │ } - ├─ CodeBlock @21:5-22:1 #42 language: none + ├─ CodeBlock @24:5-25:1 #49 language: none │ // Is this real code? Or just fantasy? - ├─ Paragraph @23:1-23:31 #43 - │ ├─ Text @23:1-23:12 #44 "This is an " - │ ├─ Link @23:12-23:30 #45 destination: "topic://autolink" - │ │ └─ Text @23:13-23:29 #46 "topic://autolink" - │ └─ Text @23:30-23:31 #47 "." - ├─ ThematicBreak @25:1-26:1 #48 - ├─ HTMLBlock @27:1-29:5 #49 + ├─ Paragraph @26:1-26:31 #50 + │ ├─ Text @26:1-26:12 #51 "This is an " + │ ├─ Link @26:12-26:30 #52 destination: "topic://autolink" + │ │ └─ Text @26:13-26:29 #53 "topic://autolink" + │ └─ Text @26:30-26:31 #54 "." + ├─ ThematicBreak @28:1-29:1 #55 + ├─ HTMLBlock @30:1-32:5 #56 │ │ An HTML Block. │ - ├─ Paragraph @31:1-31:33 #50 - │ ├─ Text @31:1-31:14 #51 "This is some " - │ ├─ InlineHTML @31:14-31:17 #52

- │ ├─ Text @31:17-31:28 #53 "inline html" - │ ├─ InlineHTML @31:28-31:32 #54

- │ └─ Text @31:32-31:33 #55 "." - ├─ Paragraph @33:1-34:6 #56 - │ ├─ Text @33:1-33:7 #57 "line" - │ ├─ LineBreak #58 - │ └─ Text @34:1-34:6 #59 "break" - ├─ Paragraph @36:1-37:6 #60 - │ ├─ Text @36:1-36:5 #61 "soft" - │ ├─ SoftBreak #62 - │ └─ Text @37:1-37:6 #63 "break" - └─ HTMLBlock @39:1-39:90 #64 + ├─ Paragraph @34:1-34:33 #57 + │ ├─ Text @34:1-34:14 #58 "This is some " + │ ├─ InlineHTML @34:14-34:17 #59

+ │ ├─ Text @34:17-34:28 #60 "inline html" + │ ├─ InlineHTML @34:28-34:32 #61

+ │ └─ Text @34:32-34:33 #62 "." + ├─ Paragraph @36:1-37:6 #63 + │ ├─ Text @36:1-36:7 #64 "line" + │ ├─ LineBreak #65 + │ └─ Text @37:1-37:6 #66 "break" + ├─ Paragraph @39:1-40:6 #67 + │ ├─ Text @39:1-39:5 #68 "soft" + │ ├─ SoftBreak #69 + │ └─ Text @40:1-40:6 #70 "break" + └─ HTMLBlock @42:1-42:90 #71 """ print(everythingDocument.debugDescription(options: [.printEverything]))