Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed
https://markdown-it.github.io/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
111 lines
2.9 KiB
111 lines
2.9 KiB
// 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) {
|
|
/*eslint max-depth:0*/
|
|
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);
|
|
}
|
|
};
|
|
|