1package main
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/yuin/goldmark"
  8	"github.com/yuin/goldmark/ast"
  9	"github.com/yuin/goldmark/parser"
 10	"github.com/yuin/goldmark/renderer"
 11	"github.com/yuin/goldmark/text"
 12	"github.com/yuin/goldmark/util"
 13)
 14
 15type alertType struct {
 16	kind  string
 17	title string
 18	icon  string
 19}
 20
 21var alertTypes = map[string]alertType{
 22	"NOTE": {
 23		kind:  "note",
 24		title: "Note",
 25		icon:  `<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-info" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="currentColor"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"/></svg>`,
 26	},
 27	"TIP": {
 28		kind:  "tip",
 29		title: "Tip",
 30		icon:  `<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-light-bulb" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="currentColor"><path fill-rule="evenodd" d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 01-1.484.211c-.04-.282-.163-.547-.37-.847a8.695 8.695 0 00-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.75.75 0 01-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75zM6 15.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5h-2.5a.75.75 0 01-.75-.75zM5.75 12a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5z"/></svg>`,
 31	},
 32	"IMPORTANT": {
 33		kind:  "important",
 34		title: "Important",
 35		icon:  `<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-report" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="currentColor"><path fill-rule="evenodd" d="M1.75 1.5a.25.25 0 00-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0114.25 13H8.06l-2.573 2.573A1.457 1.457 0 013 14.543V13H1.75A1.75 1.75 0 010 11.25v-9.5zM9 9a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"/></svg>`,
 36	},
 37	"WARNING": {
 38		kind:  "warning",
 39		title: "Warning",
 40		icon:  `<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-alert" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="currentColor"><path fill-rule="evenodd" d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"/></svg>`,
 41	},
 42	"CAUTION": {
 43		kind:  "caution",
 44		title: "Caution",
 45		icon:  `<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-law" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="currentColor"><path fill-rule="evenodd" d="M8.75.75a.75.75 0 00-1.5 0V2h-.984c-.305 0-.604.08-.869.23l-1.288.737A.25.25 0 013.984 3H1.75a.75.75 0 000 1.5h.428L.066 9.192a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.514 3.514 0 00.686.45A4.492 4.492 0 003 11c.88 0 1.556-.22 2.023-.454a3.515 3.515 0 00.686-.45l.045-.04.016-.015.006-.006.002-.002.001-.002L5.25 9.5l.53.53a.75.75 0 00.154-.838L3.822 4.5h.162c.305 0 .604-.08.869-.23l1.289-.737a.25.25 0 01.124-.033h.984V13h-2.5a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-2.5V3.5h.984a.25.25 0 01.124.033l1.29.736c.264.152.563.231.868.231h.162l-2.112 4.692a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.517 3.517 0 00.686.45A4.492 4.492 0 0013 11c.88 0 1.556-.22 2.023-.454a3.512 3.512 0 00.686-.45l.045-.04.01-.01.006-.005.006-.006.002-.002.001-.002-.529-.531.53.53a.75.75 0 00.154-.838L13.823 4.5h.427a.75.75 0 000-1.5h-2.234a.25.25 0 01-.124-.033l-1.29-.736A1.75 1.75 0 009.735 2H8.75V.75zM1.695 9.227c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327l-1.305 2.9zm10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327l-1.305 2.9z"/></svg>`,
 46	},
 47}
 48
 49var kindAlert = ast.NewNodeKind("Alert")
 50
 51type alertNode struct {
 52	ast.BaseBlock
 53	AlertType alertType
 54}
 55
 56func (n *alertNode) Dump(source []byte, level int) {
 57	ast.DumpHelper(n, source, level, nil, nil)
 58}
 59
 60func (n *alertNode) Kind() ast.NodeKind {
 61	return kindAlert
 62}
 63
 64type alertTransformer struct{}
 65
 66func (a *alertTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
 67	source := reader.Source()
 68
 69	type matchInfo struct {
 70		container ast.Node
 71		para      *ast.Paragraph
 72		info      alertType
 73		skipBytes int
 74	}
 75	var matches []matchInfo
 76
 77	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 78		if !entering {
 79			return ast.WalkContinue, nil
 80		}
 81
 82		var p *ast.Paragraph
 83		var target ast.Node
 84
 85		if bq, ok := n.(*ast.Blockquote); ok {
 86			target = bq
 87			for c := bq.FirstChild(); c != nil; c = c.NextSibling() {
 88				if cp, ok := c.(*ast.Paragraph); ok {
 89					p = cp
 90					break
 91				}
 92			}
 93		} else if para, ok := n.(*ast.Paragraph); ok {
 94			target = para
 95			p = para
 96		}
 97
 98		if p == nil {
 99			return ast.WalkContinue, nil
100		}
101
102		pText := p.Text(source)
103		raw := string(pText)
104		trimmed := strings.TrimLeft(raw, " \t\n\r")
105		if !strings.HasPrefix(trimmed, "[!") {
106			return ast.WalkContinue, nil
107		}
108
109		idx := strings.Index(trimmed, "]")
110		if idx == -1 {
111			return ast.WalkContinue, nil
112		}
113
114		name := strings.ToUpper(trimmed[2:idx])
115		info, ok := alertTypes[name]
116		if !ok {
117			return ast.WalkContinue, nil
118		}
119
120		skip := (len(raw) - len(trimmed)) + idx + 1
121		matches = append(matches, matchInfo{
122			container: target,
123			para:      p,
124			info:      info,
125			skipBytes: skip,
126		})
127
128		return ast.WalkSkipChildren, nil
129	})
130
131	for _, m := range matches {
132		rem := m.skipBytes
133		// Strip the tag from text/string children
134		for c := m.para.FirstChild(); c != nil && rem > 0; {
135			next := c.NextSibling()
136			if t, ok := c.(*ast.Text); ok {
137				l := t.Segment.Len()
138				if rem >= l {
139					rem -= l
140					m.para.RemoveChild(m.para, c)
141				} else {
142					start := t.Segment.Start + rem
143					if start < t.Segment.Stop && source[start] == ' ' {
144						start++
145					}
146					t.Segment = text.NewSegment(start, t.Segment.Stop)
147					rem = 0
148				}
149			} else if s, ok := c.(*ast.String); ok {
150				l := len(s.Value)
151				if rem >= l {
152					rem -= l
153					m.para.RemoveChild(m.para, c)
154				} else {
155					s.Value = s.Value[rem:]
156					rem = 0
157				}
158			} else {
159				break
160			}
161			c = next
162		}
163
164		an := &alertNode{AlertType: m.info}
165		parent := m.container.Parent()
166		if parent != nil {
167			parent.ReplaceChild(parent, m.container, an)
168		}
169
170		if _, ok := m.container.(*ast.Blockquote); ok {
171			for c := m.container.FirstChild(); c != nil; {
172				next := c.NextSibling()
173				m.container.RemoveChild(m.container, c)
174				an.AppendChild(an, c)
175				c = next
176			}
177		} else {
178			an.AppendChild(an, m.para)
179		}
180	}
181}
182
183type alertRenderer struct{}
184
185func (r *alertRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
186	reg.Register(kindAlert, r.render)
187}
188
189func (r *alertRenderer) render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
190	n := node.(*alertNode)
191	if entering {
192		_, _ = w.WriteString(fmt.Sprintf(`<div class="markdown-alert markdown-alert-%s"><!-- ALERT_NODE -->`, n.AlertType.kind))
193		_, _ = w.WriteString(fmt.Sprintf(`<p class="markdown-alert-title">%s %s</p>`, n.AlertType.icon, n.AlertType.title))
194	} else {
195		_, _ = w.WriteString("</div>")
196	}
197	return ast.WalkContinue, nil
198}
199
200type alertExt struct{}
201
202func (e *alertExt) Extend(m goldmark.Markdown) {
203	m.Parser().AddOptions(parser.WithASTTransformers(
204		util.Prioritized(&alertTransformer{}, 1),
205	))
206	m.Renderer().AddOptions(renderer.WithNodeRenderers(
207		util.Prioritized(&alertRenderer{}, 1),
208	))
209}
210
211var AlertExtension = &alertExt{}