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 switch {
41 // edge of the buffer is likely to be a tag that we don't get to see,
42 // so we assume there is text there
43 case wordBoundary(previousChar) && previousChar != 0 && nextChar == 0:
44 *isOpen = true
45 case previousChar == 0 && wordBoundary(nextChar) && nextChar != 0:
46 *isOpen = false
47 case wordBoundary(previousChar) && !wordBoundary(nextChar):
48 *isOpen = true
49 case !wordBoundary(previousChar) && wordBoundary(nextChar):
50 *isOpen = false
51 case !wordBoundary(previousChar) && !wordBoundary(nextChar):
52 *isOpen = true
53 default:
54 *isOpen = !*isOpen
55 }
56
57 ob.WriteByte('&')
58 if *isOpen {
59 ob.WriteByte('l')
60 } else {
61 ob.WriteByte('r')
62 }
63 ob.WriteByte(quote)
64 ob.WriteString("quo;")
65 return true
66}
67
68func smartSquote(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
69 if len(text) >= 2 {
70 t1 := tolower(text[1])
71
72 if t1 == '\'' {
73 nextChar := byte(0)
74 if len(text) >= 3 {
75 nextChar = text[2]
76 }
77 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
78 return 1
79 }
80 }
81
82 if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
83 ob.WriteString("’")
84 return 0
85 }
86
87 if len(text) >= 3 {
88 t2 := tolower(text[2])
89
90 if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && (len(text) < 4 || wordBoundary(text[3])) {
91 ob.WriteString("’")
92 return 0
93 }
94 }
95 }
96
97 nextChar := byte(0)
98 if len(text) > 1 {
99 nextChar = text[1]
100 }
101 if smartQuotesHelper(ob, previousChar, nextChar, 's', &smrt.inSingleQuote) {
102 return 0
103 }
104
105 ob.WriteByte(text[0])
106 return 0
107}
108
109func smartParens(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
110 if len(text) >= 3 {
111 t1 := tolower(text[1])
112 t2 := tolower(text[2])
113
114 if t1 == 'c' && t2 == ')' {
115 ob.WriteString("©")
116 return 2
117 }
118
119 if t1 == 'r' && t2 == ')' {
120 ob.WriteString("®")
121 return 2
122 }
123
124 if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
125 ob.WriteString("™")
126 return 3
127 }
128 }
129
130 ob.WriteByte(text[0])
131 return 0
132}
133
134func smartDash(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
135 if len(text) >= 2 {
136 if text[1] == '-' {
137 ob.WriteString("—")
138 return 1
139 }
140
141 if wordBoundary(previousChar) && wordBoundary(text[1]) {
142 ob.WriteString("–")
143 return 0
144 }
145 }
146
147 ob.WriteByte(text[0])
148 return 0
149}
150
151func smartDashLatex(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
152 if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
153 ob.WriteString("—")
154 return 2
155 }
156 if len(text) >= 2 && text[1] == '-' {
157 ob.WriteString("–")
158 return 1
159 }
160
161 ob.WriteByte(text[0])
162 return 0
163}
164
165func smartAmp(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
166 if bytes.HasPrefix(text, []byte(""")) {
167 nextChar := byte(0)
168 if len(text) >= 7 {
169 nextChar = text[6]
170 }
171 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
172 return 5
173 }
174 }
175
176 if bytes.HasPrefix(text, []byte("�")) {
177 return 3
178 }
179
180 ob.WriteByte('&')
181 return 0
182}
183
184func smartPeriod(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
185 if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
186 ob.WriteString("…")
187 return 2
188 }
189
190 if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
191 ob.WriteString("…")
192 return 4
193 }
194
195 ob.WriteByte(text[0])
196 return 0
197}
198
199func smartBacktick(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
200 if len(text) >= 2 && text[1] == '`' {
201 nextChar := byte(0)
202 if len(text) >= 3 {
203 nextChar = text[2]
204 }
205 if smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
206 return 1
207 }
208 }
209
210 return 0
211}
212
213func smartNumberGeneric(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
214 if wordBoundary(previousChar) && len(text) >= 3 {
215 // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
216 num_end := 0
217 for len(text) > num_end && isdigit(text[num_end]) {
218 num_end++
219 }
220 if num_end == 0 {
221 ob.WriteByte(text[0])
222 return 0
223 }
224 if len(text) < num_end+2 || text[num_end] != '/' {
225 ob.WriteByte(text[0])
226 return 0
227 }
228 den_end := num_end + 1
229 for len(text) > den_end && isdigit(text[den_end]) {
230 den_end++
231 }
232 if den_end == num_end+1 {
233 ob.WriteByte(text[0])
234 return 0
235 }
236 if len(text) == den_end || wordBoundary(text[den_end]) {
237 ob.WriteString("<sup>")
238 ob.Write(text[:num_end])
239 ob.WriteString("</sup>⁄<sub>")
240 ob.Write(text[num_end+1 : den_end])
241 ob.WriteString("</sub>")
242 return den_end - 1
243 }
244 }
245
246 ob.WriteByte(text[0])
247 return 0
248}
249
250func smartNumber(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
251 if wordBoundary(previousChar) && len(text) >= 3 {
252 if text[0] == '1' && text[1] == '/' && text[2] == '2' {
253 if len(text) < 4 || wordBoundary(text[3]) {
254 ob.WriteString("½")
255 return 2
256 }
257 }
258
259 if text[0] == '1' && text[1] == '/' && text[2] == '4' {
260 if len(text) < 4 || wordBoundary(text[3]) || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
261 ob.WriteString("¼")
262 return 2
263 }
264 }
265
266 if text[0] == '3' && text[1] == '/' && text[2] == '4' {
267 if len(text) < 4 || wordBoundary(text[3]) || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
268 ob.WriteString("¾")
269 return 2
270 }
271 }
272 }
273
274 ob.WriteByte(text[0])
275 return 0
276}
277
278func smartDquote(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
279 nextChar := byte(0)
280 if len(text) > 1 {
281 nextChar = text[1]
282 }
283 if !smartQuotesHelper(ob, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
284 ob.WriteString(""")
285 }
286
287 return 0
288}
289
290func smartLtag(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
291 i := 0
292
293 for i < len(text) && text[i] != '>' {
294 i++
295 }
296
297 ob.Write(text[:i+1])
298 return i
299}
300
301type smartCallback func(ob *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int
302
303type SmartypantsRenderer [256]smartCallback
304
305func Smartypants(flags int) *SmartypantsRenderer {
306 r := new(SmartypantsRenderer)
307 r['"'] = smartDquote
308 r['&'] = smartAmp
309 r['\''] = smartSquote
310 r['('] = smartParens
311 if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
312 r['-'] = smartDash
313 } else {
314 r['-'] = smartDashLatex
315 }
316 r['.'] = smartPeriod
317 if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
318 r['1'] = smartNumber
319 r['3'] = smartNumber
320 } else {
321 for ch := '1'; ch <= '9'; ch++ {
322 r[ch] = smartNumberGeneric
323 }
324 }
325 r['<'] = smartLtag
326 r['`'] = smartBacktick
327 return r
328}
329
330func htmlSmartypants(ob *bytes.Buffer, text []byte, opaque interface{}) {
331 options := opaque.(*htmlOptions)
332 smrt := smartypantsData{false, false}
333
334 // first do normal entity escaping
335 var escaped bytes.Buffer
336 attrEscape(&escaped, text)
337 text = escaped.Bytes()
338
339 mark := 0
340 for i := 0; i < len(text); i++ {
341 if action := options.smartypants[text[i]]; action != nil {
342 if i > mark {
343 ob.Write(text[mark:i])
344 }
345
346 previousChar := byte(0)
347 if i > 0 {
348 previousChar = text[i-1]
349 }
350 i += action(ob, &smrt, previousChar, text[i:])
351 mark = i + 1
352 }
353 }
354
355 if mark < len(text) {
356 ob.Write(text[mark:])
357 }
358}