The Maaku framework provides a Swift wrapper around cmark-gfm with the addition of a Swift friendly representation of the AST. gfm extensions for tables, strikethrough, autolinks, and tag filters are supported.
Maaku also supports a convention for plugins that custom renderers can use. One plugin is provided as an example.
TexturedMaaku builds on top of Maaku together with Texture to provide a native iOS CommonMark rendering framework in Swift.
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
$ gem install cocoapods
To integrate Maaku into your Xcode project using CocoaPods, specify it in your Podfile
:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'Maaku'
end
Then, run the following command:
$ pod install
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with Homebrew using the following command:
$ brew update
$ brew install carthage
To integrate Maaku into your Xcode project using Carthage, specify it in your Cartfile
:
github "KristopherGBaker/Maaku" ~> 0.6.0
Run carthage update
to build the framework and drag the built Maaku.framework
into your Xcode project.
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift
compiler. It is in early development, but Maaku does support its use on supported platforms.
Once you have your Swift package set up, adding Maaku as a dependency is as easy as adding it to the dependencies
value of your Package.swift
.
dependencies: [
.package(url: "https://github.com/KristopherGBaker/Maaku.git", from: "0.6.0")
]
$ swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.14"
$ swift test -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.14"
Document
is the primary interface for using Maaku. Document can be intialized by passing Data or String representing the CommonMark.
let document = try Document(text: commonMark)
Initializing a Document will parse the CommonMark and create an AST you can access. The document contains a list of the top level block elements, which each may contain either other block elements or inline elements. Block elements may be either container blocks or leaf blocks. Container blocks may contain other blocks, while leaf blocks may not.
Core types that adopt the Node protocol support conversion to NSAttributedString using the attributedText
method (most types are supported, but there are currently some limits, in particular with inline images and HTML - both inline and blocks).
The fonts and colors used by the attributedText method can be specified using the Style
type. A default implementation of Style
is provided with DefaultStyle
.
The CMark* types (inspired in part by CocoaMarkdown) provide a Swift friendly interface on top of cmark-gfm. The CMark types can be used on their own without Core by including only the Maaku/CMark
subspec in your Podfile.
Plugins follow a convention where the plugin appears in the form of a single link in a paragraph in the CommonMark text. Here's an example of what a youtube plugin might look like in CommonMark:
Some other markdown text.
[youtubevideo](https://youtu.be/kkdBB1hVLX0)
More markdown.
To add support for this plugin, we need to implement two protocols: Plugin and PluginParser (defined in Plugin.swift and shown below for reference).
public protocol Plugin: LeafBlock {
static var pluginName: PluginName { get }
}
public protocol PluginParser {
var name: String { get }
func parse(text: String) -> Plugin?
}
The Youtube plugin is provided as an example in the framework (as an optional subspec if you install with CocoaPods).
public struct YoutubePlugin: Plugin {
public static let pluginName: PluginName = "youtubevideo"
public let url: URL
public var videoId: String? {
return url.path.components(separatedBy: "/").last
}
public init(url: URL) {
self.url = url
}
}
The pluginName
value should be unique to the plugin. It can be the same as the name
used for the PluginParser, but does not need to be.
public struct YoutubePluginParser: PluginParser {
public let name = "youtubevideo"
public func parse(text: String) -> Plugin? {
guard let url = parseURL(text) else {
return nil
}
return YoutubePlugin(url: url)
}
public init() {
}
}
The name
value should match the link text you use for the the plugin. Since the Youtube plugin looks like [youtubevideo](https://youtu.be/kkdBB1hVLX0)
, youtubevideo
is used for the name
.
The raw link destination is passed to the parse method. You can decide how to deal with the text value to initialize your plugin, but there are convenience methods available to plugins to ease this process.
The splitPluginParams
method supports the following format for multiple plugin parameters in the link destination.
param1::value1|param2::value2|param3::value3
splitPluginParams
will split parameters in that format into a dictionary which you can use to initialize your plugin.
Let's say a Youtube plugin looked like this:
[youtubevideo](source::https://youtu.be/kkdBB1hVLX0||caption::Checkout this video)
Then the Plugin would be updated to look like:
public struct YoutubePlugin: Plugin {
public static let pluginName: PluginName = "youtubevideo"
public let url: URL
public let caption: String?
public var videoId: String? {
return url.path.components(separatedBy: "/").last
}
public init(url: URL, caption: String?) {
self.url = url
self.caption = caption
}
}
And the PluginParser might look like:
public struct YoutubePluginParser: PluginParser {
public let name = "youtubevideo"
public func parse(text: String) -> Plugin? {
let parameters = splitPluginParams(text)
guard parameters.count > 0,
let source = parameters["source"],
let url = URL(string: source) else {
return nil
}
let caption = parameters["caption"]
return YoutubePlugin(url: url, caption: caption)
}
public init() {
}
}
The PluginParser must be registered with PluginManager
before it will be used by the Maaku parser. If you don't register the plugin, it will instead appear as either a Link or Text rather than a Plugin.
To register the PluginParser, intialize it and pass it to PluginManager.registerParsers
.
For the Youtube example:
PluginManager.registerParsers([YoutubePluginParser()])