GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - hebertialmeida/MarkdownSyntax: ☄️ A Type-safe Markdown parser in Swift.

BranchesTags
Open more actions menu

Latest commit

 

History

126 Commits
126 Commits

Folders and files

NameName
Last commit message
Last commit date
.github/workflows
.github/workflows
 
 
.swiftpm/xcode
.swiftpm/xcode
 
 
Sources
Sources
 
 
Tests
Tests
 
 
.gitattributes
.gitattributes
 
 
.gitignore
.gitignore
 
 
CHANGELOG.md
CHANGELOG.md
 
 
LICENSE
LICENSE
 
 
Package.swift
Package.swift
 
 
README.md
README.md
 
 

Repository files navigation

☄️ MarkdownSyntax

codecov

MarkdownSyntax is a wrapper on top of the GitHub Flavoured Markdown that conforms to mdast. It parses markdown and creates a normalized AST so you can use it not only for rendering markdown and syntax highlighting. In addition, you can render to standard markdown formats: HTML, XML, Man, LaTeX, Plain Text.

If you want to see a real-world usage of MarkdownSyntax, check out https://pinery.app where this framework was designed for.

Usage

let input = "Hi this is **alpha**"
let tree = try await Markdown(text: input).parse()

Outputs a normalized tree:

Root(
    children: [
        Paragraph(
            children: [
                Text(
                    value: "Hi this is ",
                    position: Position(
                        start: Point(line: 1, column: 1, offset: 0),
                        end: Point(line: 1, column: 11, offset: 10),
                        indent: nil
                    )
                ),
                Strong(
                    children: [
                        Text(
                            value: "alpha",
                            position: Position(
                                start: Point(line: 1, column: 14, offset: 13),
                                end: Point(line: 1, column: 18, offset: 17),
                                indent: nil
                            )
                        )
                    ],
                    position: Position(
                        start: Point(line: 1, column: 12, offset: 11),
                        end: Point(line: 1, column: 20, offset: 19),
                        indent: nil
                    )
                )
            ],
            position: Position(
                start: Point(line: 1, column: 1, offset: 0),
                end: Point(line: 1, column: 20, offset: 19),
                indent: nil
            )
        )
    ],
    position: Position(
        start: Point(line: 1, column: 1, offset: 0),
        end: Point(line: 1, column: 20, offset: 19),
        indent: nil
    )
)

Extending the syntax

Syntax of your own, on top of Markdown, is a MarkdownExtension. It names the cmark node types its syntax starts in (its anchors), and the tree build offers it those nodes in the same walk that builds the tree. It returns nodes of its own, with positions read from the source, or nil to leave the node to cmark. Each node carries plain, what the same source reads as without the extension, so code that reads the tree and doesn't know your node still renders it.

This extension recognizes ==marked text== and renders it as <mark>:

import Foundation
import MarkdownSyntax

/// Marked text, written `==like this==`.
struct Highlight: StaticPhrasingContent, ExtensionContent {
    /// The marked text.
    let children: [PhrasingContent]
    /// The whole `==…==`, marks included.
    let position: Position
    /// The text between the marks.
    let contentPosition: Position
    /// What the source reads as without the extension: one text node.
    let plain: [Content]

    var subpositions: [String: Position] {
        ["contentPosition": contentPosition]
    }
}

/// Recognizes `==…==` inside a text node, and renders it as `<mark>…</mark>`.
struct HighlightExtension: MarkdownExtension {
    let anchors: [NodeKind] = [.text]

    func recognizePhrasing(in context: inout ParseContext) -> [PhrasingContent]? {
        let source = context.source
        // Ranges come from the source, which differs from the literal where an entity or an
        // escape was written: leave those text nodes to cmark.
        guard
            let range = context.range(of: context.anchor),
            let literal = context.anchor.literal,
            literal[...] == source[range]
        else { return nil }

        var nodes: [PhrasingContent] = []
        var rest = range.lowerBound
        while
            let open = source.range(of: "==", range: rest..<range.upperBound),
            let close = source.range(of: "==", range: open.upperBound..<range.upperBound),
            open.upperBound < close.lowerBound
        {
            let syntax = open.lowerBound..<close.upperBound
            let content = open.upperBound..<close.lowerBound
            guard
                let position = context.position(of: syntax),
                let contentPosition = context.position(of: content)
            else { return nil }

            if rest < open.lowerBound, let before = context.position(of: rest..<open.lowerBound) {
                nodes.append(Text(value: String(source[rest..<open.lowerBound]), position: before))
            }
            nodes.append(Highlight(
                children: [Text(value: String(source[content]), position: contentPosition)],
                position: position,
                contentPosition: contentPosition,
                plain: [Text(value: String(source[syntax]), position: position)]
            ))
            context.wrapHTML(content, in: syntax, onEnter: "<mark>", onExit: "</mark>")
            rest = close.upperBound
        }

        guard !nodes.isEmpty else { return nil }
        if rest < range.upperBound, let after = context.position(of: rest..<range.upperBound) {
            nodes.append(Text(value: String(source[rest..<range.upperBound]), position: after))
        }
        return nodes
    }
}

Register it when you create the parser:

let markdown = try await Markdown(text: "Some ==marked== text", syntaxExtensions: [HighlightExtension()])
let tree = await markdown.parse()
// Root > Paragraph > [Text "Some ", Highlight > [Text "marked"], Text " text"]
let html = try await markdown.renderHtml()
// <p data-sourcepos="1:1-1:20">Some <mark>marked</mark> text</p>

Syntax found in the source can hold an escape or an entity, which the reader doesn't see: ParseContext.literal(of:) reads a source range as cmark reads text, so the range comes from the source and the text from cmark's own rules — inside a GFM table cell, \| reads as |:

/// The text between `[[` and `]]` in a text node, as a reader sees it: `[[Caf&eacute;]]` gives
/// `Café`. The brackets are found in the source, so their ranges stay right where an escape or
/// an entity was written.
func bracketedText(in context: ParseContext) -> String? {
    let source = context.source
    guard
        let range = context.range(of: context.anchor),
        let open = source.range(of: "[[", range: range),
        let close = source.range(of: "]]", range: open.upperBound..<range.upperBound)
    else { return nil }
    return context.literal(of: open.upperBound..<close.lowerBound)
}

wrapHTML(_:in:onEnter:onExit:) reads its text the same way, so syntax in a text node with an escape or an entity, or in a table cell, renders with its markup and the text around as cmark renders it: | ==b\|c== | gives <td><mark>b|c</mark></td>.

A node of a type no extension names costs one lookup, and with no extension registered the tree is built exactly as before. The documentation catalog (MarkdownSyntax.docc) has an article each on anchors and claiming siblings, ranges, the plain fallback and HTML output — including omitHTML(_:), for syntax written right after a node such as ![Fox](fox.png){width=50%}, and renderHtml(of:) and renderHtml(ofEach:), the HTML of parts of the tree without parsing it again.

For more examples checkout the tests MarkdownSyntaxTests.

Installation

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler.

Once you have your Swift package set up, adding MarkdownSyntax as a dependency is as easy as adding it to the dependencies value of your Package.swift.

dependencies: [
    .package(url: "https://github.com/hebertialmeida/MarkdownSyntax", from: "1.3.0")
]

Acknowledgements

  • swift-cmark
  • libcmark_gfm

About

☄️ A Type-safe Markdown parser in Swift.

Topics

astcmarkmarkdownmarkdown-parsingmdastswiftsyntax-highlightsyntax-treeunist

Resources

Readme
MIT license
Activity

Stars

78 stars

Watchers

3 watching

Forks

6 forks
Report repository

Releases

Packages

Used by

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.