Răsfoiți Sursa

Merge pull request #18676 from overleaf/mj-web-linter-document-class

[web] Allow underscore in optional argument in \documentclass

GitOrigin-RevId: c6f8716548411ade03814c8123df76fa8aa70ecc
Mathias Jakobsen 2 ani în urmă
părinte
comite
f0eba8e742

+ 1 - 2
services/web/frontend/js/features/source-editor/languages/latex/latex-language.ts

@@ -152,8 +152,7 @@ export const LaTeXLanguage = LRLanguage.define({
         'HrefCommand/ShortTextArgument/ShortArg/...': t.link,
         'HrefCommand/ShortTextArgument/ShortArg/...': t.link,
         'HrefCommand/UrlArgument/...': t.monospace,
         'HrefCommand/UrlArgument/...': t.monospace,
         'CtrlSeq Csname': t.tagName,
         'CtrlSeq Csname': t.tagName,
-        'DocumentClass/OptionalArgument/ShortOptionalArg/Normal':
-          t.attributeValue,
+        'DocumentClass/OptionalArgument/ShortOptionalArg/...': t.attributeValue,
         'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
         'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
         'ListEnvironment/BeginEnv/OptionalArgument/...': t.monospace,
         'ListEnvironment/BeginEnv/OptionalArgument/...': t.monospace,
         Number: t.number,
         Number: t.number,

+ 28 - 2
services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js

@@ -322,7 +322,7 @@ const read1filename = function (TokeniseResult, k) {
   }
   }
 }
 }
 
 
-const readOptionalLabel = function (TokeniseResult, k) {
+const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) {
   // read a label my_label:text..
   // read a label my_label:text..
   const Tokens = TokeniseResult.tokens
   const Tokens = TokeniseResult.tokens
   const text = TokeniseResult.text
   const text = TokeniseResult.text
@@ -348,6 +348,7 @@ const readOptionalLabel = function (TokeniseResult, k) {
       label = label + str
       label = label + str
       if (str.match(/\]/)) {
       if (str.match(/\]/)) {
         // breaking due to ]
         // breaking due to ]
+        j++
         break
         break
       }
       }
     } else if (tok[1] === '_') {
     } else if (tok[1] === '_') {
@@ -800,6 +801,31 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
       ) {
       ) {
         // Environments.push({command: "end", name: "user-defined-equation", token: token});
         // Environments.push({command: "end", name: "user-defined-equation", token: token});
         seenUserDefinedEndEquation = true
         seenUserDefinedEndEquation = true
+      } else if (seq === 'documentclass') {
+        // try to read any optional params [LABEL].... allowing for
+        // underscores, advance if found
+        let newPos = readOptionalArgumentWithUnderscores(TokeniseResult, i)
+        if (newPos instanceof Error) {
+          TokenErrorFromTo(
+            Tokens[i + 1],
+            Tokens[Math.min(newPos.pos, len - 1)],
+            'invalid documentclass option'
+          )
+          i = newPos.pos
+        } else if (newPos == null) {
+          /* do nothing */
+        } else {
+          i = newPos
+        }
+        // Read parameter {....}, ignore if missing
+        newPos = readDefinition(TokeniseResult, i)
+        if (newPos === null) {
+          // NOTE: We could choose to throw an error here, as the argument is
+          // required. However, that would show errors as you are typing. So
+          // maybe it's better to be lenient.
+        } else {
+          i = newPos
+        }
       } else if (
       } else if (
         seq === 'newcommand' ||
         seq === 'newcommand' ||
         seq === 'renewcommand' ||
         seq === 'renewcommand' ||
@@ -1038,7 +1064,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
       } else if (seq === 'hyperref') {
       } else if (seq === 'hyperref') {
         // try to read any optional params [LABEL].... allowing for
         // try to read any optional params [LABEL].... allowing for
         // underscores, advance if found
         // underscores, advance if found
-        let newPos = readOptionalLabel(TokeniseResult, i)
+        let newPos = readOptionalArgumentWithUnderscores(TokeniseResult, i)
         if (newPos instanceof Error) {
         if (newPos instanceof Error) {
           TokenErrorFromTo(
           TokenErrorFromTo(
             Tokens[i + 1],
             Tokens[i + 1],

+ 1 - 0
services/web/frontend/js/features/source-editor/lezer-latex/latex.grammar

@@ -484,6 +484,7 @@ ShortOptionalArg {
     ( textBase
     ( textBase
     | NonEmptyGroup<ShortOptionalArg>
     | NonEmptyGroup<ShortOptionalArg>
     | "#" // macro character
     | "#" // macro character
+    | "_" // underscore is used in some parameter names
     )*
     )*
 }
 }
 
 

+ 17 - 0
services/web/test/frontend/features/source-editor/languages/latex/latex-linter.test.ts

@@ -494,6 +494,23 @@ describe('LatexLinter', function () {
     assert.equal(errors[0].text, 'unclosed group {')
     assert.equal(errors[0].text, 'unclosed group {')
   })
   })
 
 
+  it('should accept documentclass with no options', function () {
+    const { errors } = Parse('\\documentclass{article}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept documentclass with options', function () {
+    const { errors } = Parse('\\documentclass[a4paper]{article}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept documentclass with underscore in options', function () {
+    const { errors } = Parse(
+      '\\documentclass[my_custom_document_class_option]{my-custom-class}'
+    )
+    assert.equal(errors.length, 0)
+  })
+
   // %novalidate
   // %novalidate
   // %begin novalidate
   // %begin novalidate
   // %end novalidate
   // %end novalidate