Skip to content

Commit ab4df86

Browse files
committed
- Better ordered list support
- Better newline handling - StringAttrs.mergeAttributes fix - MarkdownStyles blockStartParagraphStyle, blockEndParagraphStyle - Tests
1 parent 92f4aa0 commit ab4df86

6 files changed

Lines changed: 354 additions & 110 deletions

File tree

Sources/MarkdownToAttributedString/AttributedStringVisitor.swift

Lines changed: 132 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ struct AttributedStringVisitor: MarkupVisitor {
2222
private var shouldAddCustomAttr: Bool {
2323
return formattingOptions?.addCustomMarkdownElementAttributes ?? false
2424
}
25+
private var currentUnorderedListEl: MarkdownElementAttribute?
26+
private var currentUnorderedListLevel: Int = 0
27+
private var currentOrderedListEl: MarkdownElementAttribute?
28+
private var currentOrderedListLevel: Int = 0
29+
2530
private let loggingQ = DispatchQueue(label: "MTAS.logging")
2631

2732
init(markdown: String,
@@ -38,7 +43,7 @@ struct AttributedStringVisitor: MarkupVisitor {
3843
let document = Document(parsing: markdown)
3944
visit(document)
4045

41-
// Apparently something in SwiftMarkdown is sometimes adding a non-breakable space at the end. Remove it.
46+
// Apparently something in SwiftMarkdown is sometimes adding a zero-width space at the end. Remove it.
4247
if attributedString.string.hasSuffix("\u{200B}") {
4348
attributedString.deleteCharacters(in: NSRange(location: attributedString.length - 1, length: 1))
4449
}
@@ -78,7 +83,15 @@ struct AttributedStringVisitor: MarkupVisitor {
7883

7984
mutating func visitInlineHTML(_ inlineHTML: InlineHTML) {
8085
if inlineHTML.rawHTML == "<br>" {
81-
appendNewline()
86+
debugLog("<open>", file: "")
87+
if shouldAddCustomAttr {
88+
var attrs = markdownStyles.baseAttributes
89+
attrs.mergeAttributes([.forcedLineBreak: true])
90+
appendNewline(attrs: attrs)
91+
} else {
92+
appendNewline()
93+
}
94+
debugLog("<close>", file: "")
8295
}
8396
}
8497

@@ -92,7 +105,15 @@ struct AttributedStringVisitor: MarkupVisitor {
92105
visitChildren(of: paragraph)
93106

94107
if paragraph.hasSuccessor {
95-
appendNewline()
108+
var attrs = markdownStyles.baseAttributes
109+
110+
// If this paragraph is part of a container block, be sure to attach the block to the newline's attrs
111+
if paragraph.isChildOfUnorderedList, let currentUnorderedListEl {
112+
attrs[.markdownElements] = MarkdownElementAttributes([.unorderedList: currentUnorderedListEl])
113+
} else if paragraph.isChildOfOrderedList, let currentOrderedListEl {
114+
attrs[.markdownElements] = MarkdownElementAttributes([.orderedList: currentOrderedListEl])
115+
}
116+
appendNewline(attrs: attrs)
96117
}
97118

98119
debugLog("<close>", file: "")
@@ -133,7 +154,6 @@ struct AttributedStringVisitor: MarkupVisitor {
133154
if parent is Strong {
134155
if let baseFont = styleAttrs[.font] as? CocoaFont {
135156
if shouldAddCustomAttr {
136-
137157
styleAttrs.addMarkdownElementAttr(
138158
MarkdownElementAttribute(elementType: .strong)
139159
)
@@ -191,22 +211,28 @@ struct AttributedStringVisitor: MarkupVisitor {
191211

192212
debugLog("<close>", file: "")
193213
}
194-
195-
/// NB about lists and SwiftMarkdown: SM considers *each* top level list item a separate list, so you can expect this to be called recursively once for each top level item. (Which yes, makes handling newlines a challenge.)
214+
215+
/// NB about lists and SwiftMarkdown: SM considers nested lists a separate list, i.e. a list can be a child of a list item, so you can expect this to be called recursively for each nested list.
196216
mutating func visitUnorderedList(_ unorderedList: UnorderedList) {
197217
guard optionsSupportEl(.unorderedList) else {
198218
appendNewline()
199219
debugLog("Skipping unsupported: unorderedList"); return
200220
}
201221
debugLog("<open>", file: "")
202-
203-
var styleAttrs = markdownStyles.attributesForType(.unorderedList)
222+
204223
let previousAttributes = currentAttributes.deepCopy()
224+
var styleAttrs = markdownStyles.attributesForType(.unorderedList)
225+
226+
if (currentUnorderedListEl == nil) {
227+
assert(currentUnorderedListLevel == 0)
228+
currentUnorderedListEl = MarkdownElementAttribute(elementType: .unorderedList)
229+
}
230+
231+
currentUnorderedListLevel += 1
205232

206233
if shouldAddCustomAttr {
207-
styleAttrs.addMarkdownElementAttr(
208-
MarkdownElementAttribute(elementType: .unorderedList)
209-
)
234+
guard let currentUnorderedListEl else { assertionFailure(); return }
235+
styleAttrs.addMarkdownElementAttr(currentUnorderedListEl)
210236
}
211237

212238
currentAttributes.mergeAttributes(styleAttrs)
@@ -222,6 +248,11 @@ struct AttributedStringVisitor: MarkupVisitor {
222248
if unorderedList.hasSuccessor {
223249
appendNewline()
224250
}
251+
252+
currentUnorderedListLevel -= 1
253+
if currentUnorderedListLevel == 0 {
254+
currentUnorderedListEl = nil
255+
}
225256

226257
currentAttributes = previousAttributes.deepCopy()
227258

@@ -230,39 +261,53 @@ struct AttributedStringVisitor: MarkupVisitor {
230261

231262
mutating func visitOrderedList(_ orderedList: OrderedList) {
232263
guard optionsSupportEl(.orderedList) else {
233-
debugLog("Skipping unsupported: orderedList")
234264
appendNewline()
235-
return
265+
debugLog("Skipping unsupported: orderedList"); return
236266
}
237267
debugLog("<open>", file: "")
238-
var styleAttrs = markdownStyles.attributesForType(.orderedList)
268+
239269
let previousAttributes = currentAttributes.deepCopy()
270+
var styleAttrs = markdownStyles.attributesForType(.orderedList)
271+
272+
if (currentOrderedListEl == nil) {
273+
assert(currentOrderedListLevel == 0)
274+
currentOrderedListEl = MarkdownElementAttribute(elementType: .orderedList)
275+
}
240276

277+
currentOrderedListLevel += 1
278+
241279
if shouldAddCustomAttr {
242-
styleAttrs.addMarkdownElementAttr(
243-
MarkdownElementAttribute(elementType: .orderedList)
244-
)
280+
guard let currentOrderedListEl else { assertionFailure(); return }
281+
styleAttrs.addMarkdownElementAttr(currentOrderedListEl)
245282
}
246283

247284
currentAttributes.mergeAttributes(styleAttrs)
248285

249-
var itemIndex = 1
250286
for child in orderedList.children {
251287
if let listItem = child as? ListItem {
252-
visitListItem(listItem, index: itemIndex)
253-
itemIndex += 1
288+
visitListItem(listItem, orderedIndex: Int(orderedList.startIndex) + child.indexInParent)
254289
} else {
255290
visit(child)
256291
}
257292
}
258293

294+
if orderedList.hasSuccessor {
295+
appendNewline()
296+
}
297+
298+
currentOrderedListLevel -= 1
299+
if currentOrderedListLevel == 0 {
300+
currentOrderedListEl = nil
301+
}
302+
259303
currentAttributes = previousAttributes.deepCopy()
260304

261305
debugLog("<close>", file: "")
262306
}
263307

264308

265-
mutating func visitListItem(_ listItem: ListItem, index: Int? = nil) {
309+
// orderedIndex is non-nil when part of an ordered list
310+
mutating func visitListItem(_ listItem: ListItem, orderedIndex: Int? = nil) {
266311
guard optionsSupportEl(.listItem) else {
267312
debugLog("Skipping unsupported: listItem")
268313
appendNewline()
@@ -274,11 +319,22 @@ struct AttributedStringVisitor: MarkupVisitor {
274319

275320
currentAttributes.mergeAttributes(styleAttrs)
276321

322+
if listItem.isFirst,
323+
let pstyle = markdownStyles.blockStartParagraphStyle
324+
{
325+
currentAttributes.mergeAttributes([.paragraphStyle: pstyle])
326+
} else if listItem.isLast,
327+
let pstyle = markdownStyles.blockEndParagraphStyle
328+
{
329+
currentAttributes.mergeAttributes([.paragraphStyle: pstyle])
330+
}
331+
332+
277333
let prefix: String
278334
let renderedDelimiter: String
279-
if let index = index {
280-
prefix = "\(index). "
281-
renderedDelimiter = markdownStyles.unorderedListBullets[0]
335+
if let orderedIndex = orderedIndex {
336+
prefix = "\t\(orderedIndex). "
337+
renderedDelimiter = "\(orderedIndex)"
282338
} else {
283339
let bullets = markdownStyles.unorderedListBullets
284340
renderedDelimiter = bullets[listItem.listDepth % bullets.count]
@@ -287,7 +343,8 @@ struct AttributedStringVisitor: MarkupVisitor {
287343
}
288344

289345
if shouldAddCustomAttr {
290-
var typedDelimiter = "-"
346+
// For ordered lists we can consider the typedDelimiter and renderedDelimiter to be the same.
347+
var typedDelimiter = orderedIndex != nil ? renderedDelimiter : "-"
291348
if let lowerBound = listItem.range?.lowerBound,
292349
let char = markdown.characterAt(line: lowerBound.line, col: lowerBound.column)
293350
{
@@ -298,6 +355,7 @@ struct AttributedStringVisitor: MarkupVisitor {
298355
ListItemMarkdownElementAttribute(
299356
listDepth: listItem.listDepth,
300357
indexInParent: listItem.indexInParent,
358+
orderedIndex: orderedIndex,
301359
prefix: prefix,
302360
typedDelimiter: typedDelimiter,
303361
renderedDelimiter: renderedDelimiter)
@@ -309,8 +367,9 @@ struct AttributedStringVisitor: MarkupVisitor {
309367

310368
visitChildren(of: listItem)
311369

312-
if listItem.hasSuccessor {
313-
appendNewline()
370+
// Don't add a newline if this had child lists, because those child list items add their own newlines
371+
if !listItem.hasChildList {
372+
appendNewline(attrs: currentAttributes)
314373
}
315374

316375
currentAttributes = previousAttributes.deepCopy()
@@ -507,15 +566,16 @@ struct AttributedStringVisitor: MarkupVisitor {
507566
attributedString.append(NSAttributedString(string: string, attributes: actualAttrs))
508567
}
509568

510-
private mutating func appendNewline() {
569+
private mutating func appendNewline(attrs: StringAttrs? = nil) {
511570
debugLog("appending newline")
512-
appendPlainText("\n")
571+
appendPlainText("\n", attrs: attrs)
513572
}
514573

515-
private func appendPlainText(_ plainText: String) {
574+
private func appendPlainText(_ plainText: String, attrs: StringAttrs? = nil) {
575+
let actualAttrs = attrs ?? markdownStyles.baseAttributes
516576
attributedString.append(NSAttributedString(
517577
string: plainText,
518-
attributes: markdownStyles.baseAttributes))
578+
attributes: actualAttrs))
519579
}
520580

521581
private func debugLog(_ message: String, file: String = #file, line: Int = #line, function: String = #function) {
@@ -533,7 +593,6 @@ struct AttributedStringVisitor: MarkupVisitor {
533593
}
534594

535595
extension ListItem {
536-
537596
// Nesting depth of the list item; 0 indexed.
538597
var listDepth: Int {
539598
var depth = 0
@@ -546,6 +605,21 @@ extension ListItem {
546605
}
547606
return max(0, depth - 1)
548607
}
608+
609+
var isFirst: Bool {
610+
return indexInParent == 0 && listDepth == 0
611+
}
612+
613+
var isLast: Bool {
614+
guard let parent = self.parent else {
615+
return false
616+
}
617+
return indexInParent == parent.childCount - 1
618+
}
619+
620+
var hasChildList: Bool {
621+
return children.contains { $0 is OrderedList || $0 is UnorderedList }
622+
}
549623
}
550624

551625
extension Markup {
@@ -565,19 +639,41 @@ extension Markup {
565639
return NSRange(location: start,
566640
length: range.upperBound.column - start - 1)
567641
}
642+
643+
var isChildOfUnorderedList: Bool {
644+
var current: Markup? = self
645+
while let parent = current?.parent {
646+
if parent is UnorderedList {
647+
return true
648+
}
649+
current = parent
650+
}
651+
return false
652+
}
653+
654+
var isChildOfOrderedList: Bool {
655+
var current: Markup? = self
656+
while let parent = current?.parent {
657+
if parent is OrderedList {
658+
return true
659+
}
660+
current = parent
661+
}
662+
return false
663+
}
568664
}
569665

570666
extension StringAttrs {
571667
mutating func mergeAttributes(_ otherAttrs: StringAttrs) {
572668
for (key, val) in otherAttrs {
573669
if key == .markdownElements, let val = val as? MarkdownElementAttributes {
574-
var attrs = (self[.markdownElements] as? MarkdownElementAttributes)?.copy() as? MarkdownElementAttributes ?? MarkdownElementAttributes()
670+
let elAttrs = (self[.markdownElements] as? MarkdownElementAttributes)?.copy() as? MarkdownElementAttributes ?? MarkdownElementAttributes()
575671

576-
for (markupType, newAttribute) in val.allAttributes {
577-
attrs.set(markupType, value: newAttribute)
672+
for (_, newAttribute) in val.allAttributes {
673+
elAttrs.add(newAttribute)
578674
}
579675

580-
self[.markdownElements] = attrs
676+
self[.markdownElements] = elAttrs
581677
} else {
582678
self[key] = val
583679
}

Sources/MarkdownToAttributedString/Extras.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ public extension NSAttributedString {
2121
if let font = nextStr.attribute(.font, at: 0, effectiveRange: nil) as? CocoaFont {
2222
str += "<Font name=“\(font.compatibleDisplayName)”>"
2323
}
24-
24+
if let _ = nextStr.attribute(.forcedLineBreak, at: 0, effectiveRange: nil) {
25+
str += "<MTASForcedLineBreak>"
26+
}
27+
if let _ = nextStr.attribute(.paragraphBreak, at: 0, effectiveRange: nil) {
28+
str += "<MTASParagraphBreak>"
29+
}
2530
if let markdownEls = nextStr.attribute(.markdownElements, at: 0, effectiveRange: nil) as? MarkdownElementAttributes {
2631
for (_, val) in markdownEls.allAttributes {
2732
str += val.betterDescriptionMarker
@@ -73,6 +78,17 @@ extension NSAttributedString {
7378
}
7479
return attributes(at: loc, effectiveRange: nil)
7580
}
81+
82+
func allAttributeRuns() -> [(NSRange, [NSAttributedString.Key: Any])] {
83+
var runs: [(NSRange, [NSAttributedString.Key: Any])] = []
84+
85+
self.enumerateAttributes(in: NSRange(location: 0, length: self.length), options: []) { attributes, range, _ in
86+
runs.append((range, attributes))
87+
}
88+
89+
return runs
90+
}
91+
7692
}
7793

7894
extension String {

0 commit comments

Comments
 (0)