`*[TERM]: expansion` on its own line, PHP Markdown Extra's form, and every whole-word use in the document becomes <abbr title="…">. A transformer rather than an inline parser, because a definition may appear after the use it explains and a parser only ever sees what it has already read. A block parser rather than a pattern found later, because the line has to stop being content — an author would notice that going wrong before anything else. Whole-word matching is the part that would have bitten: without it a definition of HTML quietly rewrites HTMLish and xHTML too, so both have cases. Code spans, autolinks, raw HTML and an already-expanded term are skipped, the longest definition wins where two could match, and the expansion is escaped into the attribute so a quoted phrase cannot end it. Definitions are document-scoped. A term defined in a page does not reach an included fragment, which is parsed on its own bytes exactly as footnotes are — stated in content-model.md rather than left to be discovered. The nesting gate caught firstMatch four levels deep; the inner search is its own function now, which reads better than it did before the warning. core 2794/2800, ext 1483/2000, 34 gates green, 0 warnings.
158 lines
5.5 KiB
Go
158 lines
5.5 KiB
Go
package notation
|
|
|
|
import (
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// mark is one delimiter byte and what a single or doubled run of it means around some text.
|
|
//
|
|
// The tilde carries both, and that is the whole reason this package exists: goldmark's own strikethrough
|
|
// claims a single tilde as well as a double, which silently turns `H~2~O` into `H<del>2</del>O`. Owning the
|
|
// byte here is what lets subscript and strikethrough coexist (ADR-0061).
|
|
var marks = []mark{
|
|
{char: '~', single: "sub", double: "del"},
|
|
{char: '^', single: "sup"},
|
|
{char: '=', double: "mark"},
|
|
}
|
|
|
|
// A single run is scanned to its closing byte and may not cross whitespace; a doubled run goes through
|
|
// goldmark's delimiter machinery and may. The split is not a preference: `x^2 + y^2 = z^2` pairs its carets
|
|
// across the whole expression under delimiter rules, which is prose silently becoming markup. Pandoc draws
|
|
// the same line, and it is why a subscript holds a formula rather than a phrase.
|
|
type mark struct {
|
|
char byte
|
|
single, double string
|
|
}
|
|
|
|
// New returns the Markdown extension.
|
|
func New() goldmark.Extender { return extension{} }
|
|
|
|
type extension struct{}
|
|
|
|
// Extend registers one inline parser per mark, and the single renderer they share.
|
|
//
|
|
// Priority 500 is goldmark's own for delimiter-run inlines, so these resolve alongside emphasis rather than
|
|
// ahead of it: a mark is ordinary inline text, not a construct that outranks the language.
|
|
func (extension) Extend(md goldmark.Markdown) {
|
|
inline := make([]util.PrioritizedValue, 0, len(marks))
|
|
for _, m := range marks {
|
|
inline = append(inline, util.Prioritized(inlineParser{m}, 500))
|
|
}
|
|
md.Parser().AddOptions(
|
|
parser.WithInlineParsers(inline...),
|
|
// Above the paragraph parser, so a definition line stops being content (ADR-0062).
|
|
parser.WithBlockParsers(util.Prioritized(definitionParser{}, 99)),
|
|
parser.WithASTTransformers(util.Prioritized(expand{}, 200)),
|
|
)
|
|
md.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(nodeRenderer{}, 500)))
|
|
}
|
|
|
|
// kind identifies a parsed mark in the tree.
|
|
var kind = ast.NewNodeKind("Notation")
|
|
|
|
// node is one matched pair, carrying the element it becomes and nothing else.
|
|
type node struct {
|
|
ast.BaseInline
|
|
tag string
|
|
}
|
|
|
|
func (n *node) Kind() ast.NodeKind { return kind }
|
|
|
|
func (n *node) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) }
|
|
|
|
// processor pairs doubled runs. A run of one is refused here, so `=marked=` stays prose and the single-run
|
|
// meaning — where a mark has one — is decided by the scan instead.
|
|
type processor struct{ tag string }
|
|
|
|
func (p processor) IsDelimiter(b byte) bool { return b == '~' || b == '=' }
|
|
|
|
func (p processor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
|
|
return opener.Char == closer.Char && opener.Length >= 2 && closer.Length >= 2
|
|
}
|
|
|
|
func (p processor) OnMatch(consumes int) ast.Node { return &node{tag: p.tag} }
|
|
|
|
// inlineParser handles one byte, both of its meanings.
|
|
type inlineParser struct{ m mark }
|
|
|
|
func (p inlineParser) Trigger() []byte { return []byte{p.m.char} }
|
|
|
|
func (p inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
|
line, segment := block.PeekLine()
|
|
if len(line) > 1 && line[1] == p.m.char {
|
|
return p.doubled(block, line, segment, pc)
|
|
}
|
|
return p.single(block, line, segment)
|
|
}
|
|
|
|
// doubled pushes a delimiter run, so the pair may hold spaces and nested markup: `~~a *b*~~`.
|
|
func (p inlineParser) doubled(block text.Reader, line []byte, segment text.Segment, pc parser.Context) ast.Node {
|
|
if p.m.double == "" {
|
|
return nil
|
|
}
|
|
before := block.PrecendingCharacter()
|
|
d := parser.ScanDelimiter(line, before, 2, processor{p.m.double})
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
d.Segment = segment.WithStop(segment.Start + d.OriginalLength)
|
|
block.Advance(d.OriginalLength)
|
|
pc.PushDelimiter(d)
|
|
return d
|
|
}
|
|
|
|
// single scans to the closing byte on this line, refusing to cross whitespace.
|
|
//
|
|
// The content is taken as text rather than parsed for markup: a span that cannot hold a space has no room
|
|
// for emphasis either, and scanning is what keeps `x^2 + y^2` out of the parser's hands.
|
|
func (p inlineParser) single(block text.Reader, line []byte, segment text.Segment) ast.Node {
|
|
// A run of the byte is not a single mark, so `^^up^^` stays prose rather than matching the pair inside
|
|
// it. goldmark's own strikethrough guards the same way.
|
|
if p.m.single == "" || block.PrecendingCharacter() == rune(p.m.char) {
|
|
return nil
|
|
}
|
|
end := 0
|
|
for i := 1; i < len(line); i++ {
|
|
if line[i] == p.m.char {
|
|
end = i
|
|
break
|
|
}
|
|
if util.IsSpace(line[i]) {
|
|
return nil
|
|
}
|
|
}
|
|
if end < 2 {
|
|
return nil
|
|
}
|
|
inner := segment
|
|
inner.Start, inner.Stop = segment.Start+1, segment.Start+end
|
|
n := &node{tag: p.m.single}
|
|
n.AppendChild(n, ast.NewTextSegment(inner))
|
|
block.Advance(end + 1)
|
|
return n
|
|
}
|
|
|
|
// nodeRenderer writes the element a matched pair became. The tag is one of a fixed set in `marks`, never
|
|
// anything an author supplied, so it is written directly.
|
|
type nodeRenderer struct{}
|
|
|
|
func (r nodeRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
reg.Register(kind, render)
|
|
r.registerAbbr(reg)
|
|
}
|
|
|
|
func render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
tag := n.(*node).tag
|
|
if entering {
|
|
_, _ = w.WriteString("<" + tag + ">")
|
|
} else {
|
|
_, _ = w.WriteString("</" + tag + ">")
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|