Browse Source

Add rule to replace quotes with typographic ones

pull/14/head
Alex Kocharin 10 years ago
parent
commit
a4bae74995
  1. 110
      lib/rules_typographer/smartquotes.js
  2. 1
      lib/typographer.js
  3. 71
      test/fixtures/remarkable/smartquotes.txt

110
lib/rules_typographer/smartquotes.js

@ -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);
}
};

1
lib/typographer.js

@ -16,6 +16,7 @@ var rules = [];
rules.push(require('./rules_typographer/replace')); rules.push(require('./rules_typographer/replace'));
rules.push(require('./rules_typographer/smartquotes'));
function Typographer() { function Typographer() {

71
test/fixtures/remarkable/smartquotes.txt

@ -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 &quot;bar’ baz&quot;</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>&quot;foo</em> bar&quot;</p>
<p>&quot;foo <em>bar&quot;</em></p>
.
.
*"foo* bar *baz"*
.
<p><em>&quot;foo</em> bar <em>baz&quot;</em></p>
.
Should try and find matching quote in this case:
.
"foo "bar 'baz"
.
<p>&quot;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…
Cancel
Save