diff --git a/README.md b/README.md index 5bbf76a..d49b81d 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,10 @@ var md = require('markdown-it')({ typographer: false, // Double + single quotes replacement pairs, when typographer enabled, - // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + // and smartquotes on. Could be either a String or an Array. + // + // For example, you can use '«»„“' for Russian, '„“‚‘' for German, + // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). quotes: '“”‘’', // Highlighter function. Should return escaped HTML, diff --git a/lib/index.js b/lib/index.js index b00481b..9f43048 100644 --- a/lib/index.js +++ b/lib/index.js @@ -152,9 +152,10 @@ function normalizeLinkText(url) { * - __typographer__ - `false`. Set `true` to enable [some language-neutral * replacement](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.js) + * quotes beautification (smartquotes). - * - __quotes__ - `“”‘’`, string. Double + single quotes replacement pairs, when - * typographer enabled and smartquotes on. Set doubles to '«»' for Russian, - * '„“' for German. + * - __quotes__ - `“”‘’`, String or Array. Double + single quotes replacement + * pairs, when typographer enabled and smartquotes on. For example, you can + * use `'«»„“'` for Russian, `'„“‚‘'` for German, and + * `['«\xA0', '\xA0»', '‹\xA0', '\xA0›']` for French (including nbsp). * - __highlight__ - `null`. Highlighter function for fenced code blocks. * Highlighter `function (str, lang)` should return escaped HTML. It can also * return empty string if the source was not changed and should be escaped externaly. diff --git a/lib/presets/commonmark.js b/lib/presets/commonmark.js index 53f788d..cb29bcf 100644 --- a/lib/presets/commonmark.js +++ b/lib/presets/commonmark.js @@ -15,7 +15,10 @@ module.exports = { typographer: false, // Double + single quotes replacement pairs, when typographer enabled, - // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + // and smartquotes on. Could be either a String or an Array. + // + // For example, you can use '«»„“' for Russian, '„“‚‘' for German, + // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). quotes: '\u201c\u201d\u2018\u2019' /* “”‘’ */, // Highlighter function. Should return escaped HTML, diff --git a/lib/presets/default.js b/lib/presets/default.js index 9bec941..7c2d3b4 100644 --- a/lib/presets/default.js +++ b/lib/presets/default.js @@ -15,7 +15,10 @@ module.exports = { typographer: false, // Double + single quotes replacement pairs, when typographer enabled, - // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + // and smartquotes on. Could be either a String or an Array. + // + // For example, you can use '«»„“' for Russian, '„“‚‘' for German, + // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). quotes: '\u201c\u201d\u2018\u2019' /* “”‘’ */, // Highlighter function. Should return escaped HTML, diff --git a/lib/presets/zero.js b/lib/presets/zero.js index 6863195..2d9cbdf 100644 --- a/lib/presets/zero.js +++ b/lib/presets/zero.js @@ -16,7 +16,10 @@ module.exports = { typographer: false, // Double + single quotes replacement pairs, when typographer enabled, - // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + // and smartquotes on. Could be either a String or an Array. + // + // For example, you can use '«»„“' for Russian, '„“‚‘' for German, + // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). quotes: '\u201c\u201d\u2018\u2019' /* “”‘’ */, // Highlighter function. Should return escaped HTML, diff --git a/lib/rules_core/replacements.js b/lib/rules_core/replacements.js index b5e2f64..bcdf5a9 100644 --- a/lib/rules_core/replacements.js +++ b/lib/rules_core/replacements.js @@ -1,7 +1,5 @@ // Simple typographyc replacements // -// '' → ‘’ -// "" → “”. Set '«»' for Russian, '„“' for German, empty to disable // (c) (C) → © // (tm) (TM) → ™ // (r) (R) → ® diff --git a/lib/rules_core/smartquotes.js b/lib/rules_core/smartquotes.js index 67eaed8..a0483b9 100644 --- a/lib/rules_core/smartquotes.js +++ b/lib/rules_core/smartquotes.js @@ -19,7 +19,7 @@ function replaceAt(str, index, ch) { function process_inlines(tokens, state) { var i, token, text, t, pos, max, thisLevel, item, lastChar, nextChar, isLastPunctChar, isNextPunctChar, isLastWhiteSpace, isNextWhiteSpace, - canOpen, canClose, j, isSingle, stack; + canOpen, canClose, j, isSingle, stack, openQuote, closeQuote; stack = []; @@ -104,16 +104,28 @@ function process_inlines(tokens, state) { if (stack[j].level < thisLevel) { break; } if (item.single === isSingle && stack[j].level === thisLevel) { item = stack[j]; + if (isSingle) { - tokens[item.token].content = replaceAt( - tokens[item.token].content, item.pos, state.md.options.quotes[2]); - token.content = replaceAt( - token.content, t.index, state.md.options.quotes[3]); + openQuote = state.md.options.quotes[2]; + closeQuote = state.md.options.quotes[3]; } else { - tokens[item.token].content = replaceAt( - tokens[item.token].content, item.pos, state.md.options.quotes[0]); - token.content = replaceAt(token.content, t.index, state.md.options.quotes[1]); + openQuote = state.md.options.quotes[0]; + closeQuote = state.md.options.quotes[1]; } + + // replace token.content *before* tokens[item.token].content, + // because, if they are pointing at the same token, replaceAt + // could mess up indices when quote length != 1 + token.content = replaceAt(token.content, t.index, closeQuote); + tokens[item.token].content = replaceAt( + tokens[item.token].content, item.pos, openQuote); + + pos += closeQuote.length - 1; + if (item.token === i) { pos += openQuote.length - 1; } + + text = token.content; + max = text.length; + stack.length = j; continue OUTER; } diff --git a/test/misc.js b/test/misc.js index 6785784..32e6ea7 100644 --- a/test/misc.js +++ b/test/misc.js @@ -275,3 +275,36 @@ describe('maxNesting', function () { }); }); + + +describe('smartquotes', function () { + var md = markdownit({ + typographer: true, + + // all strings have different length to make sure + // we didn't accidentally count the wrong one + quotes: [ '[[[', ']]', '(((((', '))))' ] + }); + + it('Should support multi-character quotes', function () { + assert.strictEqual( + md.render('"foo" \'bar\''), + '

[[[foo]] (((((bar))))

\n' + ); + }); + + it('Should support nested multi-character quotes', function () { + assert.strictEqual( + md.render('"foo \'bar\' baz"'), + '

[[[foo (((((bar)))) baz]]

\n' + ); + }); + + it('Should support multi-character quotes in different tags', function () { + assert.strictEqual( + md.render('"a *b \'c *d* e\' f* g"'), + '

[[[a b (((((c d e)))) f g]]

\n' + ); + }); + +});