Skip to content

Commit e5ab909

Browse files
authored
Add link title support for commonmark (swiftlang#140)
* Add link title support for commonmark * Update cmark_node_get logic for Link and Image
1 parent 3d4b36c commit e5ab909

File tree

6 files changed

+91
-25
lines changed

6 files changed

+91
-25
lines changed

Sources/Markdown/Base/RawMarkup.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ enum RawMarkupData: Equatable {
3535
case image(source: String?, title: String?)
3636
case inlineHTML(String)
3737
case lineBreak
38-
case link(destination: String?)
38+
case link(destination: String?, title: String?)
3939
case softBreak
4040
case strong
4141
case text(String)
@@ -274,8 +274,8 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
274274
return .create(data: .lineBreak, parsedRange: parsedRange, children: [])
275275
}
276276

277-
static func link(destination: String?, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
278-
return .create(data: .link(destination: destination), parsedRange: parsedRange, children: children)
277+
static func link(destination: String?, title: String? = nil,parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
278+
return .create(data: .link(destination: destination, title: title), parsedRange: parsedRange, children: children)
279279
}
280280

281281
static func softBreak(parsedRange: SourceRange?) -> RawMarkup {

Sources/Markdown/Inline Nodes/Inline Containers/Link.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@ public struct Link: InlineMarkup, InlineContainer {
2929

3030
public extension Link {
3131
/// Create a link with a destination and zero or more child inline elements.
32-
init<Children: Sequence>(destination: String? = nil, _ children: Children) where Children.Element == RecurringInlineMarkup {
32+
init<Children: Sequence>(destination: String? = nil, title: String? = nil, _ children: Children) where Children.Element == RecurringInlineMarkup {
3333

3434
let destinationToUse: String?
3535
if let d = destination, d.isEmpty {
3636
destinationToUse = nil
3737
} else {
3838
destinationToUse = destination
3939
}
40+
let titleToUse: String?
41+
if let t = title, t.isEmpty {
42+
titleToUse = nil
43+
} else {
44+
titleToUse = title
45+
}
4046

41-
try! self.init(.link(destination: destinationToUse, parsedRange: nil, children.map { $0.raw.markup }))
47+
try! self.init(.link(destination: destinationToUse, title: titleToUse, parsedRange: nil, children.map { $0.raw.markup }))
4248
}
4349

4450
/// Create a link with a destination and zero or more child inline elements.
@@ -49,16 +55,33 @@ public extension Link {
4955
/// The link's destination.
5056
var destination: String? {
5157
get {
52-
guard case let .link(destination) = _data.raw.markup.data else {
58+
guard case let .link(destination, _) = _data.raw.markup.data else {
5359
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
5460
}
5561
return destination
5662
}
5763
set {
5864
if let d = newValue, d.isEmpty {
59-
_data = _data.replacingSelf(.link(destination: nil, parsedRange: nil, _data.raw.markup.copyChildren()))
65+
_data = _data.replacingSelf(.link(destination: nil, title: title, parsedRange: nil, _data.raw.markup.copyChildren()))
66+
} else {
67+
_data = _data.replacingSelf(.link(destination: newValue, title: title, parsedRange: nil, _data.raw.markup.copyChildren()))
68+
}
69+
}
70+
}
71+
72+
/// The link's title.
73+
var title: String? {
74+
get {
75+
guard case let .link(_, title) = _data.raw.markup.data else {
76+
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
77+
}
78+
return title
79+
}
80+
set {
81+
if let t = newValue, t.isEmpty {
82+
_data = _data.replacingSelf(.link(destination: destination, title: nil, parsedRange: nil, _data.raw.markup.copyChildren()))
6083
} else {
61-
_data = _data.replacingSelf(.link(destination: newValue, parsedRange: nil, _data.raw.markup.copyChildren()))
84+
_data = _data.replacingSelf(.link(destination: destination, title: newValue, parsedRange: nil, _data.raw.markup.copyChildren()))
6285
}
6386
}
6487
}

Sources/Markdown/Parser/CommonMarkConverter.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -452,21 +452,37 @@ struct MarkupParser {
452452
let parsedRange = state.range(state.node)
453453
let childConversion = convertChildren(state)
454454
let destination = String(cString: cmark_node_get_url(state.node))
455+
let title = String(cString: cmark_node_get_title(state.node))
455456
precondition(childConversion.state.node == state.node)
456457
precondition(childConversion.state.event == CMARK_EVENT_EXIT)
457-
return MarkupConversion(state: childConversion.state.next(), result: .link(destination: destination, parsedRange: parsedRange, childConversion.result))
458+
return MarkupConversion(
459+
state: childConversion.state.next(),
460+
result: .link(
461+
destination: destination.isEmpty ? nil : destination,
462+
title: title.isEmpty ? nil : title,
463+
parsedRange: parsedRange,
464+
childConversion.result
465+
)
466+
)
458467
}
459468

460469
private static func convertImage(_ state: MarkupConverterState) -> MarkupConversion<RawMarkup> {
461470
precondition(state.event == CMARK_EVENT_ENTER)
462471
precondition(state.nodeType == .image)
463472
let parsedRange = state.range(state.node)
464473
let childConversion = convertChildren(state)
465-
let destination = String(cString: cmark_node_get_url(state.node))
474+
let source = String(cString: cmark_node_get_url(state.node))
466475
let title = String(cString: cmark_node_get_title(state.node))
467476
precondition(childConversion.state.node == state.node)
468477
precondition(childConversion.state.event == CMARK_EVENT_EXIT)
469-
return MarkupConversion(state: childConversion.state.next(), result: .image(source: destination, title: title, parsedRange: parsedRange, childConversion.result))
478+
return MarkupConversion(
479+
state: childConversion.state.next(),
480+
result: .image(
481+
source: source.isEmpty ? nil : source,
482+
title: title.isEmpty ? nil : title,
483+
parsedRange: parsedRange, childConversion.result
484+
)
485+
)
470486
}
471487

472488
private static func convertStrikethrough(_ state: MarkupConverterState) -> MarkupConversion<RawMarkup> {

Tests/MarkdownTests/Inline Nodes/LinkTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,35 @@ class LinkTests: XCTestCase {
4848
link.destination = "test.example.com"
4949
XCTAssertFalse(link.isAutolink)
5050
}
51+
52+
func testTitleLink() throws {
53+
let markdown = #"""
54+
[Example](example.com "The example title")
55+
[Example2](example2.com)
56+
[Example3]()
57+
"""#
58+
59+
let document = Document(parsing: markdown)
60+
XCTAssertEqual(document.childCount, 1)
61+
let paragraph = try XCTUnwrap(document.child(at: 0) as? Paragraph)
62+
XCTAssertEqual(paragraph.childCount, 5)
63+
64+
XCTAssertTrue(paragraph.child(at: 1) is SoftBreak)
65+
XCTAssertTrue(paragraph.child(at: 3) is SoftBreak)
66+
let linkWithTitle = try XCTUnwrap(paragraph.child(at: 0) as? Link)
67+
let linkWithoutTitle = try XCTUnwrap(paragraph.child(at: 2) as? Link)
68+
let linkWithoutDestination = try XCTUnwrap(paragraph.child(at: 4) as? Link)
69+
70+
XCTAssertEqual(try XCTUnwrap(linkWithTitle.child(at: 0) as? Text).string, "Example")
71+
XCTAssertEqual(linkWithTitle.destination, "example.com")
72+
XCTAssertEqual(linkWithTitle.title, "The example title")
73+
74+
XCTAssertEqual(try XCTUnwrap(linkWithoutTitle.child(at: 0) as? Text).string, "Example2")
75+
XCTAssertEqual(linkWithoutTitle.destination, "example2.com")
76+
XCTAssertEqual(linkWithoutTitle.title, nil)
77+
78+
XCTAssertEqual(try XCTUnwrap(linkWithoutDestination.child(at: 0) as? Text).string, "Example3")
79+
XCTAssertEqual(linkWithoutDestination.destination, nil)
80+
XCTAssertEqual(linkWithoutDestination.title, nil)
81+
}
5182
}

Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ class MarkupFormatterSimpleRoundTripTests: XCTestCase {
711711
func testRoundTripHardBreakWithImage() {
712712
let source = """
713713
This is some text.\(" ")
714-
![This is an image.](image.png "")
714+
![This is an image.](image.png)
715715
"""
716716
checkRoundTrip(for: source)
717717
checkCharacterEquivalence(for: source)
@@ -720,7 +720,7 @@ class MarkupFormatterSimpleRoundTripTests: XCTestCase {
720720
func testRoundTripSoftBreakWithImage() {
721721
let source = """
722722
This is some text.
723-
![This is an image.](image.png "")
723+
![This is an image.](image.png)
724724
"""
725725
checkRoundTrip(for: source)
726726
checkCharacterEquivalence(for: source)
@@ -1394,7 +1394,6 @@ class MarkupFormatterTableTests: XCTestCase {
13941394
"""
13951395

13961396
let document = Document(parsing: source)
1397-
13981397
let expectedDump = """
13991398
Document
14001399
└─ Table alignments: |l|c|r|
@@ -1414,7 +1413,7 @@ class MarkupFormatterTableTests: XCTestCase {
14141413
│ │ └─ Link destination: "https://apple.com"
14151414
│ │ └─ Text "Apple"
14161415
│ ├─ Cell
1417-
│ │ └─ Image source: "image.png" title: ""
1416+
│ │ └─ Image source: "image.png"
14181417
│ │ └─ Text "image"
14191418
│ └─ Cell
14201419
│ └─ Link destination: "https://swift.org"
@@ -1429,17 +1428,14 @@ class MarkupFormatterTableTests: XCTestCase {
14291428

14301429
let formatted = document.format()
14311430
let expected = """
1432-
|*A* |**B** |~C~ |
1433-
|:-------------------------|:--------------------:|------------------:|
1434-
|[Apple](https://apple.com)|![image](image.png "")|<https://swift.org>|
1435-
|<br/> || |
1431+
|*A* |**B** |~C~ |
1432+
|:-------------------------|:-----------------:|------------------:|
1433+
|[Apple](https://apple.com)|![image](image.png)|<https://swift.org>|
1434+
|<br/> || |
14361435
"""
1437-
14381436
XCTAssertEqual(expected, formatted)
1439-
print(formatted)
14401437

14411438
let reparsed = Document(parsing: formatted)
1442-
print(reparsed.debugDescription())
14431439
XCTAssertTrue(document.hasSameStructure(as: reparsed))
14441440
}
14451441

Tests/MarkdownTests/Visitors/MarkupTreeDumperTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -29,7 +29,7 @@ final class MarkupTreeDumperTests: XCTestCase {
2929
│ ├─ Link @3:39-3:50 #12 destination: "foo"
3030
│ │ └─ Text @3:40-3:44 #13 "link"
3131
│ ├─ Text @3:50-3:51 #14 " "
32-
│ ├─ Image @3:51-3:64 #15 source: "foo" title: ""
32+
│ ├─ Image @3:51-3:64 #15 source: "foo"
3333
│ │ └─ Text @3:53-3:58 #16 "image"
3434
│ └─ Text @3:64-3:65 #17 "."
3535
├─ UnorderedList @5:1-9:1 #18

0 commit comments

Comments
 (0)