vendor/github.com/russross/blackfriday/smartypants.go
changeset 246 0998f404dd31
parent 245 910f00ab2799
child 247 1ca743b3eb80
equal deleted inserted replaced
245:910f00ab2799 246:0998f404dd31
     1 //
       
     2 // Blackfriday Markdown Processor
       
     3 // Available at http://github.com/russross/blackfriday
       
     4 //
       
     5 // Copyright © 2011 Russ Ross <russ@russross.com>.
       
     6 // Distributed under the Simplified BSD License.
       
     7 // See README.md for details.
       
     8 //
       
     9 
       
    10 //
       
    11 //
       
    12 // SmartyPants rendering
       
    13 //
       
    14 //
       
    15 
       
    16 package blackfriday
       
    17 
       
    18 import (
       
    19 	"bytes"
       
    20 	"io"
       
    21 )
       
    22 
       
    23 // SPRenderer is a struct containing state of a Smartypants renderer.
       
    24 type SPRenderer struct {
       
    25 	inSingleQuote bool
       
    26 	inDoubleQuote bool
       
    27 	callbacks     [256]smartCallback
       
    28 }
       
    29 
       
    30 func wordBoundary(c byte) bool {
       
    31 	return c == 0 || isspace(c) || ispunct(c)
       
    32 }
       
    33 
       
    34 func tolower(c byte) byte {
       
    35 	if c >= 'A' && c <= 'Z' {
       
    36 		return c - 'A' + 'a'
       
    37 	}
       
    38 	return c
       
    39 }
       
    40 
       
    41 func isdigit(c byte) bool {
       
    42 	return c >= '0' && c <= '9'
       
    43 }
       
    44 
       
    45 func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
       
    46 	// edge of the buffer is likely to be a tag that we don't get to see,
       
    47 	// so we treat it like text sometimes
       
    48 
       
    49 	// enumerate all sixteen possibilities for (previousChar, nextChar)
       
    50 	// each can be one of {0, space, punct, other}
       
    51 	switch {
       
    52 	case previousChar == 0 && nextChar == 0:
       
    53 		// context is not any help here, so toggle
       
    54 		*isOpen = !*isOpen
       
    55 	case isspace(previousChar) && nextChar == 0:
       
    56 		// [ "] might be [ "<code>foo...]
       
    57 		*isOpen = true
       
    58 	case ispunct(previousChar) && nextChar == 0:
       
    59 		// [!"] hmm... could be [Run!"] or [("<code>...]
       
    60 		*isOpen = false
       
    61 	case /* isnormal(previousChar) && */ nextChar == 0:
       
    62 		// [a"] is probably a close
       
    63 		*isOpen = false
       
    64 	case previousChar == 0 && isspace(nextChar):
       
    65 		// [" ] might be [...foo</code>" ]
       
    66 		*isOpen = false
       
    67 	case isspace(previousChar) && isspace(nextChar):
       
    68 		// [ " ] context is not any help here, so toggle
       
    69 		*isOpen = !*isOpen
       
    70 	case ispunct(previousChar) && isspace(nextChar):
       
    71 		// [!" ] is probably a close
       
    72 		*isOpen = false
       
    73 	case /* isnormal(previousChar) && */ isspace(nextChar):
       
    74 		// [a" ] this is one of the easy cases
       
    75 		*isOpen = false
       
    76 	case previousChar == 0 && ispunct(nextChar):
       
    77 		// ["!] hmm... could be ["$1.95] or [</code>"!...]
       
    78 		*isOpen = false
       
    79 	case isspace(previousChar) && ispunct(nextChar):
       
    80 		// [ "!] looks more like [ "$1.95]
       
    81 		*isOpen = true
       
    82 	case ispunct(previousChar) && ispunct(nextChar):
       
    83 		// [!"!] context is not any help here, so toggle
       
    84 		*isOpen = !*isOpen
       
    85 	case /* isnormal(previousChar) && */ ispunct(nextChar):
       
    86 		// [a"!] is probably a close
       
    87 		*isOpen = false
       
    88 	case previousChar == 0 /* && isnormal(nextChar) */ :
       
    89 		// ["a] is probably an open
       
    90 		*isOpen = true
       
    91 	case isspace(previousChar) /* && isnormal(nextChar) */ :
       
    92 		// [ "a] this is one of the easy cases
       
    93 		*isOpen = true
       
    94 	case ispunct(previousChar) /* && isnormal(nextChar) */ :
       
    95 		// [!"a] is probably an open
       
    96 		*isOpen = true
       
    97 	default:
       
    98 		// [a'b] maybe a contraction?
       
    99 		*isOpen = false
       
   100 	}
       
   101 
       
   102 	// Note that with the limited lookahead, this non-breaking
       
   103 	// space will also be appended to single double quotes.
       
   104 	if addNBSP && !*isOpen {
       
   105 		out.WriteString("&nbsp;")
       
   106 	}
       
   107 
       
   108 	out.WriteByte('&')
       
   109 	if *isOpen {
       
   110 		out.WriteByte('l')
       
   111 	} else {
       
   112 		out.WriteByte('r')
       
   113 	}
       
   114 	out.WriteByte(quote)
       
   115 	out.WriteString("quo;")
       
   116 
       
   117 	if addNBSP && *isOpen {
       
   118 		out.WriteString("&nbsp;")
       
   119 	}
       
   120 
       
   121 	return true
       
   122 }
       
   123 
       
   124 func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   125 	if len(text) >= 2 {
       
   126 		t1 := tolower(text[1])
       
   127 
       
   128 		if t1 == '\'' {
       
   129 			nextChar := byte(0)
       
   130 			if len(text) >= 3 {
       
   131 				nextChar = text[2]
       
   132 			}
       
   133 			if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
       
   134 				return 1
       
   135 			}
       
   136 		}
       
   137 
       
   138 		if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
       
   139 			out.WriteString("&rsquo;")
       
   140 			return 0
       
   141 		}
       
   142 
       
   143 		if len(text) >= 3 {
       
   144 			t2 := tolower(text[2])
       
   145 
       
   146 			if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
       
   147 				(len(text) < 4 || wordBoundary(text[3])) {
       
   148 				out.WriteString("&rsquo;")
       
   149 				return 0
       
   150 			}
       
   151 		}
       
   152 	}
       
   153 
       
   154 	nextChar := byte(0)
       
   155 	if len(text) > 1 {
       
   156 		nextChar = text[1]
       
   157 	}
       
   158 	if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) {
       
   159 		return 0
       
   160 	}
       
   161 
       
   162 	out.WriteByte(text[0])
       
   163 	return 0
       
   164 }
       
   165 
       
   166 func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   167 	if len(text) >= 3 {
       
   168 		t1 := tolower(text[1])
       
   169 		t2 := tolower(text[2])
       
   170 
       
   171 		if t1 == 'c' && t2 == ')' {
       
   172 			out.WriteString("&copy;")
       
   173 			return 2
       
   174 		}
       
   175 
       
   176 		if t1 == 'r' && t2 == ')' {
       
   177 			out.WriteString("&reg;")
       
   178 			return 2
       
   179 		}
       
   180 
       
   181 		if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
       
   182 			out.WriteString("&trade;")
       
   183 			return 3
       
   184 		}
       
   185 	}
       
   186 
       
   187 	out.WriteByte(text[0])
       
   188 	return 0
       
   189 }
       
   190 
       
   191 func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   192 	if len(text) >= 2 {
       
   193 		if text[1] == '-' {
       
   194 			out.WriteString("&mdash;")
       
   195 			return 1
       
   196 		}
       
   197 
       
   198 		if wordBoundary(previousChar) && wordBoundary(text[1]) {
       
   199 			out.WriteString("&ndash;")
       
   200 			return 0
       
   201 		}
       
   202 	}
       
   203 
       
   204 	out.WriteByte(text[0])
       
   205 	return 0
       
   206 }
       
   207 
       
   208 func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   209 	if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
       
   210 		out.WriteString("&mdash;")
       
   211 		return 2
       
   212 	}
       
   213 	if len(text) >= 2 && text[1] == '-' {
       
   214 		out.WriteString("&ndash;")
       
   215 		return 1
       
   216 	}
       
   217 
       
   218 	out.WriteByte(text[0])
       
   219 	return 0
       
   220 }
       
   221 
       
   222 func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int {
       
   223 	if bytes.HasPrefix(text, []byte("&quot;")) {
       
   224 		nextChar := byte(0)
       
   225 		if len(text) >= 7 {
       
   226 			nextChar = text[6]
       
   227 		}
       
   228 		if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) {
       
   229 			return 5
       
   230 		}
       
   231 	}
       
   232 
       
   233 	if bytes.HasPrefix(text, []byte("&#0;")) {
       
   234 		return 3
       
   235 	}
       
   236 
       
   237 	out.WriteByte('&')
       
   238 	return 0
       
   239 }
       
   240 
       
   241 func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int {
       
   242 	var quote byte = 'd'
       
   243 	if angledQuotes {
       
   244 		quote = 'a'
       
   245 	}
       
   246 
       
   247 	return func(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   248 		return r.smartAmpVariant(out, previousChar, text, quote, addNBSP)
       
   249 	}
       
   250 }
       
   251 
       
   252 func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   253 	if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
       
   254 		out.WriteString("&hellip;")
       
   255 		return 2
       
   256 	}
       
   257 
       
   258 	if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
       
   259 		out.WriteString("&hellip;")
       
   260 		return 4
       
   261 	}
       
   262 
       
   263 	out.WriteByte(text[0])
       
   264 	return 0
       
   265 }
       
   266 
       
   267 func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   268 	if len(text) >= 2 && text[1] == '`' {
       
   269 		nextChar := byte(0)
       
   270 		if len(text) >= 3 {
       
   271 			nextChar = text[2]
       
   272 		}
       
   273 		if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
       
   274 			return 1
       
   275 		}
       
   276 	}
       
   277 
       
   278 	out.WriteByte(text[0])
       
   279 	return 0
       
   280 }
       
   281 
       
   282 func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   283 	if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
       
   284 		// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
       
   285 		// note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8)
       
   286 		//       and avoid changing dates like 1/23/2005 into fractions.
       
   287 		numEnd := 0
       
   288 		for len(text) > numEnd && isdigit(text[numEnd]) {
       
   289 			numEnd++
       
   290 		}
       
   291 		if numEnd == 0 {
       
   292 			out.WriteByte(text[0])
       
   293 			return 0
       
   294 		}
       
   295 		denStart := numEnd + 1
       
   296 		if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
       
   297 			denStart = numEnd + 3
       
   298 		} else if len(text) < numEnd+2 || text[numEnd] != '/' {
       
   299 			out.WriteByte(text[0])
       
   300 			return 0
       
   301 		}
       
   302 		denEnd := denStart
       
   303 		for len(text) > denEnd && isdigit(text[denEnd]) {
       
   304 			denEnd++
       
   305 		}
       
   306 		if denEnd == denStart {
       
   307 			out.WriteByte(text[0])
       
   308 			return 0
       
   309 		}
       
   310 		if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
       
   311 			out.WriteString("<sup>")
       
   312 			out.Write(text[:numEnd])
       
   313 			out.WriteString("</sup>&frasl;<sub>")
       
   314 			out.Write(text[denStart:denEnd])
       
   315 			out.WriteString("</sub>")
       
   316 			return denEnd - 1
       
   317 		}
       
   318 	}
       
   319 
       
   320 	out.WriteByte(text[0])
       
   321 	return 0
       
   322 }
       
   323 
       
   324 func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   325 	if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
       
   326 		if text[0] == '1' && text[1] == '/' && text[2] == '2' {
       
   327 			if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
       
   328 				out.WriteString("&frac12;")
       
   329 				return 2
       
   330 			}
       
   331 		}
       
   332 
       
   333 		if text[0] == '1' && text[1] == '/' && text[2] == '4' {
       
   334 			if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
       
   335 				out.WriteString("&frac14;")
       
   336 				return 2
       
   337 			}
       
   338 		}
       
   339 
       
   340 		if text[0] == '3' && text[1] == '/' && text[2] == '4' {
       
   341 			if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
       
   342 				out.WriteString("&frac34;")
       
   343 				return 2
       
   344 			}
       
   345 		}
       
   346 	}
       
   347 
       
   348 	out.WriteByte(text[0])
       
   349 	return 0
       
   350 }
       
   351 
       
   352 func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int {
       
   353 	nextChar := byte(0)
       
   354 	if len(text) > 1 {
       
   355 		nextChar = text[1]
       
   356 	}
       
   357 	if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) {
       
   358 		out.WriteString("&quot;")
       
   359 	}
       
   360 
       
   361 	return 0
       
   362 }
       
   363 
       
   364 func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   365 	return r.smartDoubleQuoteVariant(out, previousChar, text, 'd')
       
   366 }
       
   367 
       
   368 func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   369 	return r.smartDoubleQuoteVariant(out, previousChar, text, 'a')
       
   370 }
       
   371 
       
   372 func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int {
       
   373 	i := 0
       
   374 
       
   375 	for i < len(text) && text[i] != '>' {
       
   376 		i++
       
   377 	}
       
   378 
       
   379 	out.Write(text[:i+1])
       
   380 	return i
       
   381 }
       
   382 
       
   383 type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int
       
   384 
       
   385 // NewSmartypantsRenderer constructs a Smartypants renderer object.
       
   386 func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer {
       
   387 	var (
       
   388 		r SPRenderer
       
   389 
       
   390 		smartAmpAngled      = r.smartAmp(true, false)
       
   391 		smartAmpAngledNBSP  = r.smartAmp(true, true)
       
   392 		smartAmpRegular     = r.smartAmp(false, false)
       
   393 		smartAmpRegularNBSP = r.smartAmp(false, true)
       
   394 
       
   395 		addNBSP = flags&SmartypantsQuotesNBSP != 0
       
   396 	)
       
   397 
       
   398 	if flags&SmartypantsAngledQuotes == 0 {
       
   399 		r.callbacks['"'] = r.smartDoubleQuote
       
   400 		if !addNBSP {
       
   401 			r.callbacks['&'] = smartAmpRegular
       
   402 		} else {
       
   403 			r.callbacks['&'] = smartAmpRegularNBSP
       
   404 		}
       
   405 	} else {
       
   406 		r.callbacks['"'] = r.smartAngledDoubleQuote
       
   407 		if !addNBSP {
       
   408 			r.callbacks['&'] = smartAmpAngled
       
   409 		} else {
       
   410 			r.callbacks['&'] = smartAmpAngledNBSP
       
   411 		}
       
   412 	}
       
   413 	r.callbacks['\''] = r.smartSingleQuote
       
   414 	r.callbacks['('] = r.smartParens
       
   415 	if flags&SmartypantsDashes != 0 {
       
   416 		if flags&SmartypantsLatexDashes == 0 {
       
   417 			r.callbacks['-'] = r.smartDash
       
   418 		} else {
       
   419 			r.callbacks['-'] = r.smartDashLatex
       
   420 		}
       
   421 	}
       
   422 	r.callbacks['.'] = r.smartPeriod
       
   423 	if flags&SmartypantsFractions == 0 {
       
   424 		r.callbacks['1'] = r.smartNumber
       
   425 		r.callbacks['3'] = r.smartNumber
       
   426 	} else {
       
   427 		for ch := '1'; ch <= '9'; ch++ {
       
   428 			r.callbacks[ch] = r.smartNumberGeneric
       
   429 		}
       
   430 	}
       
   431 	r.callbacks['<'] = r.smartLeftAngle
       
   432 	r.callbacks['`'] = r.smartBacktick
       
   433 	return &r
       
   434 }
       
   435 
       
   436 // Process is the entry point of the Smartypants renderer.
       
   437 func (r *SPRenderer) Process(w io.Writer, text []byte) {
       
   438 	mark := 0
       
   439 	for i := 0; i < len(text); i++ {
       
   440 		if action := r.callbacks[text[i]]; action != nil {
       
   441 			if i > mark {
       
   442 				w.Write(text[mark:i])
       
   443 			}
       
   444 			previousChar := byte(0)
       
   445 			if i > 0 {
       
   446 				previousChar = text[i-1]
       
   447 			}
       
   448 			var tmp bytes.Buffer
       
   449 			i += action(&tmp, previousChar, text[i:])
       
   450 			w.Write(tmp.Bytes())
       
   451 			mark = i + 1
       
   452 		}
       
   453 	}
       
   454 	if mark < len(text) {
       
   455 		w.Write(text[mark:])
       
   456 	}
       
   457 }