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