Alex Kocharin
10 years ago
3 changed files with 182 additions and 0 deletions
@ -0,0 +1,110 @@ |
|||
// Convert straight quotation marks to typographic ones
|
|||
//
|
|||
'use strict'; |
|||
|
|||
|
|||
var quoteReg = /"|'/g; |
|||
var punctReg = /[-\s()\[\]]/; |
|||
var apostrophe = '’'; |
|||
|
|||
// This function returns true if the character at `pos`
|
|||
// could be inside a word.
|
|||
function isLetter(str, pos) { |
|||
if (pos < 0 || pos >= str.length) { return false; } |
|||
return !punctReg.test(str[pos]); |
|||
} |
|||
|
|||
|
|||
function addQuote(obj, tokenId, posId, str) { |
|||
if (!obj[tokenId]) { obj[tokenId] = {}; } |
|||
obj[tokenId][posId] = str; |
|||
} |
|||
|
|||
|
|||
module.exports = function smartquotes(typographer, state) { |
|||
var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, canOpen, canClose, j, isSingle, fn, chars, |
|||
options = typographer.options, |
|||
replace = {}, |
|||
tokens = state.tokens, |
|||
stack = []; |
|||
|
|||
for (i = 0; i < tokens.length; i++) { |
|||
token = tokens[i]; |
|||
thisLevel = tokens[i].level; |
|||
|
|||
for (j = stack.length - 1; j >= 0; j--) { |
|||
if (stack[j].level <= thisLevel) { break; } |
|||
} |
|||
stack.length = j + 1; |
|||
|
|||
if (token.type === 'text') { |
|||
text = token.content; |
|||
pos = 0; |
|||
max = text.length; |
|||
|
|||
while (pos < max) { |
|||
quoteReg.lastIndex = pos; |
|||
t = quoteReg.exec(text); |
|||
if (!t) { break; } |
|||
|
|||
lastSpace = !isLetter(text, t.index - 1); |
|||
pos = t.index + t[0].length; |
|||
isSingle = t[0] === "'"; |
|||
nextSpace = !isLetter(text, pos); |
|||
|
|||
if (!nextSpace && !lastSpace) { |
|||
// middle word
|
|||
if (isSingle) { |
|||
addQuote(replace, i, t.index, apostrophe); |
|||
} |
|||
continue; |
|||
} |
|||
|
|||
canOpen = !nextSpace; |
|||
canClose = !lastSpace; |
|||
|
|||
if (canClose) { |
|||
// this could be a closing quote, rewind the stack to get a match
|
|||
for (j = stack.length - 1; j >= 0; j--) { |
|||
item = stack[j]; |
|||
if (stack[j].level < thisLevel) { break; } |
|||
if (item.single === isSingle && stack[j].level === thisLevel) { |
|||
item = stack[j]; |
|||
chars = isSingle ? options.singleQuotes : options.doubleQuotes; |
|||
if (chars) { |
|||
addQuote(replace, item.token, item.start, chars[0]); |
|||
addQuote(replace, i, t.index, chars[1]); |
|||
} |
|||
stack.length = j; |
|||
canOpen = false; // should be "continue OUTER;", but eslint refuses labels :(
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (canOpen) { |
|||
stack.push({ |
|||
token: i, |
|||
start: t.index, |
|||
end: pos, |
|||
single: isSingle, |
|||
level: thisLevel |
|||
}); |
|||
} else if (canClose && isSingle) { |
|||
addQuote(replace, i, t.index, apostrophe); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fn = function(str, pos) { |
|||
if (!replace[i][pos]) { return str; } |
|||
return replace[i][pos]; |
|||
}; |
|||
|
|||
for (i = 0; i < tokens.length; i++) { |
|||
if (!replace[i]) { continue; } |
|||
quoteReg.lastIndex = 0; |
|||
tokens[i].content = tokens[i].content.replace(quoteReg, fn); |
|||
} |
|||
}; |
@ -0,0 +1,71 @@ |
|||
|
|||
Should parse nested quotes: |
|||
|
|||
. |
|||
"foo 'bar' baz" |
|||
. |
|||
<p>“foo ‘bar’ baz”</p> |
|||
. |
|||
|
|||
. |
|||
'foo 'bar' baz' |
|||
. |
|||
<p>‘foo ‘bar’ baz’</p> |
|||
. |
|||
|
|||
Should not overlap quotes: |
|||
|
|||
. |
|||
'foo "bar' baz" |
|||
. |
|||
<p>‘foo "bar’ baz"</p> |
|||
. |
|||
|
|||
Should match quotes on the same level: |
|||
|
|||
. |
|||
"foo *bar* baz" |
|||
. |
|||
<p>“foo <em>bar</em> baz”</p> |
|||
. |
|||
|
|||
Should not match quotes on different levels: |
|||
|
|||
. |
|||
*"foo* bar" |
|||
|
|||
"foo *bar"* |
|||
. |
|||
<p><em>"foo</em> bar"</p> |
|||
<p>"foo <em>bar"</em></p> |
|||
. |
|||
|
|||
. |
|||
*"foo* bar *baz"* |
|||
. |
|||
<p><em>"foo</em> bar <em>baz"</em></p> |
|||
. |
|||
|
|||
Should try and find matching quote in this case: |
|||
|
|||
. |
|||
"foo "bar 'baz" |
|||
. |
|||
<p>"foo “bar 'baz”</p> |
|||
. |
|||
|
|||
Should render an apostrophe as a rsquo: |
|||
|
|||
. |
|||
This isn't and can't be the best approach to implement this... |
|||
. |
|||
<p>This isn’t and can’t be the best approach to implement this…</p> |
|||
. |
|||
|
|||
Apostrophe could end the word, that's why original smartypants replaces all of them as rsquo: |
|||
|
|||
. |
|||
users' stuff |
|||
. |
|||
<p>users’ stuff</p> |
|||
. |
Loading…
Reference in new issue