From e96754a03b27f52b96dc21335d8dbb1b1dd6dcea Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Wed, 8 Oct 2025 10:10:41 +0100 Subject: [PATCH] Merge pull request #28893 from overleaf/mj-linter-brace-check [web] Allow braces in documentclass options GitOrigin-RevId: 9675d3fc760a3b7d402c5a9df57a0cf183a1e648 --- .../latex/linter/latex-linter.worker.js | 20 ++++++++++++++-- .../languages/latex/latex-linter.test.ts | 23 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js b/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js index c496ce767f..f1d86e369f 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js +++ b/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js @@ -338,11 +338,20 @@ const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) { } let label = '' + let groupDepth = 0 let j, tok for (j = k + 1; (tok = Tokens[j]); j++) { if (tok[1] === '{') { - // unclosed label - break + if (label.length === 0) { + // We haven't seen a [ yet, so there is no optional argument + break + } + groupDepth++ + } else if (tok[1] === '}') { + groupDepth-- + if (groupDepth < 0) { + break + } } else if (tok[1] === 'Text') { const str = text.substring(tok[2], tok[3]) label = label + str @@ -357,6 +366,13 @@ const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) { break // breaking due to unrecognised token } } + if (groupDepth !== 0) { + const missing = groupDepth > 0 ? '{' : '}' + // mismatched braces + const e = new Error(`Unmatched ${missing} in label`) + e.pos = j + 1 + return e + } if (label.length === 0) { return null diff --git a/services/web/test/frontend/features/source-editor/languages/latex/latex-linter.test.ts b/services/web/test/frontend/features/source-editor/languages/latex/latex-linter.test.ts index f5c374077f..c3f2db2383 100644 --- a/services/web/test/frontend/features/source-editor/languages/latex/latex-linter.test.ts +++ b/services/web/test/frontend/features/source-editor/languages/latex/latex-linter.test.ts @@ -473,9 +473,8 @@ describe('LatexLinter', function () { it('should reject an unclosed hyperref label', function () { const { errors } = Parse('\\hyperref[foo_bar{foo bar}') - assert.equal(errors.length, 2) + assert.equal(errors.length, 1) assert.equal(errors[0].text, 'invalid hyperref label') - assert.equal(errors[1].text, 'unexpected close group }') }) it('should accept a hyperref command without an optional argument', function () { @@ -511,6 +510,26 @@ describe('LatexLinter', function () { assert.equal(errors.length, 0) }) + it('should accept a documentclass with braces in options', function () { + const { errors } = Parse( + '\\documentclass[a4paper,margin={1in,0.5in}]{article}' + ) + assert.equal(errors.length, 0) + }) + + it('should reject documentclass with unbalanced braces in options', function () { + const { errors } = Parse('\\documentclass[foo={bar]{article}') + assert.equal(errors.length, 2) + assert.equal(errors[0].text, 'invalid documentclass option') + assert.equal(errors[1].text, 'unexpected close group }') + }) + + it('should reject documentclass with out of order braces in options', function () { + const { errors } = Parse('\\documentclass[foo=}bar{]{article}') + assert.equal(errors.length, 1) + assert.equal(errors[0].text, 'invalid documentclass option') + }) + // %novalidate // %begin novalidate // %end novalidate