written by Talaxy on 2021/5/5
Previous article native Markdown Rendering – SwiftUI
Markdown is a module of RoomTime that renders Markdown text. Those interested can go to Star 😊.
Markdown use
Markdown helps you render Markdown text in SwiftUI. You can also choose which syntax rules to enable, add custom rules, or completely customize the element view.
The basic use
If you want to simply render markdown text, you can:
import Markdown
struct MarkdownDemo: View {
let text: String
var body: some View {
ScrollView {
Markdown(text: text) { element in
ElementView(element: element)
}
.padding()
}
}
}
Copy the code
In this Demo, we passed Markdown a text, the Markdown text, and an ElementView mapping component, ElementView. ElementView tells Markdown what view each element should present.
Element Element
Currently, Markdown has the following elements (and their attributes) :
-
HeaderElement title
Property Type Description title String
The title content level Int
The title level -
QuoteElement reference
Property Type Description element [Element]
Reference to an embedded element in a block -
CodeElement title
Property Type Description lines [String]
Lines of code lang String?
Language annotation -
OrderListElement An ordered list
Property Type Description offset Int
The serial number offset items [[Element]]
List element group -
UnorderListElement Unordered list
Property Type Description sign UnorderListElement.Sign
A list of symbols items [[Element]]
List element group -
TableElement form
Property Type Description heads [String]
Header row aligns [TableElement.Alignment]
Column alignment rows [[String]]
Content of the line -
BorderElement divider (no attributes)
-
LineElement Plain text
Property Type Description text String
The text content
Custom element view
Here I can give the ElementView implementation code, using ViewBuilder switch syntax support:
public struct ElementView: View {
public let element: Element
public init(element: Element) {
self.element = element
}
public var body: some View {
switch element {
case let header as HeaderElement:
Header(element: header)
case let quote as QuoteElement:
Quote(element: quote) { item in
Markdown(elements: item) { element in
ElementView(element: element)
}
}
case let code as CodeElement:
Code(element: code)
case let orderList as OrderListElement:
OrderList(element: orderList) { item in
Markdown(elements: item) { element in
ElementView(element: element)
}
}
case let unorderList as UnorderListElement:
UnorderList(element: unorderList) { item in
Markdown(elements: item) { element in
ElementView(element: element)
}
}
case let table as TableElement:
Table(element: table)
case _ as BorderElement:
Border(a)case let line as LineElement:
Line(element: line)
default:
EmptyView()}}}Copy the code
Here, if you want to customize the view, you can follow the form above:
struct YourCustomElementView: View {
let element: Element
var body: some View {
switch element {
case let header as HeaderElement:
// customize your own `Header`, for example:
Text(header.title).bold()
case let quote as QuoteElement:
// ...
case let code as CodeElement:
// ...
/* some other cases */
default:
EmptyView()}}}Copy the code
Then use it directly in Markdown:
import Markdown
struct MarkdownDemo: View {
let text: String
var body: some View {
ScrollView {
Markdown(text: text) { element in
YourCustomElementView(element: element)
}
.padding()
}
}
}
Copy the code
Also, if you’re careful, you’ll notice that default in ElementView is set to EmptyView. This means that we can extend directly beyond the original ElementView (which I find very cool) :
import Markdown
struct MarkdownDemo: View {
let text: String
var body: some View {
ScrollView {
Markdown(text: text) { element in
ElementView(element: element)
switch element {
case let customElement as CustomElement:
CustomView(element: customElement)
/* other cases */
default:
EmptyView()
}
}
.padding()
}
}
}
Copy the code
You may have noticed that the Markdown component is also nested in Quote OrderList UnorderList, because these elements support content nesting, such as syntax nesting in text like this:
* fruit
- apple
- banana
- pine
* flow
1. eat
2. code!
3. sleep
Copy the code
Apply colours to a drawing mechanism
The profile
If you want to customize Markdown’s rendering rules, I think you need to understand the rendering mechanism inside Markdown first. Here’s what I think is a particularly clear picture of the principle:
This diagram shows roughly the input (Text) and output (View) of Markdown.
Markdown is internally split into a Resolver and an element ViewMapper. The Resolver’s job is to convert text to a set of elements, while a ViewMapper maps elements to views, such as the ElementView in the previous code presentation.
In fact, we can also see from Markdown’s initializer that it takes three arguments:
public struct Markdown<Content: View> :View {
public init(
text: String.resolver: Resolver? = Resolver(),
@ViewBuilder content: @escaping (Element) - >Content
)
}
Copy the code
Here I’d like to focus on Resolver, a text parser. Its initialization takes two parameters: a set of “partition rules” and a set of “mapping rules”.
public class Resolver {
public init(splitRules: [SplitRule].mapRules: [MapRule])
}
Copy the code
The Resolver works in two stages: “Spliting” and “Mapping, “which is text splitting and element Mapping.
Spliting Text splitting
At this stage, the parser first splits the text and marks the type of each small piece of text.
First, the parser converts the Text to Raw data. Raw is defined as follows:
public struct Raw: Hashable {
// Whether to allow partitioning
public let lock: Bool
// Text content
public let text: String
// The type marked
public let type: String?
public init(lock: Bool.text: String.type: String? = nil)
}
Copy the code
The parser then splits the text according to SplitRule. Here’s the definition of SplitRule:
// if you want to customize a split rule, simply inherit the 'SplitRule' class and implement the 'split(from:)' method
open class SplitRule {
public let priority: Double
public init(priority: Double)
// Text segmentation rules
open func split(from text: String)- > [Raw]
// Batch split, called by 'Resolver'
final func splitAll(raws: [Raw])- > [Raw]}Copy the code
Because the parser has a set of Splitrules, the parser first sorts the set of splitrules in ascending order of priority. The parser then calls splitAll(raws:) of SplitRule in turn to split any Raw whose lock is false.
This is the default split rule set for Markdown:
public let defaultSplitRules: [SplitRule] = [
// Preprocess whitespace, converting all whitespace characters (such as '\t') to pure whitespace
SpaceConvertRule(priority: 0),
// Splitter line fragment split
BorderSplitRule(priority: 0.5),
// List fragment split
ListSplitRule(priority: 1),
// Table fragment split
TableSplitRule(priority: 1.5),
// Block fragmentation
CodeBlockSplitRule(priority: 3),
// Indent code block fragment split
CodeIndentSplitRule(priority: 3.1),
// Title fragment split
HeaderSplitRule(priority: 4),
// Reference block fragment split
QuoteSplitRule(priority: 5),
// Line text split
LineSplitRule(priority: 6)]Copy the code
The point here is that although we are talking about splitting, we can also do other things with Raw, such as modifying Raw, or even discarding Raw. For example, SpaceConvertRule above is not a split function, but a modification of the original Raw.
Mapping element Mapping
At this stage, the parser subclasses Raw to Element in an orderly manner based on a set of MapRules, also based on priority. This is the definition of MapRule and Element:
// if you want to customize a mapping rule, simply inherit the 'SplitRule' class and implement the 'map(from:)' method
open class MapRule {
public let priority: Double
public init(priority: Double)
// Raw mapping rule
open func map(from raw: Raw.resolver: Resolver?). -> Element?
}
// If you want to customize an Element type, you need to inherit the 'Element' class
open class Element: Identifiable {
public let id = UUID(a)public init(a)
}
Copy the code
After [Raw] is converted to [Element], the parser passes [Element] to Markdown as its attribute. Markdown also provides another constructor:
public struct Markdown<Content: View> :View {
public let elements: [Element]
public let content: (Element) - >Content
public init(
elements: [Element].@ViewBuilder content: @escaping (Element) - >Content
)
}
Copy the code
This is the default mapping rule group for Markdown:
public let defaultMapRules: [MapRule] = [
HeaderMapRule(priority: 0),
QuoteMapRule(priority: 1),
CodeMapRule(priority: 2),
ListMapRule(priority: 3),
TableMapRule(priority: 3.5),
BorderMapRule(priority: 4),
LineMapRule(priority: 5)]Copy the code
Customize a set of grammar rules
Based on the above rendering mechanism, we can add some custom syntax. It may be a bit of a step, but it’s worth it. Here’s an example of how to add custom syntax:
We expect bold yellow text lines starting with $. For example, “$warning” is a text line starting with a $.
First, we define segmentation rules, mapping rules, and elements:
import Markdown
class DollarLineElement: Element {
let text: String
init(text: String) {
self.text = text
}
}
fileprivate let dollerLineType = "doller"
fileprivate let dollerLineRegex = # "^ \ + $* $#"
fileprivate let dollerSignRegex = # ^ \ "+ $(? # =. * $)"
class DollarSplitRule: SplitRule {
override func split(from text: String)- > [Raw] {
// We can use the inherited 'split(by:text:type:)' method to quickly split text against the re
// But what I want to say here is that we need to confirm a good Raw type for MapRule to recognize
return split(by: dollerLineRegex, text: text, type: dollerLineType)
}
}
class DollarMapRule: MapRule {
override func map(from raw: Raw.resolver: Resolver?). -> Element? {
if raw.type = = dollerLineType {
// 'replace(by:with:)' is an extension of 'StringProtocol' in 'Markdown'
// It helps you quickly replace text according to the re
// The 'Markdown' module also provides reged-correlation methods, which will be described later
let line = raw.text.replace(by: dollerSignRegex, with: "")
return DollarLineElement(text: line)
} else {
return nil}}}Copy the code
Next, define the element view:
import SwiftUI
struct DollarLine: View {
let element: DollarLineElement
var body: some View {
Text(element.text)
.bold()
.foregroundColor(Color.yellow)
}
}
Copy the code
Then, configure Resolver and add custom rules:
let splitRules: [SplitRule] = defaultSplitRules + [
DollarSplitRule(priority: 4.5)]let mapRules: [MapRule] = defaultMapRules + [
DollarMapRule(priority: 4.5)]let resolver = Resolver(splitRules: splitRules, mapRules: mapRules)
Copy the code
Finally, apply everything to the Markdown component:
struct MarkdownDemo: View {
let text: String = """ # DollarLine $ Here is a dollar line. """
var body: some View {
ScrollView {
Markdown(text: text, resolver: resolver) { element in
// default view mapping
ElementView(element: element)
switch element {
case let dollarLine as DollarLineElement:
DollarLine(element: dollarLine)
default:
EmptyView()
}
}
.padding()
}
}
}
Copy the code
Here’s the final result:
Regular text processing
Markdown’s regular text processing support will definitely help if you’re comfortable with regular and don’t want to use NSRegularExpression directly. Markdown provides a number of extended support for StringProtocol:
// Single line processing
public extension StringProtocol {
// add the suffix '\n'
var withLine: String
// add the ' 'suffix
var withSpace: String
// add the suffix ','
var withComma: String
// Add the suffix '.'
var withDot: String
// remove the ' ', '\n' symbols at the beginning and end
func trimmed(a) -> String
// remove the first and last '\n' symbols
func trimLine(a) -> String
}
// The default regex option supports global multiple lines
public let lineRegexOption: NSRegularExpression.Options = [.anchorsMatchLines]
// Regular support
public extension StringProtocol {
// The number of prefixes
var preBlankNum: Int
// Whether the regular expression is satisfied
func match(
by regexText: String.options: NSRegularExpression.Options = lineRegexOption
) -> Bool
// Replace the content in the text that satisfies the re
func replace(
by regexText: String.with template: String.options: NSRegularExpression.Options = lineRegexOption
)
// If there is anything in the text that satisfies the re
func contains(
by regexText: String.options: NSRegularExpression.Options = lineRegexOption
) -> Bool
// The amount of content in the text that satisfies the re
func matchNum(
by regexText: String.options: NSRegularExpression.Options = lineRegexOption
) -> Int
// Returns the text content that matches the re
func matchResult(
by rawRegex: String.options: NSRegularExpression.Options = lineRegexOption
)- > [String]
// Split the text content according to the re
func split(
by rawRegex: String.options: NSRegularExpression.Options = lineRegexOption
) -> RegexSplitResult
}
// split(by:options:) returns the type
public struct RegexSplitResult {
// range indicates the subscript range of the match, and match indicates whether the match is performed
public typealias Result = (range: Range<String.Index>, match: Bool)
public let raw: String
public let result: [Result]}Copy the code
conclusion
Only the first release of the open source project is currently available, and those interested can poke RoomTime/Markdown. A Star is a great support.
Thank you for reading!