Reputation: 174
I need to parse a markdown using the .full interpretedSyntax so that headings are also supported (.inlineOnly
and .inlineOnlyPreservingWhitespace
ignore the headings). Unfortunately, there is a side effect that all the new lines are being removed after parsing. My idea is to replace them with a (dummy Unicode character) heart, parse, and replace the hearts with new lines for the final output. Unfortunately, I have not found a way to replace the characters in an AttributedString
.
Here is my code / MRE:
struct ContentView: View {
@StateObject var vm = ViewModel()
var body: some View {
VStack(spacing: 50) {
VStack {
Text("before markdown")
.font(.system(size: 30))
Text(vm.markdownText)
}
VStack {
Text("after markdown")
.font(.system(size: 30))
if let attributedText = vm.attributedText {
Text(attributedText)
}
}
}
.padding()
}
}
class ViewModel: ObservableObject {
@Published private(set) var attributedText: AttributedString?
let markdownText = "##### **Lorem ipsum:** dolor sit\\n amet \\n consectetur adipiscing \\n\\n\\n elit"
init() {
self.attributedText = getAttributedText(text: markdownText)
}
func getAttributedText(text: String) -> AttributedString {
let textWithReplacements = text.replacingOccurrences(of: "\n", with: "\n♥")
guard let attributedText = try? AttributedString(
markdown: textWithReplacements,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible
),
baseURL: nil
) else {
return AttributedString(stringLiteral: markdownText)
}
var output = attributedText
let characterView = attributedText.characters
for i in characterView.indices {
if characterView[i] == "♥" {
// TODO - replace hearts with new lines
// output.characters.replace("♥", with: "\\n") // crash
// output.characters.remove(at: i) // crash
// output.characters.insert("\\n", at: i) // will only put one new line although there are multiple hearts...and the hearts remain so still need to be somehow removed
}
}
return output
}
}
Upvotes: 1
Views: 150
Reputation: 271105
The way to replace a part of an AttributedString
is through its subscript.
attributedString[someRange] = someNewAttributedSubstring
So you can just use range(of:)
to find the heart:
var output = attributedText
let newLine = AttributedString("\n")
while let range = output.range(of: "♥") {
output[range] = newLine[newLine.startIndex..<newLine.endIndex]
}
Note that we also access newLine
with a subscript [newLine.startIndex..<newLine.endIndex]
, in order to get a AttributedSubstring
. This arguably looks kind of ugly, but it is just how subscripts work unfortunately.
Replacing a new line with a heart might change how the markdown is parsed. The safest bet would be to replace it with a whitespace character, since a new line is also a white space character. There are many characters to choose from. For example, you can use U+200A HAIR SPACE
// also remember to change the argument to 'range(of:)'!
let textWithReplacements = markdown.replacingOccurrences(of: "\n", with: "\u{200A}\n")
The markdown parser would just add a PresentationIntent
when encountering a header, and SwiftUI Text
doesn't "understand" that and will just show the header with the same size as everything else. If you want a larger size, you can go through the runs
of the AttributedString
and look for the presentation intent.
For example, here I have made level 3 headers (###
in markdown) have the .title3
font.
for run in output.runs {
if run.presentationIntent?.components.map(\.kind).contains(.header(level: 3)) == true {
output[run.range].font = .title3
}
}
Full code:
struct ContentView: View {
var body: some View {
let md = "### **Lorem ipsum:** dolor sit\n amet \n consectetur adipiscing \n\n\n elit"
Text(getAttributedText(markdown: md))
}
}
func getAttributedText(markdown: String) -> AttributedString {
let textWithReplacements = markdown.replacingOccurrences(of: "\n", with: "\u{200A}\n")
guard let attributedText = try? AttributedString(
markdown: textWithReplacements,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible
),
baseURL: nil
) else {
return AttributedString(markdown)
}
var replacement = AttributedSubstring()
var output = attributedText
let newLine = AttributedString("\n")
while let range = output.range(of: "\u{200A}") {
output[range] = newLine[newLine.startIndex..<newLine.endIndex]
}
for run in output.runs {
if run.presentationIntent?.components.map(\.kind).contains(.header(level: 3)) == true {
output[run.range].font = .title3
}
}
return output
}
Upvotes: 2