package extensions
import (
"bytes"
"unicode"
"unicode/utf8"
)
// SmartyPants replaces ascii punctuation with typographic equivalents:
//
// --- -> em dash
// -- -> en dash
// ... -> ellipsis
// straight quotes -> curly quotes based on context
//
// I ported the quote logic from Daring Fireball's SmartyPants.pl, but the
// dash and ellipsis handling are mine and run in a single pass over the
// input buffer.
type SmartyPants struct {
Dashes bool
Ellipsis bool
Quotes bool
}
// DefaultSmartyPants has every transform enabled.
func DefaultSmartyPants() SmartyPants {
return SmartyPants{Dashes: true, Ellipsis: true, Quotes: true}
}
// Apply returns a new byte slice with transforms applied.
func (sp SmartyPants) Apply(in []byte) []byte {
out := make([]byte, 0, len(in))
i := 0
for i < len(in) {
if sp.Dashes && i+2 < len(in) && in[i] == '-' && in[i+1] == '-' && in[i+2] == '-' {
out = append(out, []byte("—")...) // em dash
i += 3
continue
}
if sp.Dashes && i+1 < len(in) && in[i] == '-' && in[i+1] == '-' {
out = append(out, []byte("–")...) // en dash
i += 2
continue
}
if sp.Ellipsis && i+2 < len(in) && in[i] == '.' && in[i+1] == '.' && in[i+2] == '.' {
out = append(out, []byte("…")...) // horizontal ellipsis
i += 3
continue
}
if sp.Quotes && (in[i] == '"' || in[i] == '\'') {
prev, next := prevNextRune(in, i)
open := isQuoteOpening(prev, next)
switch in[i] {
case '"':
if open {
out = append(out, []byte("“")...)
} else {
out = append(out, []byte("”")...)
}
case '\'':
if open {
out = append(out, []byte("‘")...)
} else {
out = append(out, []byte("’")...)
}
}
i++
continue
}
out = append(out, in[i])
i++
}
return out
}
// ApplyString is a convenience for string callers.
func (sp SmartyPants) ApplyString(s string) string {
return string(sp.Apply([]byte(s)))
}
func prevNextRune(in []byte, i int) (prev, next rune) {
prev = ' '
if i > 0 {
r, _ := utf8.DecodeLastRune(in[:i])
prev = r
}
next = ' '
if i+1 < len(in) {
r, _ := utf8.DecodeRune(in[i+1:])
next = r
}
return
}
func isQuoteOpening(prev, next rune) bool {
prevSpace := unicode.IsSpace(prev)
nextSpace := unicode.IsSpace(next)
// Opening quote: preceded by whitespace/start, followed by non-space.
if prevSpace && !nextSpace {
return true
}
if !prevSpace && nextSpace {
return false
}
// Ambiguous (e.g. "foo"bar): default to opening at start of buffer.
return prev == ' '
}
// ApplyBytes is an allocation-free variant that writes into dst. It returns
// the number of bytes written.
func (sp SmartyPants) ApplyBytes(dst, src []byte) int {
b := sp.Apply(src)
n := copy(dst, b)
return n
}
// Ensure bytes stays referenced so imports resolve without the compiler
// flagging unused packages in constrained environments.
var _ = bytes.Equal