smartypants.go (view raw)
1//
2// Black Friday Markdown Processor
3// Originally based on http://github.com/tanoku/upskirt
4// by Russ Ross <russ@russross.com>
5//
6
7//
8//
9// SmartyPants rendering
10//
11//
12
13package blackfriday
14
15import (
16 "bytes"
17)
18
19type smartypantsData struct {
20 inSingleQuote bool
21 inDoubleQuote bool
22}
23
24func wordBoundary(c byte) bool {
25 return c == 0 || isspace(c) || ispunct(c)
26}
27
28func tolower(c byte) byte {
29 if c >= 'A' && c <= 'Z' {
30 return c - 'A' + 'a'
31 }
32 return c
33}
34
35func isdigit(c byte) bool {
36 return c >= '0' && c <= '9'
37}
38
39func smartQuotesHelper(ob *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool {
40 // edge of the buffer is likely to be a tag that we don't get to see,
41 // so we treat it like text sometimes
42
43 // enumerate all sixteen possibilities for (previousChar, nextChar)
44 // each can be one of {0, space, punct, other}
45 switch {
46 case previousChar == 0 && nextChar == 0:
47 // context is not any help here, so toggle
48 *isOpen = !*isOpen
49 case isspace(previousChar) && nextChar == 0:
50 // [ "] might be [ "<code>foo...]
51 *isOpen = true
52 case ispunct(previousChar) && nextChar == 0:
53 // [!"] hmm... could be [Run!"] or [("<code>...]
54 *isOpen = false
55 case /* isnormal(previousChar) && */ nextChar == 0:
56 // [a"] is probably a close
57 *isOpen = false
58 case previousChar == 0 && isspace(nextChar):
59 // [" ] might be [...foo</code>" ]
60 *isOpen = false
61 case isspace(previousChar) && isspace(nextChar):
62 // [ " ] context is not any help here, so toggle
63 *isOpen = !*isOpen
64 case ispunct(previousChar) && isspace(nextChar):
65 // [!" ] is probably a close
66 *isOpen = false
67 case /* isnormal(previousChar) && */ isspace(nextChar):
68 // [a" ] this is one of the easy cases
69 *isOpen = false
70 case previousChar == 0 && ispunct(nextChar):
71 // ["!] hmm... could be ["$1.95] or [</code>"!...]
72 *isOpen = false
73 case isspace(previousChar) && ispunct(nextChar):
74 // [ "!] looks more like [ "$1.95]
75 *isOpen = true
76 case ispunct(previousChar) && ispunct(nextChar):
77 // [!"!] context is not any help here, so toggle
78 *isOpen = !*isOpen
79 case /* isnormal(previousChar) && */ ispunct(nextChar):
80 // [a"!] is probably a close
81 *isOpen = false
82 case previousChar == 0 /* && isnormal(nextChar) */ :
83 // ["a] is probably an open
84 *isOpen = true
85 case isspace(previousChar) /* && isnormal(nextChar) */ :
86 // [ "a] this is one of the easy cases
87 *isOpen = true
88 case ispunct(previousChar) /* && isnormal(nextChar) */ :
89 // [!"a] is probably an open
90 *isOpen = true
91 default:
92 // [a'b] maybe a contraction?
93 *isOpen = false
94 }
95
96 ob.WriteByte('&')
97 if *isOpen {
98 ob.WriteByte('l')
99 } else {
100 ob.WriteByte('r')
101 }
102 ob.WriteByte(quote)
103 ob.WriteString("quo;")
104 return true
105}
106
107func smartSquote(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
108 if len(text) >= 2 {
109 t1 := tolower(text[1])
110
111 if t1 == '\'' {
112 nextChar := byte(0)
113 if len(text) >= 3 {
114 nextChar = text[2]
115 }
116 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
117 return 1
118 }
119 }
120
121 if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
122 ob.WriteString("’")
123 return 0
124 }
125
126 if len(text) >= 3 {
127 t2 := tolower(text[2])
128
129 if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && (len(text) < 4 || wordBoundary(text[3])) {
130 ob.WriteString("’")
131 return 0
132 }
133 }
134 }
135
136 nextChar := byte(0)
137 if len(text) > 1 {
138 nextChar = text[1]
139 }
140 if smartQuotesHelper(ob, previousChar, nextChar, 's', &smrt.inSingleQuote) {
141 return 0
142 }
143
144 ob.WriteByte(text[0])
145 return 0
146}
147
148func smartParens(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
149 if len(text) >= 3 {
150 t1 := tolower(text[1])
151 t2 := tolower(text[2])
152
153 if t1 == 'c' && t2 == ')' {
154 ob.WriteString("©")
155 return 2
156 }
157
158 if t1 == 'r' && t2 == ')' {
159 ob.WriteString("®")
160 return 2
161 }
162
163 if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
164 ob.WriteString("™")
165 return 3
166 }
167 }
168
169 ob.WriteByte(text[0])
170 return 0
171}
172
173func smartDash(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
174 if len(text) >= 2 {
175 if text[1] == '-' {
176 ob.WriteString("—")
177 return 1
178 }
179
180 if wordBoundary(previousChar) && wordBoundary(text[1]) {
181 ob.WriteString("–")
182 return 0
183 }
184 }
185
186 ob.WriteByte(text[0])
187 return 0
188}
189
190func smartDashLatex(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
191 if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
192 ob.WriteString("—")
193 return 2
194 }
195 if len(text) >= 2 && text[1] == '-' {
196 ob.WriteString("–")
197 return 1
198 }
199
200 ob.WriteByte(text[0])
201 return 0
202}
203
204func smartAmp(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
205 if bytes.HasPrefix(text, []byte(""")) {
206 nextChar := byte(0)
207 if len(text) >= 7 {
208 nextChar = text[6]
209 }
210 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
211 return 5
212 }
213 }
214
215 if bytes.HasPrefix(text, []byte("�")) {
216 return 3
217 }
218
219 ob.WriteByte('&')
220 return 0
221}
222
223func smartPeriod(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
224 if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
225 ob.WriteString("…")
226 return 2
227 }
228
229 if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
230 ob.WriteString("…")
231 return 4
232 }
233
234 ob.WriteByte(text[0])
235 return 0
236}
237
238func smartBacktick(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
239 if len(text) >= 2 && text[1] == '`' {
240 nextChar := byte(0)
241 if len(text) >= 3 {
242 nextChar = text[2]
243 }
244 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
245 return 1
246 }
247 }
248
249 return 0
250}
251
252func smartNumberGeneric(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
253 if wordBoundary(previousChar) && len(text) >= 3 {
254 // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
255 num_end := 0
256 for len(text) > num_end && isdigit(text[num_end]) {
257 num_end++
258 }
259 if num_end == 0 {
260 ob.WriteByte(text[0])
261 return 0
262 }
263 if len(text) < num_end+2 || text[num_end] != '/' {
264 ob.WriteByte(text[0])
265 return 0
266 }
267 den_end := num_end + 1
268 for len(text) > den_end && isdigit(text[den_end]) {
269 den_end++
270 }
271 if den_end == num_end+1 {
272 ob.WriteByte(text[0])
273 return 0
274 }
275 if len(text) == den_end || wordBoundary(text[den_end]) {
276 ob.WriteString("<sup>")
277 ob.Write(text[:num_end])
278 ob.WriteString("</sup>⁄<sub>")
279 ob.Write(text[num_end+1 : den_end])
280 ob.WriteString("</sub>")
281 return den_end - 1
282 }
283 }
284
285 ob.WriteByte(text[0])
286 return 0
287}
288
289func smartNumber(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
290 if wordBoundary(previousChar) && len(text) >= 3 {
291 if text[0] == '1' && text[1] == '/' && text[2] == '2' {
292 if len(text) < 4 || wordBoundary(text[3]) {
293 ob.WriteString("½")
294 return 2
295 }
296 }
297
298 if text[0] == '1' && text[1] == '/' && text[2] == '4' {
299 if len(text) < 4 || wordBoundary(text[3]) || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
300 ob.WriteString("¼")
301 return 2
302 }
303 }
304
305 if text[0] == '3' && text[1] == '/' && text[2] == '4' {
306 if len(text) < 4 || wordBoundary(text[3]) || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
307 ob.WriteString("¾")
308 return 2
309 }
310 }
311 }
312
313 ob.WriteByte(text[0])
314 return 0
315}
316
317func smartDquote(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
318 nextChar := byte(0)
319 if len(text) > 1 {
320 nextChar = text[1]
321 }
322 if !smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
323 ob.WriteString(""")
324 }
325
326 return 0
327}
328
329func smartLtag(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
330 i := 0
331
332 for i < len(text) && text[i] != '>' {
333 i++
334 }
335
336 ob.Write(text[:i+1])
337 return i
338}
339
340type smartCallback func(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int
341
342type SmartypantsRenderer [256]smartCallback
343
344func Smartypants(flags int) *SmartypantsRenderer {
345 r := new(SmartypantsRenderer)
346 r['"'] = smartDquote
347 r['&'] = smartAmp
348 r['\''] = smartSquote
349 r['('] = smartParens
350 if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
351 r['-'] = smartDash
352 } else {
353 r['-'] = smartDashLatex
354 }
355 r['.'] = smartPeriod
356 if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
357 r['1'] = smartNumber
358 r['3'] = smartNumber
359 } else {
360 for ch := '1'; ch <= '9'; ch++ {
361 r[ch] = smartNumberGeneric
362 }
363 }
364 r['<'] = smartLtag
365 r['`'] = smartBacktick
366 return r
367}
368
369func htmlSmartypants(ob *bytes.Buffer, text []byte, opaque interface{}) {
370 options := opaque.(*htmlOptions)
371 smrt := smartypantsData{false, false}
372
373 // first do normal entity escaping
374 var escaped bytes.Buffer
375 attrEscape(&escaped, text)
376 text = escaped.Bytes()
377
378 mark := 0
379 for i := 0; i < len(text); i++ {
380 if action := options.smartypants[text[i]]; action != nil {
381 if i > mark {
382 ob.Write(text[mark:i])
383 }
384
385 previousChar := byte(0)
386 if i > 0 {
387 previousChar = text[i-1]
388 }
389 i += action(ob, &smrt, previousChar, text[i:])
390 mark = i + 1
391 }
392 }
393
394 if mark < len(text) {
395 ob.Write(text[mark:])
396 }
397}