//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 05 Nov 2024 Matthew Giannini Creation
//
**
** PostProcessors are run as the last step of parsing and provide an opportunity
** to inspect/modify the parsed AST before rendering.
**
@Js
mixin PostProcessor
{
** Post-process this node and return the result (which may be a modified node).
abstract Node process(Node node)
}
**
** A post-processor for handling link resolution
**
@Js
@NoDoc mixin LinkResolver : PostProcessor, Visitor
{
override Node process(Node node)
{
node.walk(this)
return node
}
override Void visitLink(Link link) { resolve(link) }
override Void visitImage(Image img) { resolve(img) }
** Resolve the given `LinkNode`. This will be called prior to any rendering
** for the given node and provides an opportunity to modify the link destination,
** mark the link as code, and change the link display text.
protected abstract Void resolve(LinkNode linkNode)
}
**
** A post-processor for generating anchor ids for headings
**
@Js
@NoDoc class HeadingProcessor : PostProcessor, Visitor
{
new make()
{
this.ids = [Str:Int?][:]
}
private [Str:Int?] ids
override Node process(Node node)
{
node.walk(this)
return node
}
** Generate an id per [GitHub markdown rules]`https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links`
**
** 1. Letters are converted to lower case
** 2. Spaces are replaced by hypens ('-'). Any other whitespace character
** or punctuation characters are removed
** 3. Leading and trailing whitespace are removed
** 4. Markup formatting is removed leaving only the contents
** (for example _italics_ becomes italics)
** 5. If the auto-generated anchor for a heading is identical to an earlier anchor
** in the same document, a unique identifier is generated by appending a hyphen and
** an auto-incrementing integer.
override Void visitHeading(Heading heading)
{
// get all the text content of the heading
words := Str[,]
heading.eachDescendant |node|
{
if (node is Text || node is Code) words.add(node->literal)
}
// join into a single str that is normalized to lower-case
// and has leading/trailing whitespace removed
text := words.join.lower.trimToNull
if (text == null) return
// build the normalized anchor
buf := StrBuf()
text.each |ch, i|
{
if (ch.isAlphaNum) buf.addChar(ch)
else if (ch == ' ') buf.addChar('-')
}
anchor := buf.toStr
// check for collision
id := ids.get(anchor)
if (id == null)
{
// never seen this anchor before
ids[anchor] = 0
}
else
{
// seen this anchor before, so append auto-incrementing integer
id = id + 1
ids[anchor] = id
anchor = "${anchor}-${id}"
}
// done!
heading.anchor = anchor
}
}