Skip to content

Commit 7dd6c52

Browse files
Add support for custom attributes (#25)
rdar://96536586
1 parent d491147 commit 7dd6c52

File tree

9 files changed

+171
-0
lines changed

9 files changed

+171
-0
lines changed

Sources/Markdown/Base/Markup.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ func makeMarkup(_ data: _MarkupData) -> Markup {
6969
return Table.Cell(data)
7070
case .symbolLink:
7171
return SymbolLink(data)
72+
case .inlineAttributes:
73+
return InlineAttributes(data)
7274
}
7375
}
7476

Sources/Markdown/Base/RawMarkup.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ enum RawMarkupData: Equatable {
4040
case strong
4141
case text(String)
4242
case symbolLink(destination: String?)
43+
case inlineAttributes(attributes: String)
4344

4445
// Extensions
4546
case strikethrough
@@ -290,6 +291,10 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
290291
return .create(data: .symbolLink(destination: destination), parsedRange: parsedRange, children: [])
291292
}
292293

294+
static func inlineAttributes(attributes: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
295+
return .create(data: .inlineAttributes(attributes: attributes), parsedRange: parsedRange, children: children)
296+
}
297+
293298
// MARK: Extensions
294299

295300
static func strikethrough(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/// A set of one or more inline attributes.
12+
public struct InlineAttributes: InlineMarkup, InlineContainer {
13+
public var _data: _MarkupData
14+
15+
init(_ raw: RawMarkup) throws {
16+
guard case .inlineAttributes = raw.data else {
17+
throw RawMarkup.Error.concreteConversionError(from: raw, to: InlineAttributes.self)
18+
}
19+
let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0))
20+
self.init(_MarkupData(absoluteRaw))
21+
}
22+
23+
init(_ data: _MarkupData) {
24+
self._data = data
25+
}
26+
}
27+
28+
// MARK: - Public API
29+
30+
public extension InlineAttributes {
31+
/// Create a set of custom inline attributes applied to zero or more child inline elements.
32+
init<Children: Sequence>(attributes: String, _ children: Children) where Children.Element == RecurringInlineMarkup {
33+
try! self.init(.inlineAttributes(attributes: attributes, parsedRange: nil, children.map { $0.raw.markup }))
34+
}
35+
36+
/// Create a set of custom attributes applied to zero or more child inline elements.
37+
init(attributes: String, _ children: RecurringInlineMarkup...) {
38+
self.init(attributes: attributes, children)
39+
}
40+
41+
/// The specified attributes in JSON5 format.
42+
var attributes: String {
43+
get {
44+
guard case let .inlineAttributes(attributes) = _data.raw.markup.data else {
45+
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
46+
}
47+
return attributes
48+
}
49+
set {
50+
_data = _data.replacingSelf(.inlineAttributes(attributes: newValue, parsedRange: nil, _data.raw.markup.copyChildren()))
51+
}
52+
}
53+
54+
// MARK: Visitation
55+
56+
func accept<V: MarkupVisitor>(_ visitor: inout V) -> V.Result {
57+
return visitor.visitInlineAttributes(self)
58+
}
59+
}

Sources/Markdown/Parser/CommonMarkConverter.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ fileprivate enum CommonMarkNodeType: String {
5454
case strong
5555
case link
5656
case image
57+
case inlineAttributes = "attribute"
5758
case none = "NONE"
5859
case unknown = "<unknown>"
5960

@@ -229,6 +230,8 @@ struct MarkupParser {
229230
return convertTableRow(state)
230231
case .tableCell:
231232
return convertTableCell(state)
233+
case .inlineAttributes:
234+
return convertInlineAttributes(state)
232235
default:
233236
fatalError("Unknown cmark node type '\(state.nodeType.rawValue)' encountered during conversion")
234237
}
@@ -578,6 +581,17 @@ struct MarkupParser {
578581
return MarkupConversion(state: childConversion.state.next(), result: .tableCell(parsedRange: parsedRange, colspan: colspan, rowspan: rowspan, childConversion.result))
579582
}
580583

584+
private static func convertInlineAttributes(_ state: MarkupConverterState) -> MarkupConversion<RawMarkup> {
585+
precondition(state.event == CMARK_EVENT_ENTER)
586+
precondition(state.nodeType == .inlineAttributes)
587+
let parsedRange = state.range(state.node)
588+
let childConversion = convertChildren(state)
589+
let attributes = String(cString: cmark_node_get_attributes(state.node))
590+
precondition(childConversion.state.node == state.node)
591+
precondition(childConversion.state.event == CMARK_EVENT_EXIT)
592+
return MarkupConversion(state: childConversion.state.next(), result: .inlineAttributes(attributes: attributes, parsedRange: parsedRange, childConversion.result))
593+
}
594+
581595
static func parseString(_ string: String, source: URL?, options: ParseOptions) -> Document {
582596
cmark_gfm_core_extensions_ensure_registered()
583597

Sources/Markdown/Rewriter/MarkupRewriter.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ extension MarkupRewriter {
7575
public mutating func visitLink(_ link: Link) -> Result {
7676
return defaultVisit(link)
7777
}
78+
public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result {
79+
return defaultVisit(attributes)
80+
}
7881
public mutating func visitSoftBreak(_ softBreak: SoftBreak) -> Result {
7982
return defaultVisit(softBreak)
8083
}

Sources/Markdown/Visitor/MarkupVisitor.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ public protocol MarkupVisitor {
266266
- returns: The result of the visit.
267267
*/
268268
mutating func visitSymbolLink(_ symbolLink: SymbolLink) -> Result
269+
270+
/**
271+
Visit an `InlineAttributes` element and return the result.
272+
273+
- parameter attribute: An `InlineAttributes` element.
274+
- returns: The result of the visit.
275+
*/
276+
mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result
269277
}
270278

271279
extension MarkupVisitor {
@@ -362,4 +370,7 @@ extension MarkupVisitor {
362370
public mutating func visitSymbolLink(_ symbolLink: SymbolLink) -> Result {
363371
return defaultVisit(symbolLink)
364372
}
373+
public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result {
374+
return defaultVisit(attributes)
375+
}
365376
}

Sources/Markdown/Walker/Walkers/MarkupFormatter.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,4 +1120,28 @@ public struct MarkupFormatter: MarkupWalker {
11201120
print(symbolLink.destination ?? "", for: symbolLink)
11211121
print("``", for: symbolLink)
11221122
}
1123+
1124+
public mutating func visitInlineAttributes(_ attributes: InlineAttributes) {
1125+
let savedState = state
1126+
func printInlineAttributes() {
1127+
print("[", for: attributes)
1128+
descendInto(attributes)
1129+
print("](", for: attributes)
1130+
print(attributes.attributes, for: attributes)
1131+
print(")", for: attributes)
1132+
}
1133+
1134+
printInlineAttributes()
1135+
1136+
// Inline attributes *can* have their key-value pairs split across multiple
1137+
// lines as they are formatted as JSON5, however formatting the output as such
1138+
// gets into the realm of JSON formatting which might be out of scope of
1139+
// this formatter. Therefore if exceeded, prefer to print it on the next
1140+
// line to give as much opportunity to keep the attributes on one line.
1141+
if attributes.indexInParent > 0 && (isOverPreferredLineLimit || state.lineNumber > savedState.lineNumber) {
1142+
restoreState(to: savedState)
1143+
queueNewline()
1144+
printInlineAttributes()
1145+
}
1146+
}
11231147
}

Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,8 @@ struct MarkupTreeDumper: MarkupWalker {
282282
dump(tableCell)
283283
}
284284
}
285+
286+
mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> () {
287+
dump(attributes, customDescription: "attributes: `\(attributes.attributes)`")
288+
}
285289
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
@testable import Markdown
13+
14+
class InlineAttributesTests: XCTestCase {
15+
func testInlineAttributesAttributes() {
16+
let attributes = "rainbow: 'extreme'"
17+
let inlineAttributes = InlineAttributes(attributes: attributes)
18+
XCTAssertEqual(attributes, inlineAttributes.attributes)
19+
XCTAssertEqual(0, inlineAttributes.childCount)
20+
21+
let newAttributes = "rainbow: 'medium'"
22+
var newInlineAttributes = inlineAttributes
23+
newInlineAttributes.attributes = newAttributes
24+
XCTAssertEqual(newAttributes, newInlineAttributes.attributes)
25+
XCTAssertFalse(inlineAttributes.isIdentical(to: newInlineAttributes))
26+
}
27+
28+
func testInlineAttributesFromSequence() {
29+
let children = [Text("Hello, world!")]
30+
let inlineAttributes = InlineAttributes(attributes: "rainbow: 'extreme'", children)
31+
let expectedDump = """
32+
InlineAttributes attributes: `rainbow: 'extreme'`
33+
└─ Text "Hello, world!"
34+
"""
35+
XCTAssertEqual(expectedDump, inlineAttributes.debugDescription())
36+
}
37+
38+
func testParseInlineAttributes() {
39+
let source = "^[Hello, world!](rainbow: 'extreme')"
40+
let document = Document(parsing: source)
41+
let expectedDump = """
42+
Document @1:1-1:37
43+
└─ Paragraph @1:1-1:37
44+
└─ InlineAttributes @1:1-1:37 attributes: `rainbow: 'extreme'`
45+
└─ Text @1:3-1:16 "Hello, world!"
46+
"""
47+
XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations))
48+
}
49+
}

0 commit comments

Comments
 (0)