diff options
Diffstat (limited to 'vendor/gitlab.com/golang-commonmark/markdown/smartquotes.go')
-rw-r--r-- | vendor/gitlab.com/golang-commonmark/markdown/smartquotes.go | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/vendor/gitlab.com/golang-commonmark/markdown/smartquotes.go b/vendor/gitlab.com/golang-commonmark/markdown/smartquotes.go new file mode 100644 index 00000000..cbfa9c58 --- /dev/null +++ b/vendor/gitlab.com/golang-commonmark/markdown/smartquotes.go @@ -0,0 +1,256 @@ +// Copyright 2015 The Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package markdown + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +func nextQuoteIndex(s []rune, from int) int { + for i := from; i < len(s); i++ { + r := s[i] + if r == '\'' || r == '"' { + return i + } + } + return -1 +} + +func firstRune(s string) rune { + for _, r := range s { + return r + } + return utf8.RuneError +} + +func replaceQuotes(tokens []Token, s *StateCore) { + type stackItem struct { + token int + text []rune + pos int + single bool + level int + } + var stack []stackItem + var changed map[int][]rune + + for i, tok := range tokens { + thisLevel := tok.Level() + + j := len(stack) - 1 + for j >= 0 { + if stack[j].level <= thisLevel { + break + } + j-- + } + stack = stack[:j+1] + + tok, ok := tok.(*Text) + if !ok || !strings.ContainsAny(tok.Content, `"'`) { + continue + } + + text := []rune(tok.Content) + pos := 0 + max := len(text) + + loop: + for pos < max { + index := nextQuoteIndex(text, pos) + if index < 0 { + break + } + + canOpen := true + canClose := true + pos = index + 1 + isSingle := text[index] == '\'' + + lastChar := ' ' + if index-1 > 0 { + lastChar = text[index-1] + } else { + loop1: + for j := i - 1; j >= 0; j-- { + switch tok := tokens[j].(type) { + case *Softbreak: + break loop1 + case *Hardbreak: + break loop1 + case *Text: + lastChar, _ = utf8.DecodeLastRuneInString(tok.Content) + break loop1 + default: + continue + } + } + } + + nextChar := ' ' + if pos < max { + nextChar = text[pos] + } else { + loop2: + for j := i + 1; j < len(tokens); j++ { + switch tok := tokens[j].(type) { + case *Softbreak: + break loop2 + case *Hardbreak: + break loop2 + case *Text: + nextChar, _ = utf8.DecodeRuneInString(tok.Content) + break loop2 + default: + continue + } + } + } + + isLastPunct := isMdAsciiPunct(lastChar) || unicode.IsPunct(lastChar) + isNextPunct := isMdAsciiPunct(nextChar) || unicode.IsPunct(nextChar) + isLastWhiteSpace := unicode.IsSpace(lastChar) + isNextWhiteSpace := unicode.IsSpace(nextChar) + + if isNextWhiteSpace { + canOpen = false + } else if isNextPunct { + if !(isLastWhiteSpace || isLastPunct) { + canOpen = false + } + } + + if isLastWhiteSpace { + canClose = false + } else if isLastPunct { + if !(isNextWhiteSpace || isNextPunct) { + canClose = false + } + } + + if nextChar == '"' && text[index] == '"' { + if lastChar >= '0' && lastChar <= '9' { + canClose = false + canOpen = false + } + } + + if canOpen && canClose { + canOpen = false + canClose = isNextPunct + } + + if !canOpen && !canClose { + if isSingle { + text[index] = '’' + if changed == nil { + changed = make(map[int][]rune) + } + changed[i] = text + } + continue + } + + if canClose { + for j := len(stack) - 1; j >= 0; j-- { + item := stack[j] + if item.level < thisLevel { + break + } + if item.single == isSingle && item.level == thisLevel { + if changed == nil { + changed = make(map[int][]rune) + } + + var q1, q2 string + if isSingle { + q1 = s.Md.options.Quotes[2] + q2 = s.Md.options.Quotes[3] + } else { + q1 = s.Md.options.Quotes[0] + q2 = s.Md.options.Quotes[1] + } + + if utf8.RuneCountInString(q1) == 1 && utf8.RuneCountInString(q2) == 1 { + item.text[item.pos] = firstRune(q1) + text[index] = firstRune(q2) + } else if tok == tokens[item.token] { + newText := make([]rune, 0, len(text)-2+len(q1)+len(q2)) + newText = append(newText, text[:item.pos]...) + newText = append(newText, []rune(q1)...) + newText = append(newText, text[item.pos+1:index]...) + newText = append(newText, []rune(q2)...) + newText = append(newText, text[index+1:]...) + + text = newText + item.text = newText + } else { + newText := make([]rune, 0, len(item.text)-1+len(q1)) + newText = append(newText, item.text[:item.pos]...) + newText = append(newText, []rune(q1)...) + newText = append(newText, item.text[item.pos+1:]...) + item.text = newText + + newText = make([]rune, 0, len(text)-1+len(q2)) + newText = append(newText, text[:index]...) + newText = append(newText, []rune(q2)...) + newText = append(newText, text[index+1:]...) + + text = newText + } + + max = len(text) + + if changed == nil { + changed = make(map[int][]rune) + } + changed[i] = text + changed[item.token] = item.text + stack = stack[:j] + continue loop + } + } + } + + if canOpen { + stack = append(stack, stackItem{ + token: i, + text: text, + pos: index, + single: isSingle, + level: thisLevel, + }) + } else if canClose && isSingle { + text[index] = '’' + if changed == nil { + changed = make(map[int][]rune) + } + changed[i] = text + } + } + } + + if changed != nil { + for i, text := range changed { + tokens[i].(*Text).Content = string(text) + } + } +} + +func ruleSmartQuotes(s *StateCore) { + if !s.Md.Typographer { + return + } + + tokens := s.Tokens + for i := len(tokens) - 1; i >= 0; i-- { + tok := tokens[i] + if tok, ok := tok.(*Inline); ok { + replaceQuotes(tok.Children, s) + } + } +} |