Skip to content

About

A framework for parsing and transforming Markdown written in Swift 6. The syntax is based on the CommonMark specification. The framework defines an abstract syntax for Markdown, provides parsers for parsing strings into abstract syntax trees, and comes with generators for HTML, attributed strings, plain text, and text with ANSI-markup.

Topics

Resources

Contributing

Stars

213 stars

Watchers

3 watching

Forks

Repository files navigation

Swift MarkdownKit

IDE: Xcode 26 Package managers: SwiftPM, Carthage License: Apache

Swift MarkdownKit is a framework for parsing, processing and displaying text in Markdown format. The supported syntax is based on the CommonMark Markdown specification (version 0.31.2). Swift MarkdownKit also provides two extended versions of the parser: ExtendedMarkdownParser (tables and definition lists) and FullMarkdownParser (additionally underline, strikethrough, and task lists).

Swift MarkdownKit defines an abstract syntax representation for Markdown, it provides a parser for parsing strings into abstract syntax trees, and comes with generators for creating output in plain text, HTML and attributed strings. There is also a generator which can be used to display Markdown documents in ANSI-compliant terminals.

Here is an overview of the supported Markdown features. The parser classes supporting a feature are given in parentheses (see also Markdown extensions):

  • CommonMark 0.31.2: headings, paragraphs, block quotes, lists, code blocks, thematic breaks, HTML, links, images, emphasis, code spans and line breaks (MarkdownParser, ExtendedMarkdownParser, FullMarkdownParser)
  • Tables with column alignment, in the style of GitHub Flavored Markdown (ExtendedMarkdownParser, FullMarkdownParser)
  • Definition lists (ExtendedMarkdownParser, FullMarkdownParser)
  • Underlined text: ~underlined~ (FullMarkdownParser)
  • Strikethrough text: ~~struck through~~ (FullMarkdownParser)
  • Task lists: - [ ] to do and - [x] done (FullMarkdownParser)
  • Custom syntax defined by extending the parser

 

Table of contents
1.  Parsing Markdown
   1.1  Using the framework
   1.2  Markdown extensions
   1.3  Configuring the parser
   1.4  Extending the parser
   1.5  Using MarkdownKit concurrently
2.  Processing Markdown
3.  Converting Markdown into other formats
   3.1  Generating text, HTML, and Attributed Strings
   3.2  Using the command-line tools
4.  Displaying Markdown with SwiftUI
5.  Known issues
6.  Requirements

 

Parsing Markdown

Using the framework

Class MarkdownParser provides a simple API for parsing Markdown in a string. The parser returns an abstract syntax tree representing the Markdown structure in the string:

let markdown = MarkdownParser.standard.parse("""
                 # Header
                 ## Sub-header
                 And this is a **paragraph**.
                 """)
print(markdown)

Executing this code will result in the following data structure of type Block getting printed:

document(heading(1, text("Header")),
         heading(2, text("Sub-header")),
         paragraph(text("And this is a "),
                   strong(text("paragraph")),
                   text(".")))

Block is a recursively defined enumeration of cases with associated values (also called an algebraic datatype). Case document refers to the root of a document. It contains a sequence of blocks. In the example above, two different types of blocks appear within the document: heading and paragraph. A heading case consists of a heading level (as its first argument) and heading text (as the second argument). A paragraph case simply consists of text.

Text is represented using the struct Text which is effectively a sequence of TextFragment values. TextFragment is yet another recursively defined enumeration with associated values. The example above shows two different TextFragment cases in action: text and strong. Case text represents plain strings. Case strong contains a Text object, i.e. it encapsulates a sequence of TextFragment values which are "marked up strongly".

Markdown extensions

Class ExtendedMarkdownParser has the same interface like MarkdownParser but supports tables and definition lists in addition to the block types defined by the CommonMark specification. Tables are based on the GitHub Flavored Markdown specification with one extension: within a table block, it is possible to escape newline characters to enable cell text to be written on multiple lines. Here is an example:

| Column 1     | Column 2       |
| ------------ | -------------- |
| This text \
  is very long | More cell text |
| Last line    | Last cell      |        

Definition lists are implemented in an ad hoc fashion. A definition consists of terms and their corresponding definitions. Here is an example of two definitions:

Apple
: Pomaceous fruit of plants of the genus Malus in the family Rosaceae.

Orange
: The fruit of an evergreen tree of the genus Citrus.
: A large round juicy citrus fruit with a tough bright reddish-yellow rind.

Class FullMarkdownParser supports everything that MarkdownKit implements. It is a subclass of ExtendedMarkdownParser (so it supports tables and definition lists) and adds inline markup for underlined text (~underlined~, text fragment underline) and struck-through text (~~struck through~~, text fragment strikethrough), as well as task list items (- [ ] to do and - [x] done, list type task). Tildes can also be used within words (foo~~bar~~baz), but runs of three or more tildes are plain text, as in GitHub Flavored Markdown. Unlike GFM, a single tilde does not produce strikethrough but underlined text. A task list item is a list item whose text starts with [ ], [x] or [X] and a space or tab; the type of such an item is ListType.task, which wraps the bullet or number of the item. The HTML generator creates a checkbox, the attributed string generator uses the characters ☐ and ☑, and the text generators write [ ] and [x] instead of the bullet. MarkdownParser and ExtendedMarkdownParser do not know this markup and leave tildes as they are, so existing code does not change its behavior. New Markdown features will be added to FullMarkdownParser.

let markdown = FullMarkdownParser.standard.parse("Some ~~old~~ and ~important~ text")

Configuring the parser

The Markdown dialect supported by MarkdownParser is defined by two parameters: a sequence of block parsers (each represented as a subclass of BlockParser), and a sequence of inline transformers (each represented as a subclass of InlineTransformer). The initializer of class MarkdownParser accepts both components optionally. The default configuration (neither block parsers nor inline transformers are provided for the initializer) is able to handle Markdown based on the CommonMark specification.

Since MarkdownParser objects are stateless (beyond the configuration of block parsers and inline transformers), there is a predefined default MarkdownParser object accessible via the static property MarkdownParser.standard. This default parsing object is used in the example above.

New markdown parsers with different configurations can also be created by subclassing MarkdownParser and by overriding the class properties defaultBlockParsers and defaultInlineTransformers. Here is an example of how class ExtendedMarkdownParser is derived from MarkdownParser by overriding defaultBlockParsers, by specializing standard in a covariant fashion, and by overriding the factory method documentParser(blockParsers:input:), which is needed for supporting definition lists.

open class ExtendedMarkdownParser: MarkdownParser {
  override open class var defaultBlockParsers: [BlockParser.Type] {
    return self.blockParsers
  }
  private static let blockParsers: [BlockParser.Type] = MarkdownParser.headingParsers + [
    IndentedCodeBlockParser.self,
    FencedCodeBlockParser.self,
    HtmlBlockParser.self,
    LinkRefDefinitionParser.self,
    BlockquoteParser.self,
    ExtendedListItemParser.self,
    TableParser.self
  ]
  override open class var standard: ExtendedMarkdownParser {
    return self.singleton
  }
  private static let singleton: ExtendedMarkdownParser = ExtendedMarkdownParser()
  open override func documentParser(blockParsers: [BlockParser.Type],
                                    input: String) -> DocumentParser {
    return ExtendedDocumentParser(blockParsers: blockParsers, input: input)
  }
}

Extending the parser

With version 1.1 of the MarkdownKit framework, it is now also possible to extend the abstract syntax supported by MarkdownKit. Both Block and TextFragment enumerations now include a custom case which refers to objects representing the extended syntax. These objects have to implement protocol CustomBlock for blocks and CustomTextFragment for text fragments.

Here is a simple example how one can add support for "underline" (e.g. this is ~underlined~ text) and "strikethrough" (e.g. this is using ~~strike-through~~) by subclassing existing inline transformers. FullMarkdownParser supports this markup out of the box, with the text fragments underline and strikethrough; the example implements the same markup as a custom extension in order to show how the mechanism works. Use a different delimiter if you want to try it together with FullMarkdownParser.

First, a new custom text fragment type has to be implemented for representing underlined and strike-through text. This is done with an enumeration which implements the CustomTextFragment protocol:

enum LineEmphasis: CustomTextFragment {
  case underline(Text)
  case strikethrough(Text)

  func equals(to other: CustomTextFragment) -> Bool {
    guard let that = other as? LineEmphasis else {
      return false
    }
    switch (self, that) {
      case (.underline(let lhs), .underline(let rhs)):
        return lhs == rhs
      case (.strikethrough(let lhs), .strikethrough(let rhs)):
        return lhs == rhs
      default:
        return false
    }
  }
  func transform(via transformer: InlineTransformer) -> TextFragment {
    switch self {
      case .underline(let text):
        return .custom(LineEmphasis.underline(transformer.transform(text)))
      case .strikethrough(let text):
        return .custom(LineEmphasis.strikethrough(transformer.transform(text)))
    }
  }
  func generateHtml(via htmlGen: HtmlGenerator) -> String {
    switch self {
      case .underline(let text):
        return "<u>" + htmlGen.generate(text: text) + "</u>"
      case .strikethrough(let text):
        return "<s>" + htmlGen.generate(text: text) + "</s>"
    }
  }
  func generateHtml(via htmlGen: HtmlGenerator,
                    and attrGen: AttributedStringGenerator?) -> String {
    return self.generateHtml(via: htmlGen)
  }
  var rawDescription: String {
    switch self {
      case .underline(let text):
        return text.rawDescription
      case .strikethrough(let text):
        return text.rawDescription
    }
  }
  var description: String {
    switch self {
      case .underline(let text):
        return "~\(text.description)~"
      case .strikethrough(let text):
        return "~~\(text.description)~~"
    }
  }
  var debugDescription: String {
    switch self {
      case .underline(let text):
        return "underline(\(text.debugDescription))"
      case .strikethrough(let text):
        return "strikethrough(\(text.debugDescription))"
    }
  }
}

Next, two inline transformers need to be extended to recognize the new emphasis delimiter ~:

final class EmphasisTestTransformer: EmphasisTransformer {
  override public class var supportedEmphasis: [Emphasis] {
    return super.supportedEmphasis + [
             Emphasis(ch: "~", special: false, factory: { double, text in
               return .custom(double ? LineEmphasis.strikethrough(text)
                                     : LineEmphasis.underline(text))
             })]
  }
}
final class DelimiterTestTransformer: DelimiterTransformer {
  override public class var emphasisChars: [Character] {
    return super.emphasisChars + ["~"]
  }
}

Finally, a new extended markdown parser can be created:

final class EmphasisTestMarkdownParser: MarkdownParser {
  override public class var defaultInlineTransformers: [InlineTransformer.Type] {
    return [DelimiterTestTransformer.self,
            CodeLinkHtmlTransformer.self,
            LinkTransformer.self,
            EmphasisTestTransformer.self,
            EscapeTransformer.self]
  }
  override public class var standard: EmphasisTestMarkdownParser {
    return self.singleton
  }
  private static let singleton: EmphasisTestMarkdownParser = EmphasisTestMarkdownParser()
}

The order of the inline transformers matters. In particular, EscapeTransformer needs to be the last transformer of the list, and it should not be left out:

  • It resolves backslash escapes. This includes the escapes in link destinations and titles, which only exist after LinkTransformer has created the links.
  • It decides which line endings are hard line breaks (a backslash or at least two spaces at the end of a line) and removes the trailing spaces of the other line endings. This has to happen after DelimiterTransformer has classified the delimiters (a * followed by a backslash is treated differently from a * at the end of a line), after CodeLinkHtmlTransformer has recognized code spans and HTML tags (a backslash or spaces at the end of a line are literal text in these, and the line ending is no line break), and after LinkTransformer has matched link reference labels (they are compared as written, including backslashes).

A parser without EscapeTransformer does not recognize hard line breaks; trailing spaces and backslashes at the end of lines remain in the text. Custom transformers for new inline markup, such as the ones above, belong in front of EscapeTransformer.

Using MarkdownKit concurrently

MarkdownKit is compiled in Swift 6 language mode and supports strict concurrency checking. Abstract syntax trees (Block, TextFragment, Text, ...), options and configuration types (such as RenderingOptions) are Sendable value types. Parsers (MarkdownParser, ExtendedMarkdownParser), generators (HtmlGenerator, StringGenerator, TerminalGenerator, AttributedStringGenerator), table renderers and the SyntaxHighlighter are immutable objects and Sendable as well (the highlighting configurations HighlightingConfig and AnsiHighlightingConfig are value types), so a single instance can be used from several threads or tasks at the same time. Parsing in a background task and using the resulting Block on the main actor works:

let doc = await Task.detached { ExtendedMarkdownParser.standard.parse(text) }.value
let attributedStr = try await AttributedStringGenerator.standard.generateAsync(doc: doc)

The classes are open, so their Sendable conformance is @unchecked. Subclasses restate it as @unchecked Sendable (the compiler warns otherwise) and have to make sure that state they add is immutable or synchronized. For the same reason, custom types implementing CustomBlock, CustomTextFragment or one of the TableRenderer protocols have to be Sendable. Parser internals, such as DocumentParser, InlineParser and BlockParser, are created for a single parse and are not Sendable. The synchronous methods of AttributedStringGenerator (generate(doc:) etc.) use the system's HTML importer, which Apple documents as usable on the main thread only; from other threads, use generateAsync.

Processing Markdown

The usage of abstract syntax trees for representing Markdown text has the advantage that it is very easy to process such data, in particular, to transform it and to extract information. Below is a short Swift snippet which illustrates how to process an abstract syntax tree for the purpose of extracting all top-level headers (i.e. this code prints the top-level outline of a text in Markdown format).

let markdown = MarkdownParser.standard.parse("""
                   # First *Header*
                   ## Sub-header
                   And this is a **paragraph**.
                   # Second **Header**
                   And this is another paragraph.
                 """)

func topLevelHeaders(doc: Block) -> [String] {
  guard case .document(let topLevelBlocks) = doc else {
    preconditionFailure("markdown block does not represent a document")
  }
  var outline: [String] = []
  for block in topLevelBlocks {
    if case .heading(1, let text) = block {
      outline.append(text.rawDescription)
    }
  }
  return outline
}

let headers = topLevelHeaders(doc: markdown)
print(headers)

This will print an array with the following two entries:

["First Header", "Second Header"]

Converting Markdown into other formats

Generating text, HTML, and Attributed Strings

Swift MarkdownKit provides several generators, i.e. Markdown processors which output, for a given Markdown document, a corresponding representation in a different format.

HtmlGenerator defines a simple mapping from Markdown into HTML. Here is an example for the usage of the generator:

let html = HtmlGenerator.standard.generate(doc: markdown)

Beyond the safeMode option (see the HtmlGenerator initializer), there are currently no means to customize HtmlGenerator other than subclassing. Here is an example that defines a customized HTML generator which formats blockquote Markdown blocks using HTML tables:

open class CustomizedHtmlGenerator: HtmlGenerator {
  open override func generate(block: Block, parent: Parent, tight: Bool = false) -> String {
    switch block {
      case .blockquote(let blocks):
        return "<table><tbody><tr><td style=\"background: #bbb; width: 0.2em;\"  />" +
               "<td style=\"width: 0.2em;\" /><td>\n" +
               self.generate(blocks: blocks, parent: .block(block, parent)) +
               "</td></tr></tbody></table>\n"
      default:
        return super.generate(block: block, parent: parent, tight: tight)
    }
  }
}

Swift MarkdownKit also comes with a generator for attributed strings. AttributedStringGenerator uses a customized HTML generator internally to define the translation from Markdown into NSAttributedString. The initializer of AttributedStringGenerator provides a number of parameters for customizing the style of the generated attributed string. Alternatively, it is also possible to subclass the internal HTML generator and customize the output this way. For the implementation of syntax highlighting in code blocks, AttributedStringGenerator uses Highlight.js. Syntax highlighting is configured with a SyntaxHighlightingConfig value which gets passed to the initializer of AttributedStringGenerator via parameter syntaxHighlighting:. Setting this parameter to nil will disable syntax highlighting.

let generator = AttributedStringGenerator(fontSize: 12,
                                          fontFamily: "Helvetica, sans-serif",
                                          fontColor: "#33C",
                                          h1Color: "#000")
let attributedStr = generator.generate(doc: markdown)

The generated HTML is rendered into an NSAttributedString by the system. Use RenderingOptions to configure it. baseUrl is the base for resolving relative URLs (e.g. relative image paths) in the generated HTML. AttributedStringGenerator.generate(doc:) renders synchronously on the calling thread; generateAsync(doc:) is an alternative based on NSAttributedString.loadFromHTML which does not block the calling thread. It throws an AttributedStringGenerator.RenderingError (e.g. .timedOut if the render takes longer than timeout seconds, or .cancelled). It is available on macOS and iOS; on other platforms it throws .unsupportedPlatform.

let options = AttributedStringGenerator.RenderingOptions(
                baseUrl: URL(fileURLWithPath: "/path/to/images", isDirectory: true),
                timeout: 10)
let generator = AttributedStringGenerator(renderingOptions: options)
let attributedStr = generator.generate(doc: markdown)               // synchronous
let asyncStr = try await generator.generateAsync(doc: markdown)     // asynchronous

For code that does not use Swift concurrency, there are variants with a completion handler. The handler is called exactly once, asynchronously, and always on the main thread; it receives a Result with either the attributed string or a RenderingError:

generator.generateAsync(doc: markdown) { result in
  switch result {
    case .success(let attributedStr):
      textView.textStorage.setAttributedString(attributedStr)
    case .failure(let error):
      print("cannot convert: \(error)")
  }
}

Loading images securely

By default, images referred to by Markdown may be loaded from any location: the system's HTML renderer loads any file that an image URL points to (also files outside of imageBaseUrl, reached via ../ or absolute paths), and generateAsync additionally fetches http(s) images and style sheets. Only the path extension of an image is checked by default (see imageExtensions below). For untrusted Markdown, use the preset RenderingOptions.untrusted (safe mode, and no local and no remote images): AttributedStringGenerator(renderingOptions: .untrusted). Its counterpart RenderingOptions.trusted restricts nothing; it is the same as the default options, RenderingOptions(). To allow some images, restrict image loading with the options localImages and remoteImages instead. Each is an ImageAccess: .none (never load), .any (no restriction in terms of URL path), or .within(url) (local images: only image files inside this directory, symbolic links resolved; remote images: only URLs with the same scheme, host and port as url and a path below the path of url).

let imageDirectory = URL(fileURLWithPath: "/path/to/images", isDirectory: true)
let options = AttributedStringGenerator.RenderingOptions(
                baseUrl: imageDirectory,
                localImages: .within(imageDirectory),
                remoteImages: .within(URL(string: "https://example.com/images/")!),
                textSizeMultiplier: 1.2)
let generator = AttributedStringGenerator(renderingOptions: options)

Independently of the location checks, every image that is checked must also have one of the path extensions in imageExtensions (default: RenderingOptions.defaultImageExtensions: png, jpg, jpeg, gif, tif, tiff, bmp, ico, heic, heif, webp), whether its location was permitted by .any or .within; this keeps non-image files from being loaded via an image URL. A custom set replaces the default, and "" stands for URLs without extension. This check always applies to the images of the Markdown text, also if localImages and remoteImages are both .any. Images in raw HTML are not checked. Images that are not allowed are replaced by their alternative text. Relative image paths are resolved against imageBaseUrl or, if that is not set, against baseUrl; the access options never influence this resolution, they only check the resolved URL (relative paths are rejected if there is no base). The restrictions apply to the images of the Markdown text only: raw HTML in the Markdown can still refer to images, style sheets and other resources. Set safeMode: true in the options to also omit raw HTML and to limit links to the schemes http, https and mailto (as HtmlGenerator(safeMode: true) does); it is independent of the image options and false by default. With remoteImages: .none and Markdown without raw HTML, generateAsync does not access the network. Remote images are never loaded by the synchronous methods. The restrictions are enforced while generating the HTML; they do not cover resources the application itself refers to (e.g. via customStyle), redirects of web servers, or files that change between the check and loading. textSizeMultiplier scales all font sizes.

Note: Without restrictions, do not use generateAsync with untrusted Markdown, as it then loads remote resources (images, style sheets) that the HTML refers to.

There are two generators for outputting formatted text. StringGenerator formats text based on the given Markdown and returns text that can be, e.g. edited in a text editor. TerminalGenerator formats text for output in an ANSI-compliant terminal, using control sequences to mark up the text.

Using the command-line tools

The Swift MarkdownKit Xcode project also implements a very simple command-line tool that outputs Markdown content either as HTML, RTF, or pretty-printed text, including the option to output ANSI-compliant markup. The tool either outputs a single Markdown text file into an output file or, when applied to a folder, outputs all Markdown files within the folder into output files in a target folder.

The tool is provided to serve as a demo and basis for customization to specific use cases. The simplest way to build the binary is to use the Swift Package Manager (SPM):

> git clone https://github.com/objecthub/swift-markdownkit.git
Cloning into 'swift-markdownkit'...
remote: Enumerating objects: 70, done.
remote: Counting objects: 100% (70/70), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 70 (delta 13), reused 65 (delta 11), pack-reused 0
Unpacking objects: 100% (70/70), done.
> cd swift-markdownkit
> swift run MarkdownKitProcess
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'MarkdownKitProcess' complete! (0.07s)
usage: mdkitprocess <format> <source> [<target>] [<width>]
where: <format> is either 'text', 'ansi', 'html', 'rtf', or 'rtfd'
       <source> is either a Markdown file or a directory of Markdown files
       <target> is either a file path or an existing directory into which
                the output files are written into. '-' writes the output
                into the terminal.
       <width>  defines a terminal width in columns for the formats 'text'
                and 'ansi'

A second tool, MarkdownTermViewer, does not read any files. It prints a demo document with the features of FullMarkdownParser (the document displayed by the MarkdownViewer sample app: formatted text, tables, task lists, code blocks, ...) either as plain text (StringGenerator) or marked up with ANSI escape codes (TerminalGenerator). This is a quick way to see what the text generators produce:

> swift run MarkdownTermViewer --help
USAGE: MarkdownTermViewer [<option> ...]
OPTIONS:
  -f, --format <format>
      Output format: 'text' (plain text, default) or 'ansi' (marked up with ANSI escape codes).
  -w, --width <columns>
      Width in columns, at least 10 (default: the width of the terminal, or 80).
  -h, --help
      Prints this usage description.
> swift run MarkdownTermViewer --format ansi --width 60

The output of --format ansi is only rendered correctly by a terminal. Xcode's console shows the escape sequences as raw characters.

Displaying Markdown with SwiftUI

SwiftUI comes with support for Markdown-formatted snippets but does not have a full-fledged viewer for Markdown documents. Since version 1.4, MarkdownKit includes a SwiftUI view for displaying Markdown documents. As opposed to many other custom created Markdown views, the one included in MarkdownKit is based on the CommonMark-compliant MarkdownKit parser as well as its attributed string generator. It is thus lightweight yet still quite configurable.

The initializer of MarkdownText receives the parsed Markdown content of type Block, an optional waitingMessage: parameter which defines what is being displayed while the content is being rendered (especially if many images need to be loaded), as well as a generator: parameter defining how the parsed Markdown is transformed into an attributed string, which is then being shown in the view. The generator is not just called once, but every time the color scheme has changed to potentially update the colors in use. A variant of this initializer accepts a Markdown document as a string and uses the extended Markdown parser internally to parse it. There are two initializers for each of the two ways of providing the document. Either you pass a generator:, or you do not provide one and the view uses the default generators for light and dark mode, which can be configured with renderingOptions: (see AttributedStringGenerator.RenderingOptions; the default is .trusted). The view generates the attributed string synchronously, which is why only the rendering options baseUrl, localImages, imageExtensions, safeMode and textSizeMultiplier have an effect (remote images are never loaded). Use MarkdownText(string: text, renderingOptions: .untrusted) if the Markdown does not come from a trusted source. The following code snippet shows the signatures of the initializers.

struct MarkdownText: View {
  init(_ text: Block,
       waitingMessage: NSAttributedString = NSAttributedString(string: "⏳"),
       renderingOptions: AttributedStringGenerator.RenderingOptions = .trusted) {
    ...
  }
  init(_ text: Block,
       waitingMessage: NSAttributedString = NSAttributedString(string: "⏳"),
       generator: ((Block, ColorScheme) -> NSAttributedString?)?) {
    ...
  }
  init(string: String,
       waitingMessage: NSAttributedString = NSAttributedString(string: "⏳"),
       renderingOptions: AttributedStringGenerator.RenderingOptions = .trusted) {
    ...
  }
  init(string: String,
       waitingMessage: NSAttributedString = NSAttributedString(string: "⏳"),
       generator: ((Block, ColorScheme) -> NSAttributedString?)?) {
    ...
  }
  ...
}

Here is an example how view MarkdownText could be used in a simple Markdown viewer. It defines an attributed string generator that uses two color schemes depending on whether dark mode is active or not.

struct ContentView: View {
  private static let lightGenerator = AttributedStringGenerator()
  private static let darkGenerator = AttributedStringGenerator(
      fontColor: "#FFF", codeFontColor: "#FFF",
      codeBlockFontColor: "#FF6", codeBlockBackground: "#333",
      syntaxHighlighting: .defaultDark,
      borderColor: "#BBB", h1Color: "#FFF", h2Color: "#FFF",
      h3Color: "#FFF", h4Color: "#FFF")
  
  let content = ExtendedMarkdownParser.standard.parse("""
    Lorem ipsum dolor sit amet, **consectetur adipiscing** elit. Aliquam
    non risus in massa ornare lacinia. _Etiam_ at ullamcorper ligula.
    
    1. This is the first item
    2. This is the second item
    3. This is the third item
    
    Cras laoreet tellus dolor, ac `suscipit augue` molestie a.
    Integer efficitur odio massa, in dictum arcu dictum in.
    
    | Column 1           | Column 2          | Col 3 |
    | ------------------ | ----------------- | :---: |
    | Nunc at dignissim  | More `cell` text  | One   |
    | Integer **ligula** | Justo nec finibus | Two   |
    | Cras nibh ex       | Praesent congue   | Three |
    """)
  
  var body: some View {
    MarkdownText(self.content) { doc, colorScheme in
      switch colorScheme {
        case .dark:
          return Self.darkGenerator.generate(doc: doc)
        default:
          return Self.lightGenerator.generate(doc: doc)
      }
    }
    .padding()
  }
}

Known issues

The parser passes all 652 examples of the CommonMark 0.31.2 specification. The test suite compares the generated HTML with the reference HTML of the specification, ignoring differences that do not change how browsers render the output: whitespace outside of code, &quot; vs. ", <ol start="1"> vs. <ol>, and percent-encoding of URLs (MarkdownKit does not encode URLs).

There are a number of limitations:

  • Extensions of GitHub Flavored Markdown other than tables, strikethrough and task lists (supported by FullMarkdownParser) are not supported: extended autolinks and footnotes.
  • The block parser keeps trailing spaces and backslashes at the end of the lines of a paragraph; whether a line ending is a hard line break is only decided by the inline parser (EscapeTransformer), because it depends on the inline markup (e.g. code spans). If you only parse the block structure (blockOnly: true), hard line breaks are not recognized.

Requirements

The following technologies are needed to build the components of the Swift MarkdownKit framework. The command-line tools can be compiled with the Swift Package Manager, so Xcode is not strictly needed for that. Similarly, just for compiling the framework and trying the command-line tools in Xcode, the Swift Package Manager is not needed.

  • Xcode 16 or later (the framework is developed with Xcode 26)
  • Swift 6 toolchain (the package is compiled in Swift 6 language mode; clients can use any language mode)
  • Swift Package Manager

Swift MarkdownKit supports Apple platforms only: macOS 11, iOS 15, tvOS 15 and watchOS 8 or later. Linux and other non-Apple platforms are not supported. Syntax highlighting relies on JavaScriptCore and is not available on watchOS. The SwiftUI view MarkdownText is available on macOS 14 and iOS 17 or later. The command-line tools are only functional on macOS.

License

MarkdownKit is released under an Apache License 2.0.

MarkdownKit contains some code snippets from HighlighterSwift which is licensed under an MIT license. HighlighterSwift is © 2026, Tony Smith. Portions are © 2016, Juan Pablo Illanes.

The MarkdownKit framework comes with a copy of Highlight.js. Highlight.js is released under a BSD License.

Copyright

Author: Matthias Zenger (matthias@objecthub.net)
Copyright © 2019-2025 Google LLC.
Copyright © 2026 Matthias Zenger.
Please note: This is not an official Google product.

About

A framework for parsing and transforming Markdown written in Swift 6. The syntax is based on the CommonMark specification. The framework defines an abstract syntax for Markdown, provides parsers for parsing strings into abstract syntax trees, and comes with generators for HTML, attributed strings, plain text, and text with ANSI-markup.

Topics

Resources

Contributing

Stars

213 stars

Watchers

3 watching

Forks

Releases

Used by

Contributors

Languages