// Convert straight quotation marks to typographic ones // 'use strict'; var QUOTE_TEST_RE = /['"]/; var QUOTE_RE = /['"]/g; var PUNCT_RE = /[-\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 !PUNCT_RE.test(str[pos]); } function replaceAt(str, index, ch) { return str.substr(0, index) + ch + str.substr(index + 1); } var stack = []; 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, chars, options = typographer.options, tokens = state.tokens; stack.length = 0; for (i = 0; i < tokens.length; i++) { token = tokens[i]; if (token.type !== 'text' || QUOTE_TEST_RE.test(token.text)) { continue; } thisLevel = tokens[i].level; for (j = stack.length - 1; j >= 0; j--) { if (stack[j].level <= thisLevel) { break; } } stack.length = j + 1; text = token.content; pos = 0; max = text.length; /*eslint no-labels:0,block-scoped-var:0*/ OUTER: while (pos < max) { QUOTE_RE.lastIndex = pos; t = QUOTE_RE.exec(text); if (!t) { break; } lastSpace = !isLetter(text, t.index - 1); pos = t.index + 1; isSingle = (t[0] === "'"); nextSpace = !isLetter(text, pos); if (!nextSpace && !lastSpace) { // middle of word if (isSingle) { token.content = replaceAt(token.content, 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) { tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, chars[0]); token.content = replaceAt(token.content, t.index, chars[1]); } stack.length = j; continue OUTER; } } } if (canOpen) { stack.push({ token: i, pos: t.index, single: isSingle, level: thisLevel }); } else if (canClose && isSingle) { token.content = replaceAt(token.content, t.index, APOSTROPHE); } } } };