Skip to content

Add footnotes. #228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added Container
Empty file.
4 changes: 4 additions & 0 deletions Sources/Markdown/Base/Markup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func makeMarkup(_ data: _MarkupData) -> Markup {
return SymbolLink(data)
case .inlineAttributes:
return InlineAttributes(data)
case .footnoteReference:
return FootnoteReference(data)
case .footnoteDefinition:
return FootnoteDefinition(data)
case .doxygenDiscussion:
return DoxygenDiscussion(data)
case .doxygenNote:
Expand Down
11 changes: 11 additions & 0 deletions Sources/Markdown/Base/RawMarkup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ enum RawMarkupData: Equatable {
case tableRow
case tableCell(colspan: UInt, rowspan: UInt)

case footnoteReference(footnoteID: String)
case footnoteDefinition(footnoteID: String)

case doxygenDiscussion
case doxygenNote
case doxygenParam(name: String)
Expand Down Expand Up @@ -344,6 +347,14 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
return .create(data: .tableCell(colspan: colspan, rowspan: rowspan), parsedRange: parsedRange, children: children)
}

static func footnoteDefinition(footnoteID: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
return .create(data: .footnoteDefinition(footnoteID: footnoteID), parsedRange: parsedRange, children: children)
}

static func footnoteReference(footnoteID: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
return .create(data: .footnoteReference(footnoteID: footnoteID), parsedRange: parsedRange, children: children)
}

static func doxygenDiscussion(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
return .create(data: .doxygenDiscussion, parsedRange: parsedRange, children: children)
}
Expand Down
Empty file added Sources/Markdown/Block
Empty file.
Original file line number Diff line number Diff line change
@@ -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<Children: Sequence>(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<V: MarkupVisitor>(_ visitor: inout V) -> V.Result {
return visitor.visitFootnoteDefinition(self)
}
}
Original file line number Diff line number Diff line change
@@ -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<Children: Sequence>(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<V: MarkupVisitor>(_ visitor: inout V) -> V.Result {
return visitor.visitFootnoteReference(self)
}
}
40 changes: 40 additions & 0 deletions Sources/Markdown/Parser/CommonMarkConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ fileprivate enum CommonMarkNodeType: String {
case tableCell = "table_cell"

case taskListItem = "tasklist"

case footnoteReference = "footnote_reference"
case footnoteDefinition = "footnote_definition"
}

/// Represents the result of a cmark conversion: the current `MarkupConverterState` and the resulting converted node.
Expand Down Expand Up @@ -139,6 +142,16 @@ fileprivate struct MarkupConverterState {
guard let type = CommonMarkNodeType(rawValue: typeString) else {
fatalError("Unknown cmark node type '\(typeString)' encountered during conversion")
}
if type == .unknown {
// NOTE: cmark does not expose strings for the footnote types, but
// does correctly identify their type.
switch cmark_node_get_type(node) {
case CMARK_NODE_FOOTNOTE_DEFINITION: return .footnoteDefinition
case CMARK_NODE_FOOTNOTE_REFERENCE: return .footnoteReference
default:
fatalError("Unknown cmark node type '\(typeString)' encountered during conversion")
}
}
return type
}

Expand Down Expand Up @@ -232,6 +245,10 @@ struct MarkupParser {
return convertTableCell(state)
case .inlineAttributes:
return convertInlineAttributes(state)
case .footnoteReference:
return convertFootnoteReference(state)
case .footnoteDefinition:
return convertFootnoteDefinition(state)
default:
fatalError("Unknown cmark node type '\(state.nodeType.rawValue)' encountered during conversion")
}
Expand Down Expand Up @@ -608,6 +625,28 @@ struct MarkupParser {
return MarkupConversion(state: childConversion.state.next(), result: .inlineAttributes(attributes: attributes, parsedRange: parsedRange, childConversion.result))
}

private static func convertFootnoteReference(_ state: MarkupConverterState) -> MarkupConversion<RawMarkup> {
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_literal(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<RawMarkup> {
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_literal(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()

Expand All @@ -618,6 +657,7 @@ struct MarkupParser {
if !options.contains(.disableSourcePosOpts) {
cmarkOptions |= CMARK_OPT_SOURCEPOS
}
cmarkOptions |= CMARK_OPT_FOOTNOTES

let parser = cmark_parser_new(cmarkOptions)

Expand Down
7 changes: 7 additions & 0 deletions Sources/Markdown/Rewriter/MarkupRewriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
22 changes: 22 additions & 0 deletions Sources/Markdown/Visitor/MarkupVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ public protocol MarkupVisitor<Result> {
*/
mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result

/**
Visit an `FootnoteReference` element and return the result.

- parameter attributes: An `FootnoteReference` element.
- returns: The result of the visit.
*/
mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> Result

/**
Visit an `FootnoteDefinition` element and return the result.

- parameter attributes: An `FootnoteDefinition` element.
- returns: The result of the visit.
*/
mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> Result

/**
Visit a `DoxygenDiscussion` element and return the result.

Expand Down Expand Up @@ -405,6 +421,12 @@ extension MarkupVisitor {
public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result {
return defaultVisit(attributes)
}
public mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> Result {
return defaultVisit(footnoteReference)
}
public mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> Result {
return defaultVisit(footnoteDefinition)
}
public mutating func visitDoxygenDiscussion(_ doxygenDiscussion: DoxygenDiscussion) -> Result {
return defaultVisit(doxygenDiscussion)
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ struct MarkupTreeDumper: MarkupWalker {
dump(attributes, customDescription: "attributes: `\(attributes.attributes)`")
}

mutating func visitFootnoteReference(_ footnoteReference: FootnoteReference) -> () {
dump(footnoteReference, customDescription: "footnoteID: `\(footnoteReference.footnoteID)`")
}

mutating func visitFootnoteDefinition(_ footnoteDefinition: FootnoteDefinition) -> () {
dump(footnoteDefinition, customDescription: "footnoteID: `\(footnoteDefinition.footnoteID)`")
}

mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> () {
dump(doxygenParam, customDescription: "parameter: \(doxygenParam.name)")
}
Expand Down
70 changes: 70 additions & 0 deletions Tests/MarkdownTests/Block Nodes/FootnoteTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
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 testSimpleFootnote() {
let source = """
text with a footnote [^1].
[^1]: footnote definition.
"""

let document = Document(parsing: source)

let expectedDump = """
Document @1:1-2: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 @2:7-2:27 footnoteID: `1`
└─ Paragraph @2:7-2:27
└─ Text @2:7-2:27 "footnote definition."
"""

XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations))
}

func testBlockFootnote() {
let source = """
text with a block footnote [^1].
[^1]: This is a long footnote, including a quote:

> This is a multi-line quote, spanning
> multiple lines.

And then some more text.
"""

let document = Document(parsing: source)

let expectedDump = """
Document @1:1-7:29
├─ Paragraph @1:1-1:33
│ ├─ Text @1:1-1:28 "text with a block footnote "
│ ├─ FootnoteReference @1:28-1:32 footnoteID: `1`
│ └─ Text @1:32-1:33 "."
└─ FootnoteDefinition @2:7-7:29 footnoteID: `1`
├─ Paragraph @2:7-2:50
│ └─ Text @2:7-2:50 "This is a long footnote, including a quote:"
├─ BlockQuote @4:5-5:22
│ └─ Paragraph @4:7-5:22
│ ├─ Text @4:7-4:43 "This is a multi-line quote, spanning"
│ ├─ SoftBreak
│ └─ Text @5:7-5:22 "multiple lines."
└─ Paragraph @7:5-7:29
└─ Text @7:5-7:29 "And then some more text."
"""

XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations))
}
}