diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index f06ac260..2c3d7eae 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -51,7 +51,7 @@ let package = Package( if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { // Building standalone, so fetch all dependencies remotely. package.dependencies += [ - .package(url: "https://github.com/apple/swift-cmark.git", .branch("gfm")), + .package(url: "https://github.com/apple/swift-cmark.git", .branch("QuietMisdreavus/footnote-fixes")), .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.0.1")), ] diff --git a/Sources/Markdown/Base/Markup.swift b/Sources/Markdown/Base/Markup.swift index 4b271bff..f8e03445 100644 --- a/Sources/Markdown/Base/Markup.swift +++ b/Sources/Markdown/Base/Markup.swift @@ -75,6 +75,10 @@ func makeMarkup(_ data: _MarkupData) -> Markup { return DoxygenParameter(data) case .doxygenReturns: return DoxygenReturns(data) + case .footnoteReference: + return FootnoteReference(data) + case .footnoteDefinition: + return FootnoteDefinition(data) } } diff --git a/Sources/Markdown/Base/RawMarkup.swift b/Sources/Markdown/Base/RawMarkup.swift index 3391265e..d2d26cd0 100644 --- a/Sources/Markdown/Base/RawMarkup.swift +++ b/Sources/Markdown/Base/RawMarkup.swift @@ -54,6 +54,8 @@ enum RawMarkupData: Equatable { case doxygenParam(name: String) case doxygenReturns + case footnoteReference(footnoteID: String) + case footnoteDefinition(footnoteID: String) } extension RawMarkupData { @@ -247,6 +249,11 @@ final class RawMarkup: ManagedBuffer { static func blockDirective(name: String, nameLocation: SourceLocation?, argumentText: DirectiveArgumentText, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { return .create(data: .blockDirective(name: name, nameLocation: nameLocation, arguments: argumentText), parsedRange: parsedRange, children: children) } + + static func footnoteDefinition(footnoteID: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + + return .create(data: .footnoteDefinition(footnoteID: footnoteID), parsedRange: parsedRange, children: children) + } // MARK: Inline Creation @@ -297,6 +304,11 @@ final class RawMarkup: ManagedBuffer { static func inlineAttributes(attributes: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { return .create(data: .inlineAttributes(attributes: attributes), parsedRange: parsedRange, children: children) } + + static func footnoteReference(footnoteID: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + return .create(data: .footnoteReference(footnoteID: footnoteID), parsedRange: parsedRange, children: children) + } + // MARK: Extensions diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/FootnoteDefinition.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/FootnoteDefinition.swift new file mode 100644 index 00000000..6f903845 --- /dev/null +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/FootnoteDefinition.swift @@ -0,0 +1,56 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +public struct FootnoteDefinition: BlockContainer { + public var _data: _MarkupData + init(_ raw: RawMarkup) throws { + guard case .footnoteDefinition = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: FootnoteDefinition.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } +} + +// MARK: - Public API + +public extension FootnoteDefinition { + // MARK: BasicBlockContainer + + init(footnoteID: String, _ children: Children) where Children.Element == BlockMarkup { + try! self.init(.footnoteDefinition(footnoteID: footnoteID, parsedRange: nil, children.map { $0.raw.markup })) + } + + init(footnoteID: String, _ children: BlockMarkup...) { + self.init(footnoteID: footnoteID, children) + } + + var footnoteID: String { + get { + guard case let .footnoteDefinition(footnoteID: footnoteID) = _data.raw.markup.data else { + fatalError("\(self) markup wrapped unexpected \(_data.raw)") + } + return footnoteID + } + set { + _data = _data.replacingSelf(.footnoteDefinition(footnoteID: newValue, parsedRange: nil, _data.raw.markup.copyChildren())) + } + } + + // MARK: Visitation + + func accept(_ visitor: inout V) -> V.Result { + return visitor.visitFootnoteDefinition(self) + } +} diff --git a/Sources/Markdown/Inline Nodes/Inline Containers/FootnoteReference.swift b/Sources/Markdown/Inline Nodes/Inline Containers/FootnoteReference.swift new file mode 100644 index 00000000..36b1869f --- /dev/null +++ b/Sources/Markdown/Inline Nodes/Inline Containers/FootnoteReference.swift @@ -0,0 +1,57 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +/// A reference to a footnote +public struct FootnoteReference: InlineMarkup, InlineContainer { + public var _data: _MarkupData + + init(_ raw: RawMarkup) throws { + guard case .footnoteReference = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: FootnoteReference.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } +} + +// MARK: - Public API + +public extension FootnoteReference { + init(footnoteID: String, _ children: Children) where Children.Element == RecurringInlineMarkup { + try! self.init(.footnoteReference(footnoteID: footnoteID, parsedRange: nil, children.map { $0.raw.markup })) + } + + init(footnoteID: String, _ children: RecurringInlineMarkup...) { + self.init(footnoteID: footnoteID, children) + } + + /// The specified attributes in JSON5 format. + var footnoteID: String { + get { + guard case let .footnoteReference(footnoteID: footnoteID) = _data.raw.markup.data else { + fatalError("\(self) markup wrapped unexpected \(_data.raw)") + } + return footnoteID + } + set { + _data = _data.replacingSelf(.footnoteReference(footnoteID: newValue, parsedRange: nil, _data.raw.markup.copyChildren())) + } + } + + // MARK: Visitation + + func accept(_ visitor: inout V) -> V.Result { + return visitor.visitFootnoteReference(self) + } +} diff --git a/Sources/Markdown/Parser/CommonMarkConverter.swift b/Sources/Markdown/Parser/CommonMarkConverter.swift index f2a62da3..6dab0edf 100644 --- a/Sources/Markdown/Parser/CommonMarkConverter.swift +++ b/Sources/Markdown/Parser/CommonMarkConverter.swift @@ -56,6 +56,8 @@ fileprivate enum CommonMarkNodeType: String { case image case inlineAttributes = "attribute" case none = "NONE" + case footnoteReference = "footnote_reference" + case footnoteDefinition = "footnote_definition" case unknown = "" // Extensions @@ -68,6 +70,7 @@ fileprivate enum CommonMarkNodeType: String { case tableCell = "table_cell" case taskListItem = "tasklist" + } /// Represents the result of a cmark conversion: the current `MarkupConverterState` and the resulting converted node. @@ -230,6 +233,10 @@ struct MarkupParser { return convertTableRow(state) case .tableCell: return convertTableCell(state) + case .footnoteReference: + return convertFootnoteReference(state) + case .footnoteDefinition: + return convertFootnoteDefinition(state) case .inlineAttributes: return convertInlineAttributes(state) default: @@ -591,6 +598,28 @@ struct MarkupParser { precondition(childConversion.state.event == CMARK_EVENT_EXIT) return MarkupConversion(state: childConversion.state.next(), result: .inlineAttributes(attributes: attributes, parsedRange: parsedRange, childConversion.result)) } + + private static func convertFootnoteReference(_ state: MarkupConverterState) -> MarkupConversion { + precondition(state.event == CMARK_EVENT_ENTER) + precondition(state.nodeType == .footnoteReference) + let parsedRange = state.range(state.node) + let childConversion = convertChildren(state) + let footnoteID = String(cString: cmark_node_get_footnote_id(state.node)) + precondition(childConversion.state.node == state.node) + precondition(childConversion.state.event == CMARK_EVENT_EXIT) + return MarkupConversion(state: childConversion.state.next(), result: .footnoteReference(footnoteID: footnoteID, parsedRange: parsedRange, childConversion.result)) + } + + private static func convertFootnoteDefinition(_ state: MarkupConverterState) -> MarkupConversion { + precondition(state.event == CMARK_EVENT_ENTER) + precondition(state.nodeType == .footnoteDefinition) + let parsedRange = state.range(state.node) + let childConversion = convertChildren(state) + let footnoteID = String(cString: cmark_node_get_footnote_id(state.node)) + precondition(childConversion.state.node == state.node) + precondition(childConversion.state.event == CMARK_EVENT_EXIT) + return MarkupConversion(state: childConversion.state.next(), result: .footnoteDefinition(footnoteID: footnoteID, parsedRange: parsedRange, childConversion.result)) + } static func parseString(_ string: String, source: URL?, options: ParseOptions) -> Document { cmark_gfm_core_extensions_ensure_registered() @@ -598,6 +627,7 @@ struct MarkupParser { var cmarkOptions = CMARK_OPT_TABLE_SPANS if !options.contains(.disableSmartOpts) { cmarkOptions |= CMARK_OPT_SMART + cmarkOptions |= CMARK_OPT_FOOTNOTES } let parser = cmark_parser_new(cmarkOptions) @@ -605,6 +635,8 @@ struct MarkupParser { cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("table")) cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("strikethrough")) cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("tasklist")) + + cmark_parser_feed(parser, string, string.utf8.count) let rawDocument = cmark_parser_finish(parser) let initialState = MarkupConverterState(source: source, iterator: cmark_iter_new(rawDocument), event: CMARK_EVENT_NONE, node: nil, options: options, headerSeen: false, pendingTableBody: nil).next() diff --git a/Sources/Markdown/Rewriter/MarkupRewriter.swift b/Sources/Markdown/Rewriter/MarkupRewriter.swift index 931224b9..c1b16bef 100644 --- a/Sources/Markdown/Rewriter/MarkupRewriter.swift +++ b/Sources/Markdown/Rewriter/MarkupRewriter.swift @@ -87,4 +87,11 @@ extension MarkupRewriter { public mutating func visitText(_ text: Text) -> Result { return defaultVisit(text) } + public mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> Result { + return defaultVisit(footnoteReference) + } + + public mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> Result { + return defaultVisit(footnoteDefinition) + } } diff --git a/Sources/Markdown/Visitor/MarkupVisitor.swift b/Sources/Markdown/Visitor/MarkupVisitor.swift index 48ec9622..6fef9ac5 100644 --- a/Sources/Markdown/Visitor/MarkupVisitor.swift +++ b/Sources/Markdown/Visitor/MarkupVisitor.swift @@ -274,6 +274,10 @@ public protocol MarkupVisitor { - returns: The result of the visit. */ mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result + + mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> Result + + mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> Result /** Visit a `DoxygenParam` element and return the result. @@ -395,4 +399,11 @@ extension MarkupVisitor { public mutating func visitDoxygenReturns(_ doxygenReturns: DoxygenReturns) -> Result { return defaultVisit(doxygenReturns) } + + public mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> Result { + return defaultVisit(footnoteReference) + } + public mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> Result { + return defaultVisit(footnoteDefinition) + } } diff --git a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift index 40594d18..fe2fe0fe 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift @@ -290,4 +290,13 @@ struct MarkupTreeDumper: MarkupWalker { mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> () { dump(doxygenParam, customDescription: "parameter: \(doxygenParam.name)") } + + mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> () { + dump(footnoteReference, customDescription: "footnoteID: `\(footnoteReference.footnoteID)`") + } + + mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> () { + dump(footnoteDefinition, customDescription: "footnoteID: `\(footnoteDefinition.footnoteID)`") + } + } diff --git a/Sources/markdown-tool/main.swift b/Sources/markdown-tool/main.swift index 7f6b2286..bc6101f8 100644 --- a/Sources/markdown-tool/main.swift +++ b/Sources/markdown-tool/main.swift @@ -30,6 +30,8 @@ struct MarkdownCommand: ParsableCommand { ]) static func parseFile(at path: String, options: ParseOptions) throws -> (source: String, parsed: Document) { + print(path) + print(Process().currentDirectoryPath) let data = try Data(contentsOf: URL(fileURLWithPath: path)) guard let inputString = String(data: data, encoding: .utf8) else { throw Error.couldntDecodeInputAsUTF8 diff --git a/Tests/MarkdownTests/Parsing/FootnoteTest.swift b/Tests/MarkdownTests/Parsing/FootnoteTest.swift new file mode 100644 index 00000000..0763f758 --- /dev/null +++ b/Tests/MarkdownTests/Parsing/FootnoteTest.swift @@ -0,0 +1,36 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +@testable import Markdown +import XCTest + +class FootnoteTests: XCTestCase { + func testFootnotes() { + let text = """ + text with a footnote [^1]. + + [^1]: footnote definition. + """ + + let expectedDump = """ + Document @1:1-3:27 + ├─ Paragraph @1:1-1:27 + │ ├─ Text @1:1-1:22 "text with a footnote " + │ ├─ FootnoteReference @1:22-1:26 footnoteID: `1` + │ └─ Text @1:26-1:27 "." + └─ FootnoteDefinition @3:7-3:27 footnoteID: `1` + └─ Paragraph @3:7-3:27 + └─ Text @3:7-3:27 "footnote definition." + """ + + let document = Document(parsing: text, source: nil, options: [.parseBlockDirectives, .parseSymbolLinks]) + XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations)) + } +}