all repos — grayfriday @ 45ab8d0dc4470fdfdf90a48ebbccb3c8924ebbb1

blackfriday fork with a few changes

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("&rsquo;")
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("&rsquo;")
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("&copy;")
155			return 2
156		}
157
158		if t1 == 'r' && t2 == ')' {
159			ob.WriteString("&reg;")
160			return 2
161		}
162
163		if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
164			ob.WriteString("&trade;")
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("&mdash;")
177			return 1
178		}
179
180		if wordBoundary(previousChar) && wordBoundary(text[1]) {
181			ob.WriteString("&ndash;")
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("&mdash;")
193		return 2
194	}
195	if len(text) >= 2 && text[1] == '-' {
196		ob.WriteString("&ndash;")
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("&quot;")) {
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("&#0;")) {
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("&hellip;")
226		return 2
227	}
228
229	if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
230		ob.WriteString("&hellip;")
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>&frasl;<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("&frac12;")
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("&frac14;")
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("&frac34;")
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("&quot;")
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}