diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 36d010ecdb..f20ed460c1 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -212,3 +212,4 @@ All changes included in 1.9: - ([#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". - ([#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) - ([#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. +- ([#14156](https://github.com/quarto-dev/quarto-cli/issues/14156)): Avoid O(n^2) performance in handling large code blocks. \ No newline at end of file diff --git a/src/resources/filters/quarto-pre/engine-escape.lua b/src/resources/filters/quarto-pre/engine-escape.lua index ee3c6f5925..87e4a67329 100644 --- a/src/resources/filters/quarto-pre/engine-escape.lua +++ b/src/resources/filters/quarto-pre/engine-escape.lua @@ -4,6 +4,38 @@ local patterns = require("modules/patterns") function engine_escape() + -- Line-by-line replacement for the pattern (\n?[^`\n]+`+){({+([^<}]+)}+)} + -- which suffers from O(n^2) backtracking on long lines without backticks. + -- See https://github.com/quarto-dev/quarto-cli/issues/14156 + -- + -- The original pattern cannot cross newlines (due to [^`\n]+), so processing + -- per-line is semantically equivalent and avoids catastrophic backtracking. + local line_pattern = "([^`\n]+`+)" .. patterns.engine_escape + local function unescape_inline_engine_codes(text) + if not text:find("{{", 1, true) then + return text + end + local result = {} + local pos = 1 + local len = #text + while pos <= len do + local nl = text:find("\n", pos, true) + local line + if nl then + line = text:sub(pos, nl) + pos = nl + 1 + else + line = text:sub(pos) + pos = len + 1 + end + if line:find("`", 1, true) and line:find("{{", 1, true) then + line = line:gsub(line_pattern, "%1%2") + end + result[#result + 1] = line + end + return table.concat(result) + end + return { CodeBlock = function(el) @@ -26,7 +58,7 @@ function engine_escape() end) -- handles escaped inline code cells within a code block - el.text = el.text:gsub("(\n?[^`\n]+`+)" .. patterns.engine_escape, "%1%2") + el.text = unescape_inline_engine_codes(el.text) return el end, @@ -46,156 +78,151 @@ end -- FIXME these should be determined dynamically local kHighlightClasses = { - "abc", - "actionscript", - "ada", - "agda", - "apache", - "asn1", - "asp", - "ats", - "awk", - "bash", - "bibtex", - "boo", - "c", - "changelog", - "clojure", - "cmake", - "coffee", - "coldfusion", - "comments", - "commonlisp", - "cpp", - "cs", - "css", - "curry", - "d", - "default", - "diff", - "djangotemplate", - "dockerfile", - "dot", - "doxygen", - "doxygenlua", - "dtd", - "eiffel", - "elixir", - "elm", - "email", - "erlang", - "fasm", - "fortranfixed", - "fortranfree", - "fsharp", - "gap", - "gcc", - "glsl", - "gnuassembler", - "go", - "graphql", - "groovy", - "hamlet", - "haskell", - "haxe", - "html", - "idris", - "ini", - "isocpp", - "j", - "java", - "javadoc", - "javascript", - "javascriptreact", - "json", - "jsp", - "julia", - "kotlin", - "latex", - "lex", - "lilypond", - "literatecurry", - "literatehaskell", - "llvm", - "lua", - "m4", - "makefile", - "mandoc", - "markdown", - "mathematica", - "matlab", - "maxima", - "mediawiki", - "metafont", - "mips", - "modelines", - "modula2", - "modula3", - "monobasic", - "mustache", - "nasm", - "nim", - "noweb", - "objectivec", - "objectivecpp", - "ocaml", - "octave", - "opencl", - "pascal", - "perl", - "php", - "pike", - "postscript", - "povray", - "powershell", - "prolog", - "protobuf", - "pure", - "purebasic", - "python", - "qml", - "r", - "raku", - "relaxng", - "relaxngcompact", - "rest", - "rhtml", - "roff", - "ruby", - "rust", - "scala", - "scheme", - "sci", - "sed", - "sgml", - "sml", - "spdxcomments", - "sql", - "sqlmysql", - "sqlpostgresql", - "stata", - "swift", - "tcl", - "tcsh", - "texinfo", - "toml", - "typescript", - "verilog", - "vhdl", - "xml", - "xorg", - "xslt", - "xul", - "yacc", - "yaml", - "zsh" + ["abc"] = true, + ["actionscript"] = true, + ["ada"] = true, + ["agda"] = true, + ["apache"] = true, + ["asn1"] = true, + ["asp"] = true, + ["ats"] = true, + ["awk"] = true, + ["bash"] = true, + ["bibtex"] = true, + ["boo"] = true, + ["c"] = true, + ["changelog"] = true, + ["clojure"] = true, + ["cmake"] = true, + ["coffee"] = true, + ["coldfusion"] = true, + ["comments"] = true, + ["commonlisp"] = true, + ["cpp"] = true, + ["cs"] = true, + ["css"] = true, + ["curry"] = true, + ["d"] = true, + ["default"] = true, + ["diff"] = true, + ["djangotemplate"] = true, + ["dockerfile"] = true, + ["dot"] = true, + ["doxygen"] = true, + ["doxygenlua"] = true, + ["dtd"] = true, + ["eiffel"] = true, + ["elixir"] = true, + ["elm"] = true, + ["email"] = true, + ["erlang"] = true, + ["fasm"] = true, + ["fortranfixed"] = true, + ["fortranfree"] = true, + ["fsharp"] = true, + ["gap"] = true, + ["gcc"] = true, + ["glsl"] = true, + ["gnuassembler"] = true, + ["go"] = true, + ["graphql"] = true, + ["groovy"] = true, + ["hamlet"] = true, + ["haskell"] = true, + ["haxe"] = true, + ["html"] = true, + ["idris"] = true, + ["ini"] = true, + ["isocpp"] = true, + ["j"] = true, + ["java"] = true, + ["javadoc"] = true, + ["javascript"] = true, + ["javascriptreact"] = true, + ["json"] = true, + ["jsp"] = true, + ["julia"] = true, + ["kotlin"] = true, + ["latex"] = true, + ["lex"] = true, + ["lilypond"] = true, + ["literatecurry"] = true, + ["literatehaskell"] = true, + ["llvm"] = true, + ["lua"] = true, + ["m4"] = true, + ["makefile"] = true, + ["mandoc"] = true, + ["markdown"] = true, + ["mathematica"] = true, + ["matlab"] = true, + ["maxima"] = true, + ["mediawiki"] = true, + ["metafont"] = true, + ["mips"] = true, + ["modelines"] = true, + ["modula2"] = true, + ["modula3"] = true, + ["monobasic"] = true, + ["mustache"] = true, + ["nasm"] = true, + ["nim"] = true, + ["noweb"] = true, + ["objectivec"] = true, + ["objectivecpp"] = true, + ["ocaml"] = true, + ["octave"] = true, + ["opencl"] = true, + ["pascal"] = true, + ["perl"] = true, + ["php"] = true, + ["pike"] = true, + ["postscript"] = true, + ["povray"] = true, + ["powershell"] = true, + ["prolog"] = true, + ["protobuf"] = true, + ["pure"] = true, + ["purebasic"] = true, + ["python"] = true, + ["qml"] = true, + ["r"] = true, + ["raku"] = true, + ["relaxng"] = true, + ["relaxngcompact"] = true, + ["rest"] = true, + ["rhtml"] = true, + ["roff"] = true, + ["ruby"] = true, + ["rust"] = true, + ["scala"] = true, + ["scheme"] = true, + ["sci"] = true, + ["sed"] = true, + ["sgml"] = true, + ["sml"] = true, + ["spdxcomments"] = true, + ["sql"] = true, + ["sqlmysql"] = true, + ["sqlpostgresql"] = true, + ["stata"] = true, + ["swift"] = true, + ["tcl"] = true, + ["tcsh"] = true, + ["texinfo"] = true, + ["toml"] = true, + ["typescript"] = true, + ["verilog"] = true, + ["vhdl"] = true, + ["xml"] = true, + ["xorg"] = true, + ["xslt"] = true, + ["xul"] = true, + ["yacc"] = true, + ["yaml"] = true, + ["zsh"] = true } function isHighlightClass(class) - for _, v in ipairs (kHighlightClasses) do - if v == class then - return true - end - end - return false + if kHighlightClasses[class] then return true else return false end end \ No newline at end of file