I am using the swift-markdown library in my app.
I am trying to figure out how to add my own custom Markup
to the MarkupVisitor
?
I have been able to use a workaround using visitInlineAttributes
which applies to ^[]()
.
Is there a way to add my own custom Markup?
For example, if I wanted my own inline markup where text inside @[text]
would have gray color? Similar to how **bold** makes things bold.
Someone asked this question in the library but I don't understand how to implement the only answer there.
https://github.com/swiftlang/swift-markdown/issues/122
Here's a below example I have:
import UIKit
import Markdown
import SnapKit
class ViewController: UIViewController, UITextViewDelegate {
let label = UITextView()
override func viewDidLoad() {
super.viewDidLoad()
label.isEditable = false
label.delegate = self
label.linkTextAttributes = [:]
view.addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide).inset(10)
}
var md = MarkdownParser()
let attr = md.attributedString(from: "Hello World! Here's some text which is **bold** and here's some which is *italics*. Here's some ^[[metadata containing link](https://old.reddit.com/r/SaaS/comments/1fgv248/fuck_founder_mode_work_in_fuck_off_mode/)]() which is gray and not underlined. And here's some normal [link](https://hckrnews.com) which is underlined and red. You are welcome!")
print("ATTR:\n\n\(attr)")
label.attributedText = attr
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print("shouldInteractWith: \(URL)")
return true
}
}
struct MarkdownParser: MarkupVisitor {
typealias Result = NSMutableAttributedString
mutating func attributedString(from : String) -> NSMutableAttributedString {
let document = Document(parsing: from)
print(document.debugDescription())
return NSMutableAttributedString(attributedString: visit(document))
}
mutating func visit(_ markup: Markup) -> NSAttributedString {
return markup.accept(&self)
}
mutating func defaultVisit(_ markup: any Markdown.Markup) -> NSMutableAttributedString {
let result = NSMutableAttributedString()
for child in markup.children {
result.append(visit(child))
}
return result
}
mutating func visitText(_ text: Text) -> NSMutableAttributedString {
return NSMutableAttributedString(string: text.plainText, attributes: [.font:UIFont.systemFont(ofSize: 18, weight: .regular),.foregroundColor:UIColor.label])
}
mutating func visitEmphasis(_ emphasis: Emphasis) -> NSMutableAttributedString {
return doVisit(emphasis)
}
mutating func visitStrong(_ strong: Strong) -> NSMutableAttributedString {
return doVisit(strong)
}
mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> NSMutableAttributedString {
return doVisit(attributes)
}
mutating func visitLink(_ link: Link) -> NSMutableAttributedString {
return doVisit(link)
}
mutating func doVisit(_ markup: any Markup) -> NSMutableAttributedString {
let result = NSMutableAttributedString()
for child in markup.children {
result.append(visit(child))
}
switch markup {
case is Strong:
fallthrough
case is Emphasis:
result.enumerateAttribute(.font, in: result.fullRange, options: []) { value, range, stop in
if let newFont = (value as? UIFont)?.addTrait(trait: markup is Strong ? .traitBold : .traitItalic) {
result.addAttribute(.font, value: newFont, range: range)
}
}
case is InlineAttributes:
result.removeAttributes([.underlineStyle,.underlineColor])
result.addAttribute(.foregroundColor, value: UIColor.tertiaryLabel)
case is Link:
if let destination = (markup as? Link)?.destination, let url = URL(string: destination) {
let color = UIColor.systemRed
result.addAttributes([.underlineStyle : NSUnderlineStyle.single.rawValue, .underlineColor : color,.foregroundColor : color, .link : url])
}
default:
break
}
return result
}
}
extension NSAttributedString {
var fullRange : NSRange {
NSRange(location: 0, length: length)
}
}
extension NSMutableAttributedString {
func addAttribute(_ name: NSAttributedString.Key, value: Any) {
addAttribute(name, value: value, range: fullRange)
}
func addAttributes(_ attrs: [NSAttributedString.Key : Any]) {
addAttributes(attrs, range: fullRange)
}
func removeAttribute(_ name: NSAttributedString.Key) {
removeAttribute(name, range: fullRange)
}
func removeAttributes(_ names: [NSAttributedString.Key]) {
for attribute in names {
removeAttribute(attribute)
}
}
}
extension UIFont {
func addTrait(trait : UIFontDescriptor.SymbolicTraits) -> UIFont {
var traits = fontDescriptor.symbolicTraits
if traits.contains(trait) {
return self
} else {
traits.insert([trait])
return UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: pointSize)
}
}
}
This shows the following:
There is no general way to add a new type of Markup
that has arbitrary syntax (e.g. starting with @[
and ending with ]
). To do that you'd have to fork cmark and swift-markdown. From the documentation of Markup
,
All supported markup elements are already implemented in the framework. Use this protocol only as a generic constraint.
The GitHub you found is about parsing a BlockDirective
. This is kind of like extending the syntax, because you can assign different meanings to block directives with different names. For example, you can implement @Gray { ... }
meaning gray text, and @LaTeX { ... }
meaning LaTeX code. But at the end of the day, these are just BlockDirective
s. There is no new type of Markup
being parsed. It's always the same syntax - @
followed by the name, optionally followed by some arguments, optionally followed by the children.
If you want to adapt the solution in the linked GitHub issue, you need to pass the parseBlockDirectives
option when creating the Document
.
let document = Document(parsing: from, options: .parseBlockDirectives)
Then implement the corresponding visitor method
mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> NSMutableAttributedString {
return doVisit(blockDirective)
}
Then add a corresponding case in your switch
:
case is BlockDirective:
guard let block = markup as? BlockDirective, block.name == "Gray" else {
break
}
result.addAttribute(.foregroundColor, value: UIColor.tertiaryLabel)
However, this is a block container, not an inline one like **bold**
. That is, you must write it on a separate line in the markdown source:
Some Text
@Gray { Some Gray Text }
Some Other Text
If you want it to be inline, then InlineAttributes
is the intended way to do this. You are already doing this correctly.
The text inside a ^[]()
doesn't have to be a link. You can just write
^[Some Gray Text](gray)
and check for it like this:
case is InlineAttributes:
guard let attribute = markup as? InlineAttributes, attribute.attributes == "gray" else {
break
}
result.addAttribute(.foregroundColor, value: UIColor.tertiaryLabel)
Just like the name and arguments of a BlockDirective
, arbitrary data can be put into the ()
of a InlineAttributes
. You can implement ^[...](gray)
to mean gray text, ^[...](serif)
to mean the text should be written with a serif font, and so on.