Skip to content

Commit 56700eb

Browse files
authored
Fix performance regression in src/resources/filters/quarto-pre/engine-escape.lua (#14162)
* avoid O(n^2) backtracking (#14156) * changelog * move local decl inside function to avoid too many variables
1 parent 09539d1 commit 56700eb

File tree

2 files changed

+178
-150
lines changed

2 files changed

+178
-150
lines changed

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,4 @@ All changes included in 1.9:
222222
- ([#13998](https://github.com/quarto-dev/quarto-cli/issues/13998)): Fix YAML validation error with CR-only line terminators (old Mac format). Documents using `\r` line endings no longer fail with "Expected YAML front matter to contain at least 2 lines".
223223
- ([#14012](https://github.com/quarto-dev/quarto-cli/issues/14012)): Add `fr-CA` language translation for Quebec French inclusive writing conventions, using parenthetical forms instead of middle dots for author labels. (author: @tdhock)
224224
- ([#14032](https://github.com/quarto-dev/quarto-cli/issues/14032)): Add `editor_options` with `chunk_output_type` to YAML schema for autocompletion and validation in RStudio and Positron.
225+
- ([#14156](https://github.com/quarto-dev/quarto-cli/issues/14156)): Avoid O(n^2) performance in handling large code blocks.

src/resources/filters/quarto-pre/engine-escape.lua

Lines changed: 177 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@
44
local patterns = require("modules/patterns")
55

66
function engine_escape()
7+
-- Line-by-line replacement for the pattern (\n?[^`\n]+`+){({+([^<}]+)}+)}
8+
-- which suffers from O(n^2) backtracking on long lines without backticks.
9+
-- See https://github.com/quarto-dev/quarto-cli/issues/14156
10+
--
11+
-- The original pattern cannot cross newlines (due to [^`\n]+), so processing
12+
-- per-line is semantically equivalent and avoids catastrophic backtracking.
13+
local line_pattern = "([^`\n]+`+)" .. patterns.engine_escape
14+
local function unescape_inline_engine_codes(text)
15+
if not text:find("{{", 1, true) then
16+
return text
17+
end
18+
local result = {}
19+
local pos = 1
20+
local len = #text
21+
while pos <= len do
22+
local nl = text:find("\n", pos, true)
23+
local line
24+
if nl then
25+
line = text:sub(pos, nl)
26+
pos = nl + 1
27+
else
28+
line = text:sub(pos)
29+
pos = len + 1
30+
end
31+
if line:find("`", 1, true) and line:find("{{", 1, true) then
32+
line = line:gsub(line_pattern, "%1%2")
33+
end
34+
result[#result + 1] = line
35+
end
36+
return table.concat(result)
37+
end
38+
739
return {
840
CodeBlock = function(el)
941

@@ -26,7 +58,7 @@ function engine_escape()
2658
end)
2759

2860
-- handles escaped inline code cells within a code block
29-
el.text = el.text:gsub("(\n?[^`\n]+`+)" .. patterns.engine_escape, "%1%2")
61+
el.text = unescape_inline_engine_codes(el.text)
3062
return el
3163
end,
3264

@@ -46,156 +78,151 @@ end
4678

4779
-- FIXME these should be determined dynamically
4880
local kHighlightClasses = {
49-
"abc",
50-
"actionscript",
51-
"ada",
52-
"agda",
53-
"apache",
54-
"asn1",
55-
"asp",
56-
"ats",
57-
"awk",
58-
"bash",
59-
"bibtex",
60-
"boo",
61-
"c",
62-
"changelog",
63-
"clojure",
64-
"cmake",
65-
"coffee",
66-
"coldfusion",
67-
"comments",
68-
"commonlisp",
69-
"cpp",
70-
"cs",
71-
"css",
72-
"curry",
73-
"d",
74-
"default",
75-
"diff",
76-
"djangotemplate",
77-
"dockerfile",
78-
"dot",
79-
"doxygen",
80-
"doxygenlua",
81-
"dtd",
82-
"eiffel",
83-
"elixir",
84-
"elm",
85-
"email",
86-
"erlang",
87-
"fasm",
88-
"fortranfixed",
89-
"fortranfree",
90-
"fsharp",
91-
"gap",
92-
"gcc",
93-
"glsl",
94-
"gnuassembler",
95-
"go",
96-
"graphql",
97-
"groovy",
98-
"hamlet",
99-
"haskell",
100-
"haxe",
101-
"html",
102-
"idris",
103-
"ini",
104-
"isocpp",
105-
"j",
106-
"java",
107-
"javadoc",
108-
"javascript",
109-
"javascriptreact",
110-
"json",
111-
"jsp",
112-
"julia",
113-
"kotlin",
114-
"latex",
115-
"lex",
116-
"lilypond",
117-
"literatecurry",
118-
"literatehaskell",
119-
"llvm",
120-
"lua",
121-
"m4",
122-
"makefile",
123-
"mandoc",
124-
"markdown",
125-
"mathematica",
126-
"matlab",
127-
"maxima",
128-
"mediawiki",
129-
"metafont",
130-
"mips",
131-
"modelines",
132-
"modula2",
133-
"modula3",
134-
"monobasic",
135-
"mustache",
136-
"nasm",
137-
"nim",
138-
"noweb",
139-
"objectivec",
140-
"objectivecpp",
141-
"ocaml",
142-
"octave",
143-
"opencl",
144-
"pascal",
145-
"perl",
146-
"php",
147-
"pike",
148-
"postscript",
149-
"povray",
150-
"powershell",
151-
"prolog",
152-
"protobuf",
153-
"pure",
154-
"purebasic",
155-
"python",
156-
"qml",
157-
"r",
158-
"raku",
159-
"relaxng",
160-
"relaxngcompact",
161-
"rest",
162-
"rhtml",
163-
"roff",
164-
"ruby",
165-
"rust",
166-
"scala",
167-
"scheme",
168-
"sci",
169-
"sed",
170-
"sgml",
171-
"sml",
172-
"spdxcomments",
173-
"sql",
174-
"sqlmysql",
175-
"sqlpostgresql",
176-
"stata",
177-
"swift",
178-
"tcl",
179-
"tcsh",
180-
"texinfo",
181-
"toml",
182-
"typescript",
183-
"verilog",
184-
"vhdl",
185-
"xml",
186-
"xorg",
187-
"xslt",
188-
"xul",
189-
"yacc",
190-
"yaml",
191-
"zsh"
81+
["abc"] = true,
82+
["actionscript"] = true,
83+
["ada"] = true,
84+
["agda"] = true,
85+
["apache"] = true,
86+
["asn1"] = true,
87+
["asp"] = true,
88+
["ats"] = true,
89+
["awk"] = true,
90+
["bash"] = true,
91+
["bibtex"] = true,
92+
["boo"] = true,
93+
["c"] = true,
94+
["changelog"] = true,
95+
["clojure"] = true,
96+
["cmake"] = true,
97+
["coffee"] = true,
98+
["coldfusion"] = true,
99+
["comments"] = true,
100+
["commonlisp"] = true,
101+
["cpp"] = true,
102+
["cs"] = true,
103+
["css"] = true,
104+
["curry"] = true,
105+
["d"] = true,
106+
["default"] = true,
107+
["diff"] = true,
108+
["djangotemplate"] = true,
109+
["dockerfile"] = true,
110+
["dot"] = true,
111+
["doxygen"] = true,
112+
["doxygenlua"] = true,
113+
["dtd"] = true,
114+
["eiffel"] = true,
115+
["elixir"] = true,
116+
["elm"] = true,
117+
["email"] = true,
118+
["erlang"] = true,
119+
["fasm"] = true,
120+
["fortranfixed"] = true,
121+
["fortranfree"] = true,
122+
["fsharp"] = true,
123+
["gap"] = true,
124+
["gcc"] = true,
125+
["glsl"] = true,
126+
["gnuassembler"] = true,
127+
["go"] = true,
128+
["graphql"] = true,
129+
["groovy"] = true,
130+
["hamlet"] = true,
131+
["haskell"] = true,
132+
["haxe"] = true,
133+
["html"] = true,
134+
["idris"] = true,
135+
["ini"] = true,
136+
["isocpp"] = true,
137+
["j"] = true,
138+
["java"] = true,
139+
["javadoc"] = true,
140+
["javascript"] = true,
141+
["javascriptreact"] = true,
142+
["json"] = true,
143+
["jsp"] = true,
144+
["julia"] = true,
145+
["kotlin"] = true,
146+
["latex"] = true,
147+
["lex"] = true,
148+
["lilypond"] = true,
149+
["literatecurry"] = true,
150+
["literatehaskell"] = true,
151+
["llvm"] = true,
152+
["lua"] = true,
153+
["m4"] = true,
154+
["makefile"] = true,
155+
["mandoc"] = true,
156+
["markdown"] = true,
157+
["mathematica"] = true,
158+
["matlab"] = true,
159+
["maxima"] = true,
160+
["mediawiki"] = true,
161+
["metafont"] = true,
162+
["mips"] = true,
163+
["modelines"] = true,
164+
["modula2"] = true,
165+
["modula3"] = true,
166+
["monobasic"] = true,
167+
["mustache"] = true,
168+
["nasm"] = true,
169+
["nim"] = true,
170+
["noweb"] = true,
171+
["objectivec"] = true,
172+
["objectivecpp"] = true,
173+
["ocaml"] = true,
174+
["octave"] = true,
175+
["opencl"] = true,
176+
["pascal"] = true,
177+
["perl"] = true,
178+
["php"] = true,
179+
["pike"] = true,
180+
["postscript"] = true,
181+
["povray"] = true,
182+
["powershell"] = true,
183+
["prolog"] = true,
184+
["protobuf"] = true,
185+
["pure"] = true,
186+
["purebasic"] = true,
187+
["python"] = true,
188+
["qml"] = true,
189+
["r"] = true,
190+
["raku"] = true,
191+
["relaxng"] = true,
192+
["relaxngcompact"] = true,
193+
["rest"] = true,
194+
["rhtml"] = true,
195+
["roff"] = true,
196+
["ruby"] = true,
197+
["rust"] = true,
198+
["scala"] = true,
199+
["scheme"] = true,
200+
["sci"] = true,
201+
["sed"] = true,
202+
["sgml"] = true,
203+
["sml"] = true,
204+
["spdxcomments"] = true,
205+
["sql"] = true,
206+
["sqlmysql"] = true,
207+
["sqlpostgresql"] = true,
208+
["stata"] = true,
209+
["swift"] = true,
210+
["tcl"] = true,
211+
["tcsh"] = true,
212+
["texinfo"] = true,
213+
["toml"] = true,
214+
["typescript"] = true,
215+
["verilog"] = true,
216+
["vhdl"] = true,
217+
["xml"] = true,
218+
["xorg"] = true,
219+
["xslt"] = true,
220+
["xul"] = true,
221+
["yacc"] = true,
222+
["yaml"] = true,
223+
["zsh"] = true
192224
}
193225

194226
function isHighlightClass(class)
195-
for _, v in ipairs (kHighlightClasses) do
196-
if v == class then
197-
return true
198-
end
199-
end
200-
return false
227+
if kHighlightClasses[class] then return true else return false end
201228
end

0 commit comments

Comments
 (0)