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{}