-
Notifications
You must be signed in to change notification settings - Fork 192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question: guidelines for how to implement visitBlockQuote for NSAttributedString renderer? #198
Comments
cc @christianselig in case you have some pointers. But also, if you're looking for more of a discussion forum to ask questions in, you might have better luck in the Swift Forums. Since Swift-Markdown is part of the Swift-DocC project, asking in that subforum might get you in front of people who have tried to do the same thing. |
Thanks for the quick response. |
Posted on Swift-DocC subforum: |
Thanks for the ping y'all! So cool you're using Markdownosaur or it at least helped :D It definitely grew some when I was using it in my app Apollo, and I'm 99% sure Apollo supported nested blockquotes so hopefully this works. Some of it is specific to Apollo but hopefully should be general enough overall to be helpful. If this doesn't solve your problem let me know and I'll poke around the codebase more haha. Here's the most recent implementation I had of mutating func visitBlockQuote(_ blockQuote: BlockQuote) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in blockQuote.children {
let quoteAttributedString = visit(child).mutableCopy() as! NSMutableAttributedString
// Simple algorithm: iterate over all the existing indents and increase their indent (due to being in a blockquote). If we didn't find any existing indents to increase, introduce the indent manually.
var didIndentExisting = false
quoteAttributedString.enumerateAttribute(.indent, in: NSRange(location: 0, length: quoteAttributedString.length), options: []) { value, range, stop in
guard value != nil else { return }
let paragraphStyle = (quoteAttributedString.attribute(.paragraphStyle, at: range.location, effectiveRange: nil) as! NSParagraphStyle).mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.headIndent += 15.0
paragraphStyle.tabStops = paragraphStyle.tabStops.map { NSTextTab(textAlignment: $0.alignment, location: $0.location + 15.0) }
quoteAttributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
didIndentExisting = true
}
if !didIndentExisting {
// No existing indents, so introduce an indent
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 15.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: 15.0)]
let tabAttributedString = NSAttributedString(string: "\t", attributes: [.paragraphStyle: paragraphStyle, .font: UIFont.systemFont(ofSize: FontManager.shared.regularFont().pointSize), .indent: true])
if child is CodeBlock || child is Paragraph {
// For codeblocks we have to do things a little differently as they span multiple lines, so instead of just inserting a single tab character at the beginning, we also have to insert one at the beginning of each line
// This is also true of Paragraphs, as if they use the single newline trick they can have multiple lines needing to be indented separately (see https://redd.it/ul6261)
quoteAttributedString.insert(tabAttributedString, at: 0)
// Insert for the rest of the lines
let regex = try! NSRegularExpression(pattern: "\n", options: [])
// Track how many we've found because with each insertion we'll be increasing the offset by 1 and we need to account for that
var totalFound = 0
regex.enumerateMatches(in: quoteAttributedString.string, options: [], range: NSRange(location: 0, length: quoteAttributedString.length)) { result, flags, stop in
guard let result = result else { return }
// Insert immediately after the newline, plus remembering to offset for each insertion
quoteAttributedString.insert(tabAttributedString, at: result.range.location + 1 + totalFound)
totalFound += 1
}
} else {
quoteAttributedString.insert(tabAttributedString, at: 0)
}
}
quoteAttributedString.addAttribute(.blockQuote, value: true)
// Add the quote depth attribute, but only to parts of the attributed string that don't already have one (we don't want to overwrite).
// Note that enumerated attributed strings is weird, it enumerates the substrings where the attribute *isn't*, as well as the ones where it is, so checking value is important
quoteAttributedString.enumerateAttribute(.quoteDepth, in: NSRange(location: 0, length: quoteAttributedString.length), options: []) { value, range, stop in
guard value == nil else { return }
quoteAttributedString.addAttribute(.quoteDepth, value: blockQuote.quoteDepth, range: range)
}
// Similar to the above with quote depth, we don't want to overwrite the code block syntax highlighting colors, so only do it where it isn't
quoteAttributedString.enumerateAttribute(.codeBlock, in: NSRange(location: 0, length: quoteAttributedString.length), options: []) { value, range, stop in
guard value == nil else { return }
let quoteColor: UIColor = {
if let forceTextColor = forceTextColor {
// If we're forcing a text color, make it a slightly lighter version of it via transparency
return forceTextColor.withAlphaComponent(0.7)
} else {
return ThemeManager.shared.currentTheme.textColor(for: .small, isRead: false)
}
}()
quoteAttributedString.addAttribute(.foregroundColor, value: quoteColor, range: range)
}
// By changing the text color of anything that isn't a code block, you probably just changed link colors to gray too (which should be tint colored).
// As a result, just go back over all the link attributes and re-color them.
// Note: yes it would be nice if there was an API where you could enumerate over either code blocks or link attributes, but this seems like the cleanest alternative.
quoteAttributedString.enumerateAttribute(.apolloLink, in: NSRange(location: 0, length: quoteAttributedString.length), options: []) { value, range, stop in
guard value != nil else { return }
let linkColor = forceTextColor ?? ThemeManager.shared.currentTheme.linkTintColor(isRead: false)
quoteAttributedString.addAttribute(.foregroundColor, value: linkColor, range: range)
}
result.append(quoteAttributedString)
}
if blockQuote.hasValidSuccessor(parseTablesInline: parseTablesInline) {
result.append(.doubleNewline(withFontSize: FontManager.shared.regularFont().pointSize))
}
return result
} And a bonus extension: extension BlockQuote {
/// Depth of the quote if nested within others. Index starts at 0.
var quoteDepth: Int {
var index = 0
var currentElement = parent
while currentElement != nil {
if currentElement is BlockQuote {
index += 1
}
currentElement = currentElement?.parent
}
return index
}
} |
@christianselig - thank you for responding so quickly. I will surely try to integrate this code into my app. BTW, I've just integrated Markdownosaur into my apps with only minor changes, and it works perfectly! Thank you so much for publishing this project. |
Yay that makes me so happy to hear! But truthfully Victoria and co are the real heroes of the effort, I just put it in a little box :p |
Sorry for creating an issue for a question. I couldn't find a Discussion section on this repository nor did I find a better place to post a question on this topic.
I am implementing NSAttributedString renderer using the Visitor pattern of the swift-markdown library and successfully targeted most elements. This is thanks to the code available at https://github.com/christianselig/Markdownosaur.
However, I am facing hard time with implementing visitBlockQuote, especially implementing multiple quote levels. This seems like a difficult task for the NSAttributedString renderer.
Can anyone kindly provide some guidelines, sample code, or any lead to help with that?
The text was updated successfully, but these errors were encountered: