// SPDX-FileCopyrightText: 2025 Free Mobile // SPDX-License-Identifier: AGPL-3.0-only package console import ( "bytes" "regexp" "strings" "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" ) var ( admonitionRegexp = regexp.MustCompile(`^\[!(IMPORTANT|NOTE|TIP|WARNING|CAUTION)\]$`) admonitionIcons = map[string]string{ "IMPORTANT": ``, "NOTE": ``, "TIP": ``, "WARNING": ``, "CAUTION": ``, } kindAdmonition = ast.NewNodeKind("Admonition") ) // admonitionNode represents an admonition (kind of like a blockquote with an icon) type admonitionNode struct { ast.BaseBlock AdmonitionType string } // newAdmonitionNode creates a new admonition node func newAdmonitionNode(admonitionType string) *admonitionNode { return &admonitionNode{ AdmonitionType: admonitionType, } } // Dump implements ast.Node func (n *admonitionNode) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) } // Kind implements ast.Node func (n *admonitionNode) Kind() ast.NodeKind { return kindAdmonition } // admonitionTransformer converts blockquotes to admonitions. type admonitionTransformer struct{} // Transform transforms the AST to turns blockquotes into admonitions. func (t *admonitionTransformer) Transform(node *ast.Document, reader text.Reader, _ parser.Context) { walker := func(n ast.Node) ast.WalkStatus { blockquote, ok := n.(*ast.Blockquote) if !ok { return ast.WalkContinue } // Check if the first child is a paragraph with admonition syntax firstChild := blockquote.FirstChild() if firstChild == nil { return ast.WalkSkipChildren } paragraph, ok := firstChild.(*ast.Paragraph) if !ok { return ast.WalkSkipChildren } lines := paragraph.Lines() if lines.Len() == 0 { return ast.WalkSkipChildren } firstLine := lines.At(0) lineContent := bytes.TrimSpace(firstLine.Value(reader.Source())) matches := admonitionRegexp.FindSubmatch(lineContent) if matches == nil { return ast.WalkSkipChildren } // Recreate the paragraph with the admonition marker removed. We assume it was alone on its line. newSegments := text.NewSegments() for i := 1; i < lines.Len(); i++ { line := lines.At(i) newSegments.Append(line) } paragraph.SetLines(newSegments) for child := paragraph.FirstChild(); child != nil; { next := child.NextSibling() if s, ok := child.(*ast.Text); ok && s.Segment.Stop <= firstLine.Stop { paragraph.RemoveChild(paragraph, child) } child = next } // Move all children from blockquote to admonition and replace the // blockquote with admonition. admonitionType := string(matches[1]) admonitionNode := newAdmonitionNode(admonitionType) admonitionNode.AppendChild(admonitionNode, paragraph) for child := blockquote.FirstChild(); child != nil; { next := child.NextSibling() blockquote.RemoveChild(blockquote, child) admonitionNode.AppendChild(admonitionNode, child) child = next } parent := blockquote.Parent() parent.ReplaceChild(parent, blockquote, admonitionNode) return ast.WalkSkipChildren } var walk func(ast.Node) ast.WalkStatus // This is almost like ast.Walk(), except it keeps a reference to the next // sibling in case the current element gets removed. We can switch back to // ast.Walk() once https://github.com/yuin/goldmark/pull/523 is merged. walk = func(n ast.Node) ast.WalkStatus { status := walker(n) if status == ast.WalkStop { return status } if status != ast.WalkSkipChildren { for c := n.FirstChild(); c != nil; { next := c.NextSibling() if st := walk(c); st == ast.WalkStop { return st } c = next } } return ast.WalkContinue } walk(node) } // admonitionRenderer renders admonitions to HTML type admonitionRenderer struct{} // RegisterFuncs implements renderer.NodeRenderer func (r *admonitionRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(kindAdmonition, r.renderAdmonition) } func (r *admonitionRenderer) renderAdmonition(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*admonitionNode) if entering { admonitionClass := "admonition admonition-" + strings.ToLower(n.AdmonitionType) admonitionIcon := admonitionIcons[n.AdmonitionType] _, _ = w.WriteString(`
`) _, _ = w.WriteString(`

`) _, _ = w.WriteString(``) title := strings.ToLower(n.AdmonitionType) if len(title) > 0 { title = strings.ToUpper(title[:1]) + title[1:] } _, _ = w.WriteString(title) _, _ = w.WriteString(`

`) } else { _, _ = w.WriteString(`
`) } return ast.WalkContinue, nil } // admonitionExtension extends goldmark with admonitions type admonitionExtension struct{} // Extend implements goldmark.Extender func (e *admonitionExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions( parser.WithASTTransformers( util.Prioritized(&admonitionTransformer{}, 500), ), ) m.Renderer().AddOptions( renderer.WithNodeRenderers( util.Prioritized(&admonitionRenderer{}, 500), ), ) }