Skip to content

Commit daf5a3c

Browse files
committed
Backported 15.0.2 fixes
1 parent a855f10 commit daf5a3c

2 files changed

Lines changed: 53 additions & 27 deletions

File tree

‎lib/rules_core/smartquotes.mjs‎

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import { isWhiteSpace, isPunctCharCode, isMdAsciiPunct } from '../common/utils.m
66
const QUOTE_TEST_RE = /['"]/
77
const QUOTE_RE = /['"]/g
88
const APOSTROPHE = '\u2019' /* ’ */
9+
// Caps memory on malicious input
10+
const MAX_OPENERS = 1000
11+
12+
function truncateStack (stack, heads, length) {
13+
while (stack.length > length) {
14+
const item = stack.pop()
15+
if (item.isSingleQuote) {
16+
heads.single = item.prevSameQuoteIdx
17+
} else {
18+
heads.double = item.prevSameQuoteIdx
19+
}
20+
}
21+
}
922

1023
function addReplacement (replacements, tokenIdx, pos, ch) {
1124
if (!replacements[tokenIdx]) {
@@ -35,6 +48,8 @@ function process_inlines (tokens, state) {
3548
let j
3649

3750
const stack = []
51+
// stack indexes of topmost openers of each type (-1 if none)
52+
const heads = { single: -1, double: -1 }
3853
// token index -> list of replacements in the original token content
3954
const replacements = {}
4055

@@ -46,7 +61,7 @@ function process_inlines (tokens, state) {
4661
for (j = stack.length - 1; j >= 0; j--) {
4762
if (stack[j].level <= thisLevel) { break }
4863
}
49-
stack.length = j + 1
64+
truncateStack(stack, heads, j + 1)
5065

5166
if (token.type !== 'text') { continue }
5267

@@ -150,39 +165,45 @@ function process_inlines (tokens, state) {
150165
}
151166

152167
if (canClose) {
153-
// this could be a closing quote, rewind the stack to get a match
154-
for (j = stack.length - 1; j >= 0; j--) {
155-
let item = stack[j]
156-
if (stack[j].level < thisLevel) { break }
157-
if (item.single === isSingle && stack[j].level === thisLevel) {
158-
item = stack[j]
159-
160-
let openQuote
161-
let closeQuote
162-
if (isSingle) {
163-
openQuote = state.md.options.quotes[2]
164-
closeQuote = state.md.options.quotes[3]
165-
} else {
166-
openQuote = state.md.options.quotes[0]
167-
closeQuote = state.md.options.quotes[1]
168-
}
169-
170-
addReplacement(replacements, i, t.index, closeQuote)
171-
addReplacement(replacements, item.token, item.pos, openQuote)
172-
173-
stack.length = j
174-
continue OUTER
168+
// Stack levels never decrease, so an opener of this type below
169+
// the current level means there is no match
170+
j = isSingle ? heads.single : heads.double
171+
if (j >= 0 && stack[j].level === thisLevel) {
172+
const item = stack[j]
173+
174+
let openQuote
175+
let closeQuote
176+
if (isSingle) {
177+
openQuote = state.md.options.quotes[2]
178+
closeQuote = state.md.options.quotes[3]
179+
} else {
180+
openQuote = state.md.options.quotes[0]
181+
closeQuote = state.md.options.quotes[1]
175182
}
183+
184+
addReplacement(replacements, i, t.index, closeQuote)
185+
addReplacement(replacements, item.tokenIdx, item.contentPos, openQuote)
186+
187+
truncateStack(stack, heads, j)
188+
continue OUTER
176189
}
177190
}
178191

179192
if (canOpen) {
193+
if (stack.length >= MAX_OPENERS) { return }
180194
stack.push({
181-
token: i,
182-
pos: t.index,
183-
single: isSingle,
184-
level: thisLevel
195+
tokenIdx: i,
196+
contentPos: t.index,
197+
isSingleQuote: isSingle,
198+
level: thisLevel,
199+
// stack index of the previous opener of the same quote type, -1 if none
200+
prevSameQuoteIdx: isSingle ? heads.single : heads.double
185201
})
202+
if (isSingle) {
203+
heads.single = stack.length - 1
204+
} else {
205+
heads.double = stack.length - 1
206+
}
186207
} else if (canClose && isSingle) {
187208
addReplacement(replacements, i, t.index, APOSTROPHE)
188209
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,10 @@ describe('Pathological sequences speed', () => {
164164
it('a lot of smartquotes', async () => {
165165
await test_pattern('"'.repeat(160000), { typographer: true })
166166
})
167+
168+
it('smartquotes with mismatched opener and closer types', async () => {
169+
await test_pattern('"a '.repeat(100000) + "b' ".repeat(100000), { typographer: true })
170+
await test_pattern("'a ".repeat(100000) + 'b" '.repeat(100000), { typographer: true })
171+
})
167172
})
168173
})

0 commit comments

Comments
 (0)