// 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(`