Publish icon indicating copy to clipboard operation
Publish copied to clipboard

Replace dependency on Ink with swift-markdown.

Open peterkovacs opened this issue 1 year ago • 2 comments

The motivation for this PR boils down to:

  • Standardize on a single markdown implementation that's well-known and well-tested.
  • Convert from Markdown → Node tree.
  • Modifiers can hook into the parsing of (nearly) every type of markdown markup and adjust or replace the generated node structure.
    • One such use of this could be to create a image resizing plugin that writes width and height attributes to the generated HTML.
  • Unlock parsing of Block Directives.

For example:

One might imagine some HTML such as:

@TOC 

# Top Level 

## Second Level

Where the @TOC is generates HTML table of contents for the document. The implementation of such a plugin might look like this:

public static var tableOfContentsPlugin: Self {
    var headings: [MarkdownDocument.ID: [(level: Int, heading: String, slug: String)]] = .init()

    return .init(name: "Table of Contents") { context in

        let slugSafeCharacters = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-")

        func slug(_ string: String) -> String? {
            if let latin = string.applyingTransform(StringTransform("Any-Latin; Latin-ASCII; Lower;"), reverse: false) {
                let urlComponents = latin.components(separatedBy: slugSafeCharacters.inverted)
                let result = urlComponents.filter { $0 != "" }.joined(separator: "-")

                if result.count > 0 {
                    return result
                }
            }

            return nil
        }

        // capture any heading, and it's level.
        context.markdownParser.addModifier(for: .heading) { html, document, markup in
            guard let heading = markup as? Markdown.Heading else { return html }
            let slug = slug(heading.plainText) ?? UUID().uuidString
            headings[document.id, default: []].append((heading.level, heading.plainText, slug))

            return .group( .a(.attribute(named: "name", value: slug)), html )
        }

        context.markdownParser.addModifier(for: .blockDirective("TOC")) { _, document, _ in
            let id = document.id

            func tree(headings: ArraySlice<(level: Int, heading: String, slug: String)>, level: Int) -> Node<HTML.ListContext> {
                guard !headings.isEmpty else { return .empty }
                precondition(headings.allSatisfy { $0.0 >= level })

                var headings = headings
                var result: [Node<HTML.ListContext>] = []

                while let first = headings.first {
                    let atLevel = first.level == level
                    if atLevel { headings = headings.dropFirst() }
                    let children = headings.prefix { $0.level > level }

                    result.append(
                        .li(
                            .if(atLevel, .a(.href("#\(first.slug)"), .text(first.heading))),
                            .if(!children.isEmpty, .ul(tree(headings: children, level: level + 1)))
                        )
                    )

                    headings = headings.dropFirst(children.count)
                }

                return .group(result)
            }

            return .lazy {
                .ul(tree(headings: headings[id, default: []][...], level: 1))
            }
        }
    }
}

Note that this depends on the Node.lazy PR that I opened up a few weeks ago.

I should also note that I don't actually expect you to merge this (especially since its adding a couple of new external dependencies), but I thought you might be interested in some of the ideas!

peterkovacs avatar Aug 07 '23 14:08 peterkovacs

Great jobs done! I have been trying the same thing (actually my site czj.io has been using swift-markdown since last autumn) but haven't achieved a proper state for merging. in fact there has been discussion about introducing html conversion to swift-markdown (e.g. https://github.com/apple/swift-markdown/pull/106). Maybe we can implement the node conversion part and wait for "official" support for html of swift-markdown?

Ze0nC avatar Aug 07 '23 14:08 Ze0nC

+1 for this. I'm also trying to use swift-markdown to replace Ink on my blog's Publish-end. Hope the upstream will merge it soon.

Kyle-Ye avatar Aug 14 '23 07:08 Kyle-Ye