1
2
3
4
5 package relnote
6
7 import (
8 "fmt"
9 "strings"
10 "unicode"
11 "unicode/utf8"
12
13 "golang.org/x/mod/module"
14 md "rsc.io/markdown"
15 )
16
17
18
19
20
21 func addSymbolLinks(doc *md.Document, defaultPackage string) {
22 addSymbolLinksBlocks(doc.Blocks, defaultPackage)
23 }
24
25 func addSymbolLinksBlocks(bs []md.Block, defaultPackage string) {
26 for _, b := range bs {
27 addSymbolLinksBlock(b, defaultPackage)
28 }
29 }
30
31 func addSymbolLinksBlock(b md.Block, defaultPackage string) {
32 switch b := b.(type) {
33 case *md.Heading:
34 addSymbolLinksBlock(b.Text, defaultPackage)
35 case *md.Text:
36 b.Inline = addSymbolLinksInlines(b.Inline, defaultPackage)
37 case *md.List:
38 addSymbolLinksBlocks(b.Items, defaultPackage)
39 case *md.Item:
40 addSymbolLinksBlocks(b.Blocks, defaultPackage)
41 case *md.Paragraph:
42 addSymbolLinksBlock(b.Text, defaultPackage)
43 case *md.Quote:
44 addSymbolLinksBlocks(b.Blocks, defaultPackage)
45
46 case *md.CodeBlock:
47 case *md.HTMLBlock:
48 case *md.Empty:
49 case *md.ThematicBreak:
50 default:
51 panic(fmt.Sprintf("unknown block type %T", b))
52 }
53 }
54
55
56
57 func addSymbolLinksInlines(ins []md.Inline, defaultPackage string) []md.Inline {
58 ins = splitAtBrackets(ins)
59 var res []md.Inline
60 for i := 0; i < len(ins); i++ {
61 if txt := symbolLinkText(i, ins); txt != "" {
62 link, ok := symbolLink(txt, defaultPackage)
63 if ok {
64 res = append(res, link)
65 i += 2
66 continue
67 }
68 }
69
70
71 switch in := ins[i].(type) {
72 case *md.Strong:
73 res = append(res, &md.Strong{
74 Marker: in.Marker,
75 Inner: addSymbolLinksInlines(in.Inner, defaultPackage),
76 })
77
78 case *md.Emph:
79 res = append(res, &md.Emph{
80 Marker: in.Marker,
81 Inner: addSymbolLinksInlines(in.Inner, defaultPackage),
82 })
83
84
85 case *md.Del:
86 res = append(res, &md.Del{
87 Marker: in.Marker,
88 Inner: addSymbolLinksInlines(in.Inner, defaultPackage),
89 })
90
91 default:
92 res = append(res, in)
93 }
94 }
95 return res
96 }
97
98
99
100
101
102
103
104
105
106
107
108
109 func splitAtBrackets(ins []md.Inline) []md.Inline {
110 var res []md.Inline
111 for _, in := range ins {
112 if p, ok := in.(*md.Plain); ok {
113 text := p.Text
114 for len(text) > 0 {
115 i := strings.IndexAny(text, "[]")
116
117
118 if i < 0 {
119 res = append(res, &md.Plain{Text: text})
120 break
121 }
122
123 if i > 0 {
124 res = append(res, &md.Plain{Text: text[:i]})
125 }
126 res = append(res, &md.Plain{Text: text[i : i+1]})
127 text = text[i+1:]
128 }
129 } else {
130 res = append(res, in)
131 }
132 }
133 return res
134 }
135
136
137
138
139
140
141
142
143
144
145 func symbolLinkText(i int, ins []md.Inline) string {
146
147 plainText := func(j int) string {
148 if j < 0 || j >= len(ins) {
149 return ""
150 }
151 if p, ok := ins[j].(*md.Plain); ok {
152 return p.Text
153 }
154 return ""
155 }
156
157
158 if plainText(i) != "[" {
159 return ""
160 }
161
162 if t := plainText(i - 1); t != "" {
163 r, _ := utf8.DecodeLastRuneInString(t)
164 if !isLinkAdjacentRune(r) {
165 return ""
166 }
167 }
168
169 if plainText(i+2) != "]" {
170 return ""
171 }
172
173 if t := plainText(i + 3); t != "" {
174 r, _ := utf8.DecodeRuneInString(t)
175 if !isLinkAdjacentRune(r) {
176 return ""
177 }
178 }
179
180
181
182 if i+1 >= len(ins) {
183 return ""
184 }
185 switch in := ins[i+1].(type) {
186 case *md.Plain:
187 return in.Text
188 case *md.Code:
189 return in.Text
190 default:
191 return ""
192 }
193 }
194
195
196
197
198
199
200 func symbolLink(s, defaultPackage string) (md.Inline, bool) {
201 pkg, sym, ok := splitRef(s)
202 if !ok {
203 return nil, false
204 }
205 if pkg == "" {
206 if defaultPackage == "" {
207 return nil, false
208 }
209 pkg = defaultPackage
210 }
211 if sym != "" {
212 sym = "#" + sym
213 }
214 return &md.Link{
215 Inner: []md.Inline{&md.Code{Text: s}},
216 URL: fmt.Sprintf("/pkg/%s%s", pkg, sym),
217 }, true
218 }
219
220
221
222 func isLinkAdjacentRune(r rune) bool {
223 return unicode.IsPunct(r) || r == ' ' || r == '\t' || r == '\n'
224 }
225
226
227
228
229
230
231
232 func splitRef(s string) (pkg, name string, ok bool) {
233 s = strings.TrimPrefix(s, "*")
234 pkg, name, ok = splitDocName(s)
235 var recv string
236 if ok {
237 pkg, recv, _ = splitDocName(pkg)
238 }
239 if pkg != "" {
240 if err := module.CheckImportPath(pkg); err != nil {
241 return "", "", false
242 }
243 }
244 if recv != "" {
245 name = recv + "." + name
246 }
247 return pkg, name, true
248 }
249
250
251
252
253
254
255 func splitDocName(text string) (before, name string, foundDot bool) {
256 i := strings.LastIndex(text, ".")
257 name = text[i+1:]
258 if !isName(name) {
259 return text, "", false
260 }
261 if i >= 0 {
262 before = text[:i]
263 }
264 return before, name, true
265 }
266
267
268 func isName(s string) bool {
269 t, ok := ident(s)
270 if !ok || t != s {
271 return false
272 }
273 r, _ := utf8.DecodeRuneInString(s)
274 return unicode.IsUpper(r)
275 }
276
277
278
279
280
281
282 func ident(s string) (id string, ok bool) {
283
284 n := 0
285 for n < len(s) {
286 if c := s[n]; c < utf8.RuneSelf {
287 if isIdentASCII(c) && (n > 0 || c < '0' || c > '9') {
288 n++
289 continue
290 }
291 break
292 }
293 r, nr := utf8.DecodeRuneInString(s[n:])
294 if unicode.IsLetter(r) {
295 n += nr
296 continue
297 }
298 break
299 }
300 return s[:n], n > 0
301 }
302
303
304 func isIdentASCII(c byte) bool {
305
306
307
308
309 const mask = 0 |
310 (1<<26-1)<<'A' |
311 (1<<26-1)<<'a' |
312 (1<<10-1)<<'0' |
313 1<<'_'
314
315 return ((uint64(1)<<c)&(mask&(1<<64-1)) |
316 (uint64(1)<<(c-64))&(mask>>64)) != 0
317 }
318
View as plain text