// 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) } } }