Browse Source

Fix quadratic complexity in smartquotes rule when quote types don't match (#1209)

master
GSGFs7 4 weeks ago
committed by GitHub
parent
commit
1b379c1b6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 74
      src/rules_core/smartquotes.ts
  2. 4
      test/markdown-it/pathological.test.mjs

74
src/rules_core/smartquotes.ts

@ -16,6 +16,14 @@ interface Replacement {
type ReplacementMap = Record<string, Replacement[]>
interface QuoteOpener {
token: number
pos: number
single: boolean
level: number
prevSame: number
}
function addReplacement (
replacements: ReplacementMap,
tokenIdx: number,
@ -48,10 +56,21 @@ function applyReplacements (str: string, replacements: Replacement[]) {
function process_inlines (tokens: Token[], state: StateCore) {
let j
const stack = []
const stack: QuoteOpener[] = []
const heads = new Map<number, { single: number, double: number }>()
// token index -> list of replacements in the original token content
const replacements: ReplacementMap = {}
function truncateStack (length: number) {
// Each opener is removed at most once
while (stack.length > length) {
const item = stack.pop()!
const head = heads.get(item.level)!
head[item.single ? 'single' : 'double'] = item.prevSame
if (head.single === -1 && head.double === -1) heads.delete(item.level)
}
}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
@ -60,7 +79,7 @@ function process_inlines (tokens: Token[], state: StateCore) {
for (j = stack.length - 1; j >= 0; j--) {
if (stack[j].level <= thisLevel) { break }
}
stack.length = j + 1
truncateStack(j + 1)
if (token.type !== 'text') { continue }
@ -164,39 +183,44 @@ function process_inlines (tokens: Token[], state: StateCore) {
}
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]
}
addReplacement(replacements, i, t.index, closeQuote)
addReplacement(replacements, item.token, item.pos, openQuote)
stack.length = j
continue OUTER
// Index by level and type
j = heads.get(thisLevel)?.[isSingle ? 'single' : 'double'] ?? -1
if (j >= 0) {
const 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]
}
addReplacement(replacements, i, t.index, closeQuote)
addReplacement(replacements, item.token, item.pos, openQuote)
truncateStack(j)
continue OUTER
}
}
if (canOpen) {
let head = heads.get(thisLevel)
if (!head) {
head = { single: -1, double: -1 }
heads.set(thisLevel, head)
}
const kind = isSingle ? 'single' : 'double'
stack.push({
token: i,
pos: t.index,
single: isSingle,
level: thisLevel
level: thisLevel,
prevSame: head[kind]
})
head[kind] = stack.length - 1
} else if (canClose && isSingle) {
addReplacement(replacements, i, t.index, APOSTROPHE)
}

4
test/markdown-it/pathological.test.mjs

@ -164,5 +164,9 @@ describe('Pathological sequences speed', () => {
it('a lot of smartquotes', async () => {
await test_pattern('"'.repeat(160000), { typographer: true })
})
it('smartquotes with mismatched opener and closer types', async () => {
await test_pattern('"a '.repeat(100000) + "b' ".repeat(100000), { typographer: true })
})
})
})

Loading…
Cancel
Save