
After go1.16, go will use module mode by default, even when the repository is checked out under GOPATH or in a one-off directory. Add go.mod, go.sum to keep this repo buildable without opting out of the module mode. > go mod init github.com/mmcgrana/gobyexample > go mod tidy > go mod vendor In module mode, the 'vendor' directory is special and its contents will be actively maintained by the go command. pygments aren't the dependency the go will know about, so it will delete the contents from vendor directory. Move it to `third_party` directory now. And, vendor the blackfriday package. Note: the tutorial contents are not affected by the change in go1.16 because all the examples in this tutorial ask users to run the go command with the explicit list of files to be compiled (e.g. `go run hello-world.go` or `go build command-line-arguments.go`). When the source list is provided, the go command does not have to compute the build list and whether it's running in GOPATH mode or module mode becomes irrelevant.
431 lines
11 KiB
Go
431 lines
11 KiB
Go
//
|
||
// Blackfriday Markdown Processor
|
||
// Available at http://github.com/russross/blackfriday
|
||
//
|
||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||
// Distributed under the Simplified BSD License.
|
||
// See README.md for details.
|
||
//
|
||
|
||
//
|
||
//
|
||
// SmartyPants rendering
|
||
//
|
||
//
|
||
|
||
package blackfriday
|
||
|
||
import (
|
||
"bytes"
|
||
)
|
||
|
||
type smartypantsData struct {
|
||
inSingleQuote bool
|
||
inDoubleQuote bool
|
||
}
|
||
|
||
func wordBoundary(c byte) bool {
|
||
return c == 0 || isspace(c) || ispunct(c)
|
||
}
|
||
|
||
func tolower(c byte) byte {
|
||
if c >= 'A' && c <= 'Z' {
|
||
return c - 'A' + 'a'
|
||
}
|
||
return c
|
||
}
|
||
|
||
func isdigit(c byte) bool {
|
||
return c >= '0' && c <= '9'
|
||
}
|
||
|
||
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
|
||
// edge of the buffer is likely to be a tag that we don't get to see,
|
||
// so we treat it like text sometimes
|
||
|
||
// enumerate all sixteen possibilities for (previousChar, nextChar)
|
||
// each can be one of {0, space, punct, other}
|
||
switch {
|
||
case previousChar == 0 && nextChar == 0:
|
||
// context is not any help here, so toggle
|
||
*isOpen = !*isOpen
|
||
case isspace(previousChar) && nextChar == 0:
|
||
// [ "] might be [ "<code>foo...]
|
||
*isOpen = true
|
||
case ispunct(previousChar) && nextChar == 0:
|
||
// [!"] hmm... could be [Run!"] or [("<code>...]
|
||
*isOpen = false
|
||
case /* isnormal(previousChar) && */ nextChar == 0:
|
||
// [a"] is probably a close
|
||
*isOpen = false
|
||
case previousChar == 0 && isspace(nextChar):
|
||
// [" ] might be [...foo</code>" ]
|
||
*isOpen = false
|
||
case isspace(previousChar) && isspace(nextChar):
|
||
// [ " ] context is not any help here, so toggle
|
||
*isOpen = !*isOpen
|
||
case ispunct(previousChar) && isspace(nextChar):
|
||
// [!" ] is probably a close
|
||
*isOpen = false
|
||
case /* isnormal(previousChar) && */ isspace(nextChar):
|
||
// [a" ] this is one of the easy cases
|
||
*isOpen = false
|
||
case previousChar == 0 && ispunct(nextChar):
|
||
// ["!] hmm... could be ["$1.95] or [</code>"!...]
|
||
*isOpen = false
|
||
case isspace(previousChar) && ispunct(nextChar):
|
||
// [ "!] looks more like [ "$1.95]
|
||
*isOpen = true
|
||
case ispunct(previousChar) && ispunct(nextChar):
|
||
// [!"!] context is not any help here, so toggle
|
||
*isOpen = !*isOpen
|
||
case /* isnormal(previousChar) && */ ispunct(nextChar):
|
||
// [a"!] is probably a close
|
||
*isOpen = false
|
||
case previousChar == 0 /* && isnormal(nextChar) */ :
|
||
// ["a] is probably an open
|
||
*isOpen = true
|
||
case isspace(previousChar) /* && isnormal(nextChar) */ :
|
||
// [ "a] this is one of the easy cases
|
||
*isOpen = true
|
||
case ispunct(previousChar) /* && isnormal(nextChar) */ :
|
||
// [!"a] is probably an open
|
||
*isOpen = true
|
||
default:
|
||
// [a'b] maybe a contraction?
|
||
*isOpen = false
|
||
}
|
||
|
||
// Note that with the limited lookahead, this non-breaking
|
||
// space will also be appended to single double quotes.
|
||
if addNBSP && !*isOpen {
|
||
out.WriteString(" ")
|
||
}
|
||
|
||
out.WriteByte('&')
|
||
if *isOpen {
|
||
out.WriteByte('l')
|
||
} else {
|
||
out.WriteByte('r')
|
||
}
|
||
out.WriteByte(quote)
|
||
out.WriteString("quo;")
|
||
|
||
if addNBSP && *isOpen {
|
||
out.WriteString(" ")
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 2 {
|
||
t1 := tolower(text[1])
|
||
|
||
if t1 == '\'' {
|
||
nextChar := byte(0)
|
||
if len(text) >= 3 {
|
||
nextChar = text[2]
|
||
}
|
||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||
return 1
|
||
}
|
||
}
|
||
|
||
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
|
||
out.WriteString("’")
|
||
return 0
|
||
}
|
||
|
||
if len(text) >= 3 {
|
||
t2 := tolower(text[2])
|
||
|
||
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
|
||
(len(text) < 4 || wordBoundary(text[3])) {
|
||
out.WriteString("’")
|
||
return 0
|
||
}
|
||
}
|
||
}
|
||
|
||
nextChar := byte(0)
|
||
if len(text) > 1 {
|
||
nextChar = text[1]
|
||
}
|
||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) {
|
||
return 0
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartParens(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 3 {
|
||
t1 := tolower(text[1])
|
||
t2 := tolower(text[2])
|
||
|
||
if t1 == 'c' && t2 == ')' {
|
||
out.WriteString("©")
|
||
return 2
|
||
}
|
||
|
||
if t1 == 'r' && t2 == ')' {
|
||
out.WriteString("®")
|
||
return 2
|
||
}
|
||
|
||
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
|
||
out.WriteString("™")
|
||
return 3
|
||
}
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartDash(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 2 {
|
||
if text[1] == '-' {
|
||
out.WriteString("—")
|
||
return 1
|
||
}
|
||
|
||
if wordBoundary(previousChar) && wordBoundary(text[1]) {
|
||
out.WriteString("–")
|
||
return 0
|
||
}
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
|
||
out.WriteString("—")
|
||
return 2
|
||
}
|
||
if len(text) >= 2 && text[1] == '-' {
|
||
out.WriteString("–")
|
||
return 1
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte, addNBSP bool) int {
|
||
if bytes.HasPrefix(text, []byte(""")) {
|
||
nextChar := byte(0)
|
||
if len(text) >= 7 {
|
||
nextChar = text[6]
|
||
}
|
||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) {
|
||
return 5
|
||
}
|
||
}
|
||
|
||
if bytes.HasPrefix(text, []byte("�")) {
|
||
return 3
|
||
}
|
||
|
||
out.WriteByte('&')
|
||
return 0
|
||
}
|
||
|
||
func smartAmp(angledQuotes, addNBSP bool) func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
var quote byte = 'd'
|
||
if angledQuotes {
|
||
quote = 'a'
|
||
}
|
||
|
||
return func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
return smartAmpVariant(out, smrt, previousChar, text, quote, addNBSP)
|
||
}
|
||
}
|
||
|
||
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
|
||
out.WriteString("…")
|
||
return 2
|
||
}
|
||
|
||
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
|
||
out.WriteString("…")
|
||
return 4
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if len(text) >= 2 && text[1] == '`' {
|
||
nextChar := byte(0)
|
||
if len(text) >= 3 {
|
||
nextChar = text[2]
|
||
}
|
||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||
return 1
|
||
}
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
|
||
// note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8)
|
||
// and avoid changing dates like 1/23/2005 into fractions.
|
||
numEnd := 0
|
||
for len(text) > numEnd && isdigit(text[numEnd]) {
|
||
numEnd++
|
||
}
|
||
if numEnd == 0 {
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
denStart := numEnd + 1
|
||
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
|
||
denStart = numEnd + 3
|
||
} else if len(text) < numEnd+2 || text[numEnd] != '/' {
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
denEnd := denStart
|
||
for len(text) > denEnd && isdigit(text[denEnd]) {
|
||
denEnd++
|
||
}
|
||
if denEnd == denStart {
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
|
||
out.WriteString("<sup>")
|
||
out.Write(text[:numEnd])
|
||
out.WriteString("</sup>⁄<sub>")
|
||
out.Write(text[denStart:denEnd])
|
||
out.WriteString("</sub>")
|
||
return denEnd - 1
|
||
}
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartNumber(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||
if text[0] == '1' && text[1] == '/' && text[2] == '2' {
|
||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
|
||
out.WriteString("½")
|
||
return 2
|
||
}
|
||
}
|
||
|
||
if text[0] == '1' && text[1] == '/' && text[2] == '4' {
|
||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
|
||
out.WriteString("¼")
|
||
return 2
|
||
}
|
||
}
|
||
|
||
if text[0] == '3' && text[1] == '/' && text[2] == '4' {
|
||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
|
||
out.WriteString("¾")
|
||
return 2
|
||
}
|
||
}
|
||
}
|
||
|
||
out.WriteByte(text[0])
|
||
return 0
|
||
}
|
||
|
||
func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
|
||
nextChar := byte(0)
|
||
if len(text) > 1 {
|
||
nextChar = text[1]
|
||
}
|
||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) {
|
||
out.WriteString(""")
|
||
}
|
||
|
||
return 0
|
||
}
|
||
|
||
func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd')
|
||
}
|
||
|
||
func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a')
|
||
}
|
||
|
||
func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||
i := 0
|
||
|
||
for i < len(text) && text[i] != '>' {
|
||
i++
|
||
}
|
||
|
||
out.Write(text[:i+1])
|
||
return i
|
||
}
|
||
|
||
type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int
|
||
|
||
type smartypantsRenderer [256]smartCallback
|
||
|
||
var (
|
||
smartAmpAngled = smartAmp(true, false)
|
||
smartAmpAngledNBSP = smartAmp(true, true)
|
||
smartAmpRegular = smartAmp(false, false)
|
||
smartAmpRegularNBSP = smartAmp(false, true)
|
||
)
|
||
|
||
func smartypants(flags int) *smartypantsRenderer {
|
||
r := new(smartypantsRenderer)
|
||
addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0
|
||
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
|
||
r['"'] = smartDoubleQuote
|
||
if !addNBSP {
|
||
r['&'] = smartAmpRegular
|
||
} else {
|
||
r['&'] = smartAmpRegularNBSP
|
||
}
|
||
} else {
|
||
r['"'] = smartAngledDoubleQuote
|
||
if !addNBSP {
|
||
r['&'] = smartAmpAngled
|
||
} else {
|
||
r['&'] = smartAmpAngledNBSP
|
||
}
|
||
}
|
||
r['\''] = smartSingleQuote
|
||
r['('] = smartParens
|
||
if flags&HTML_SMARTYPANTS_DASHES != 0 {
|
||
if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
|
||
r['-'] = smartDash
|
||
} else {
|
||
r['-'] = smartDashLatex
|
||
}
|
||
}
|
||
r['.'] = smartPeriod
|
||
if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
|
||
r['1'] = smartNumber
|
||
r['3'] = smartNumber
|
||
} else {
|
||
for ch := '1'; ch <= '9'; ch++ {
|
||
r[ch] = smartNumberGeneric
|
||
}
|
||
}
|
||
r['<'] = smartLeftAngle
|
||
r['`'] = smartBacktick
|
||
return r
|
||
}
|