package main import ( "fmt" "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" ) type alertType struct { kind string title string icon string } var alertTypes = map[string]alertType{ "NOTE": { kind: "note", title: "Note", icon: ``, }, "TIP": { kind: "tip", title: "Tip", icon: ``, }, "IMPORTANT": { kind: "important", title: "Important", icon: ``, }, "WARNING": { kind: "warning", title: "Warning", icon: ``, }, "CAUTION": { kind: "caution", title: "Caution", icon: ``, }, } var kindAlert = ast.NewNodeKind("Alert") type alertNode struct { ast.BaseBlock AlertType alertType } func (n *alertNode) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) } func (n *alertNode) Kind() ast.NodeKind { return kindAlert } type alertTransformer struct{} func (a *alertTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { source := reader.Source() type matchInfo struct { container ast.Node para *ast.Paragraph info alertType skipBytes int } var matches []matchInfo _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } var p *ast.Paragraph var target ast.Node if bq, ok := n.(*ast.Blockquote); ok { target = bq for c := bq.FirstChild(); c != nil; c = c.NextSibling() { if cp, ok := c.(*ast.Paragraph); ok { p = cp break } } } else if para, ok := n.(*ast.Paragraph); ok { target = para p = para } if p == nil { return ast.WalkContinue, nil } pText := p.Text(source) raw := string(pText) trimmed := strings.TrimLeft(raw, " \t\n\r") if !strings.HasPrefix(trimmed, "[!") { return ast.WalkContinue, nil } idx := strings.Index(trimmed, "]") if idx == -1 { return ast.WalkContinue, nil } name := strings.ToUpper(trimmed[2:idx]) info, ok := alertTypes[name] if !ok { return ast.WalkContinue, nil } skip := (len(raw) - len(trimmed)) + idx + 1 matches = append(matches, matchInfo{ container: target, para: p, info: info, skipBytes: skip, }) return ast.WalkSkipChildren, nil }) for _, m := range matches { rem := m.skipBytes // Strip the tag from text/string children for c := m.para.FirstChild(); c != nil && rem > 0; { next := c.NextSibling() if t, ok := c.(*ast.Text); ok { l := t.Segment.Len() if rem >= l { rem -= l m.para.RemoveChild(m.para, c) } else { start := t.Segment.Start + rem if start < t.Segment.Stop && source[start] == ' ' { start++ } t.Segment = text.NewSegment(start, t.Segment.Stop) rem = 0 } } else if s, ok := c.(*ast.String); ok { l := len(s.Value) if rem >= l { rem -= l m.para.RemoveChild(m.para, c) } else { s.Value = s.Value[rem:] rem = 0 } } else { break } c = next } an := &alertNode{AlertType: m.info} parent := m.container.Parent() if parent != nil { parent.ReplaceChild(parent, m.container, an) } if _, ok := m.container.(*ast.Blockquote); ok { for c := m.container.FirstChild(); c != nil; { next := c.NextSibling() m.container.RemoveChild(m.container, c) an.AppendChild(an, c) c = next } } else { an.AppendChild(an, m.para) } } } type alertRenderer struct{} func (r *alertRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(kindAlert, r.render) } func (r *alertRenderer) render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*alertNode) if entering { _, _ = w.WriteString(fmt.Sprintf(`
%s %s
`, n.AlertType.icon, n.AlertType.title)) } else { _, _ = w.WriteString("