diff --git a/html.go b/html.go index f1a60de..bab3472 100644 --- a/html.go +++ b/html.go @@ -44,6 +44,7 @@ const ( SmartypantsDashes // Enable smart dashes (with Smartypants) SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering + SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) TOC // Generate a table of contents TagName = "[A-Za-z][A-Za-z0-9-]*" diff --git a/inline_test.go b/inline_test.go index b1a7ace..966dc22 100644 --- a/inline_test.go +++ b/inline_test.go @@ -1034,6 +1034,18 @@ func TestSmartDoubleQuotes(t *testing.T) { doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants}) } +func TestSmartDoubleQuotesNBSP(t *testing.T) { + var tests = []string{ + "this should be normal \"quoted\" text.\n", + "

this should be normal “ quoted ” text.

\n", + "this \" single double\n", + "

this “  single double

\n", + "two pair of \"some\" quoted \"text\".\n", + "

two pair of “ some ” quoted “ text ”.

\n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsQuotesNBSP}) +} + func TestSmartAngledDoubleQuotes(t *testing.T) { var tests = []string{ "this should be angled \"quoted\" text.\n", @@ -1046,6 +1058,18 @@ func TestSmartAngledDoubleQuotes(t *testing.T) { doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsAngledQuotes}) } +func TestSmartAngledDoubleQuotesNBSP(t *testing.T) { + var tests = []string{ + "this should be angled \"quoted\" text.\n", + "

this should be angled « quoted » text.

\n", + "this \" single double\n", + "

this «  single double

\n", + "two pair of \"some\" quoted \"text\".\n", + "

two pair of « some » quoted « text ».

\n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsAngledQuotes | SmartypantsQuotesNBSP}) +} + func TestSmartFractions(t *testing.T) { var tests = []string{ "1/2, 1/4 and 3/4; 1/4th and 3/4ths\n", @@ -1140,3 +1164,13 @@ func TestSkipHTML(t *testing.T) { "

text inline html more text

\n", }, TestParams{HTMLFlags: SkipHTML}) } + +func BenchmarkSmartDoubleQuotes(b *testing.B) { + params := TestParams{HTMLFlags: Smartypants} + params.extensions |= Autolink | Strikethrough + params.HTMLFlags |= UseXHTML + + for i := 0; i < b.N; i++ { + runMarkdown("this should be normal \"quoted\" text.\n", params) + } +} diff --git a/smartypants.go b/smartypants.go index 1b61018..3a220e9 100644 --- a/smartypants.go +++ b/smartypants.go @@ -42,7 +42,7 @@ func isdigit(c byte) bool { return c >= '0' && c <= '9' } -func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool { +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 @@ -99,6 +99,12 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote *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') @@ -107,6 +113,11 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote } out.WriteByte(quote) out.WriteString("quo;") + + if addNBSP && *isOpen { + out.WriteString(" ") + } + return true } @@ -119,7 +130,7 @@ func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text if len(text) >= 3 { nextChar = text[2] } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote) { + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { return 1 } } @@ -144,7 +155,7 @@ func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text if len(text) > 1 { nextChar = text[1] } - if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote) { + if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { return 0 } @@ -208,13 +219,13 @@ func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text [ return 0 } -func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { +func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, 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, &r.inDoubleQuote) { + if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { return 5 } } @@ -227,12 +238,15 @@ func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text return 0 } -func (r *SPRenderer) smartAmp(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartAmpVariant(out, previousChar, text, 'd') -} +func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { + var quote byte = 'd' + if angledQuotes { + quote = 'a' + } -func (r *SPRenderer) smartAmpAngledQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartAmpVariant(out, previousChar, text, 'a') + return func(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) + } } func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { @@ -256,7 +270,7 @@ func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text [] if len(text) >= 3 { nextChar = text[2] } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote) { + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { return 1 } } @@ -340,7 +354,7 @@ func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byt if len(text) > 1 { nextChar = text[1] } - if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote) { + if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { out.WriteString(""") } @@ -370,13 +384,31 @@ type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int // NewSmartypantsRenderer constructs a Smartypants renderer object. func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { - var r SPRenderer + var ( + r SPRenderer + + smartAmpAngled = r.smartAmp(true, false) + smartAmpAngledNBSP = r.smartAmp(true, true) + smartAmpRegular = r.smartAmp(false, false) + smartAmpRegularNBSP = r.smartAmp(false, true) + + addNBSP = flags&SmartypantsQuotesNBSP != 0 + ) + if flags&SmartypantsAngledQuotes == 0 { r.callbacks['"'] = r.smartDoubleQuote - r.callbacks['&'] = r.smartAmp + if !addNBSP { + r.callbacks['&'] = smartAmpRegular + } else { + r.callbacks['&'] = smartAmpRegularNBSP + } } else { r.callbacks['"'] = r.smartAngledDoubleQuote - r.callbacks['&'] = r.smartAmpAngledQuote + if !addNBSP { + r.callbacks['&'] = smartAmpAngled + } else { + r.callbacks['&'] = smartAmpAngledNBSP + } } r.callbacks['\''] = r.smartSingleQuote r.callbacks['('] = r.smartParens