I am trying to render some markdown. I am handling headers already, thanks to this SO question. But now, I would also like to handle the bullet points and indentation, but I cannot figure out how to do that.
In the code below, when I reach case .unorderedList, I would like to add a char resembling a bullet point and indentation (conditionally of course). I cannot mutate output[intentRange], which is of type AttributedSubstring.
Trying to do say output[intentRange] = "⏺️" + output[intentRange] throws an error telling me: Cannot assign value of type 'AttributedString' to subscript of type 'AttributedSubstring'.
I am holding it wrong for sure, can someone tell me how to hold it correctly?
Here's the extension:
#if os(macOS)
import AppKit
#else
import UIKit
#endif
extension AttributedString {
#if os(macOS)
typealias PlatformFont = NSFont
#else
typealias PlatformFont = UIFont
#endif
/// This method allows to render not only the inline markdown tags, but also the different headers, bullet points, todos etc.
///
/// Inspired by [this so question](https://stackoverflow.com/questions/70643384/how-to-render-markdown-headings-in-swiftui-attributedstring).
init?(styledMarkdown markdownString: String, baseSize: Double = 14) {
let output = try? AttributedString(
markdown: markdownString,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible
),
baseURL: nil
)
guard
var output = output
else { return nil }
for (intentBlock, intentRange) in output.runs[AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self].reversed() {
guard let intentBlock = intentBlock else { continue }
for intent in intentBlock.components {
switch intent.kind {
case .header(level: let level):
let fontName = PlatformFont.preferredFont(forTextStyle: .body)
.fontName
let scaleFactor = 0.25
switch level {
case 1:
output[intentRange].font =
.custom(fontName, size: baseSize * (1 + scaleFactor * 4), relativeTo: .title)
.bold()
case 2:
output[intentRange].font =
.custom(fontName, size: baseSize * (1 + scaleFactor * 3), relativeTo: .title)
.bold()
case 3:
output[intentRange].font =
.custom(fontName, size: baseSize * (1 + scaleFactor * 2), relativeTo: .title)
.bold()
case 4:
output[intentRange].font =
.custom(fontName, size: baseSize * (1 + scaleFactor * 1), relativeTo: .title)
.bold()
default:
break
}
case .unorderedList:
//TODO: check for `- [ ]` here to show a checkbox or a checkmark depending on whether the task is marked done
//TODO: check for tab increments here!
AppLogger.misc.debug("\(output[intentRange]))")
// output[intentRange] = "⏺️" + output[intentRange] // this fails w/: `Cannot assign value of type 'AttributedString' to subscript of type 'AttributedSubstring'`
default:
break
}
}
if intentRange.lowerBound != output.startIndex {
output.characters.insert(contentsOf: "\n", at: intentRange.lowerBound)
}
}
self = output
}
}