Browse Source

Fix smartquotes perfomance

pull/1163/head
Vitaly Puzrin 5 months ago
parent
commit
9ce2087562
  1. 3
      CHANGELOG.md
  2. 52
      lib/rules_core/smartquotes.mjs
  3. 4
      test/pathological.mjs

3
CHANGELOG.md

@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- More strict entities decode to avoid false positives `;`, #1096.
- Restore block parser state on fail in `lheading` rule, #1131.
### Security
- Fixed poor smartquotes perfomance on > 70k quotes in single block
## [14.1.1] - 2026-01-11
### Security

52
lib/rules_core/smartquotes.mjs

@ -7,14 +7,36 @@ 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 addReplacement (replacements, tokenIdx, pos, ch) {
if (!replacements[tokenIdx]) {
replacements[tokenIdx] = []
}
replacements[tokenIdx].push({ pos, ch })
}
function applyReplacements (str, replacements) {
let result = ''
let lastPos = 0
replacements.sort((a, b) => a.pos - b.pos)
for (let i = 0; i < replacements.length; i++) {
const replacement = replacements[i]
result += str.slice(lastPos, replacement.pos) + replacement.ch
lastPos = replacement.pos + 1
}
return result + str.slice(lastPos)
}
function process_inlines (tokens, state) {
let j
const stack = []
// token index -> list of replacements in the original token content
const replacements = {}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
@ -28,9 +50,9 @@ function process_inlines (tokens, state) {
if (token.type !== 'text') { continue }
let text = token.content
const text = token.content
let pos = 0
let max = text.length
const max = text.length
/* eslint no-labels:0,block-scoped-var:0 */
OUTER:
@ -122,7 +144,7 @@ function process_inlines (tokens, state) {
if (!canOpen && !canClose) {
// middle of word
if (isSingle) {
token.content = replaceAt(token.content, t.index, APOSTROPHE)
addReplacement(replacements, i, t.index, APOSTROPHE)
}
continue
}
@ -145,18 +167,8 @@ function process_inlines (tokens, state) {
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
addReplacement(replacements, i, t.index, closeQuote)
addReplacement(replacements, item.token, item.pos, openQuote)
stack.length = j
continue OUTER
@ -172,10 +184,14 @@ function process_inlines (tokens, state) {
level: thisLevel
})
} else if (canClose && isSingle) {
token.content = replaceAt(token.content, t.index, APOSTROPHE)
addReplacement(replacements, i, t.index, APOSTROPHE)
}
}
}
Object.keys(replacements).forEach(function (tokenIdx) {
tokens[tokenIdx].content = applyReplacements(tokens[tokenIdx].content, replacements[tokenIdx])
})
}
export default function smartquotes (state) {

4
test/pathological.mjs

@ -170,5 +170,9 @@ describe('Pathological sequences speed', () => {
{ linkify: true }
)
})
it('a lot of smartquotes', async () => {
await test_pattern('"'.repeat(160000), { typographer: true })
})
})
})

Loading…
Cancel
Save