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 `H2O`. 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("") } return ast.WalkContinue, nil }