summaryrefslogtreecommitdiff
path: root/packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs')
-rw-r--r--packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs193
1 files changed, 193 insertions, 0 deletions
diff --git a/packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs b/packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs
new file mode 100644
index 0000000..3b990ed
--- /dev/null
+++ b/packages/markdown-it-14.1.0/lib/rules_core/smartquotes.mjs
@@ -0,0 +1,193 @@
+// Convert straight quotation marks to typographic ones
+//
+
+import { isWhiteSpace, isPunctChar, isMdAsciiPunct } from '../common/utils.mjs'
+
+const QUOTE_TEST_RE = /['"]/
+const QUOTE_RE = /['"]/g
+const APOSTROPHE = '\u2019' /* ’ */
+
+function replaceAt (str, index, ch) {
+ return str.slice(0, index) + ch + str.slice(index + 1)
+}
+
+function process_inlines (tokens, state) {
+ let j
+
+ const stack = []
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i]
+
+ const 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') { continue }
+
+ let text = token.content
+ let pos = 0
+ let max = text.length
+
+ /* eslint no-labels:0,block-scoped-var:0 */
+ OUTER:
+ while (pos < max) {
+ QUOTE_RE.lastIndex = pos
+ const t = QUOTE_RE.exec(text)
+ if (!t) { break }
+
+ let canOpen = true
+ let canClose = true
+ pos = t.index + 1
+ const isSingle = (t[0] === "'")
+
+ // Find previous character,
+ // default to space if it's the beginning of the line
+ //
+ let lastChar = 0x20
+
+ if (t.index - 1 >= 0) {
+ lastChar = text.charCodeAt(t.index - 1)
+ } else {
+ for (j = i - 1; j >= 0; j--) {
+ if (tokens[j].type === 'softbreak' || tokens[j].type === 'hardbreak') break // lastChar defaults to 0x20
+ if (!tokens[j].content) continue // should skip all tokens except 'text', 'html_inline' or 'code_inline'
+
+ lastChar = tokens[j].content.charCodeAt(tokens[j].content.length - 1)
+ break
+ }
+ }
+
+ // Find next character,
+ // default to space if it's the end of the line
+ //
+ let nextChar = 0x20
+
+ if (pos < max) {
+ nextChar = text.charCodeAt(pos)
+ } else {
+ for (j = i + 1; j < tokens.length; j++) {
+ if (tokens[j].type === 'softbreak' || tokens[j].type === 'hardbreak') break // nextChar defaults to 0x20
+ if (!tokens[j].content) continue // should skip all tokens except 'text', 'html_inline' or 'code_inline'
+
+ nextChar = tokens[j].content.charCodeAt(0)
+ break
+ }
+ }
+
+ const isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar))
+ const isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar))
+
+ const isLastWhiteSpace = isWhiteSpace(lastChar)
+ const isNextWhiteSpace = isWhiteSpace(nextChar)
+
+ if (isNextWhiteSpace) {
+ canOpen = false
+ } else if (isNextPunctChar) {
+ if (!(isLastWhiteSpace || isLastPunctChar)) {
+ canOpen = false
+ }
+ }
+
+ if (isLastWhiteSpace) {
+ canClose = false
+ } else if (isLastPunctChar) {
+ if (!(isNextWhiteSpace || isNextPunctChar)) {
+ canClose = false
+ }
+ }
+
+ if (nextChar === 0x22 /* " */ && t[0] === '"') {
+ if (lastChar >= 0x30 /* 0 */ && lastChar <= 0x39 /* 9 */) {
+ // special case: 1"" - count first quote as an inch
+ canClose = canOpen = false
+ }
+ }
+
+ if (canOpen && canClose) {
+ // Replace quotes in the middle of punctuation sequence, but not
+ // in the middle of the words, i.e.:
+ //
+ // 1. foo " bar " baz - not replaced
+ // 2. foo-"-bar-"-baz - replaced
+ // 3. foo"bar"baz - not replaced
+ //
+ canOpen = isLastPunctChar
+ canClose = isNextPunctChar
+ }
+
+ if (!canOpen && !canClose) {
+ // middle of word
+ if (isSingle) {
+ token.content = replaceAt(token.content, t.index, APOSTROPHE)
+ }
+ continue
+ }
+
+ if (canClose) {
+ // this could be a closing quote, rewind the stack to get a match
+ for (j = stack.length - 1; j >= 0; j--) {
+ let item = stack[j]
+ if (stack[j].level < thisLevel) { break }
+ if (item.single === isSingle && stack[j].level === thisLevel) {
+ item = stack[j]
+
+ let openQuote
+ let closeQuote
+ if (isSingle) {
+ openQuote = state.md.options.quotes[2]
+ closeQuote = state.md.options.quotes[3]
+ } else {
+ openQuote = state.md.options.quotes[0]
+ closeQuote = state.md.options.quotes[1]
+ }
+
+ // replace token.content *before* tokens[item.token].content,
+ // because, if they are pointing at the same token, replaceAt
+ // could mess up indices when quote length != 1
+ token.content = replaceAt(token.content, t.index, closeQuote)
+ tokens[item.token].content = replaceAt(
+ tokens[item.token].content, item.pos, openQuote)
+
+ pos += closeQuote.length - 1
+ if (item.token === i) { pos += openQuote.length - 1 }
+
+ text = token.content
+ max = text.length
+
+ 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)
+ }
+ }
+ }
+}
+
+export default function smartquotes (state) {
+ /* eslint max-depth:0 */
+ if (!state.md.options.typographer) { return }
+
+ for (let blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) {
+ if (state.tokens[blkIdx].type !== 'inline' ||
+ !QUOTE_TEST_RE.test(state.tokens[blkIdx].content)) {
+ continue
+ }
+
+ process_inlines(state.tokens[blkIdx].children, state)
+ }
+}