Просмотр исходного кода

Merge pull request #14934 from overleaf/revert-14926-revert-14121-bg-best-allow-underscore-in-hyperref-labels

Revert "Revert "allow underscore in hyperref labels""

GitOrigin-RevId: f7b2dd418fa9c0940b778604ed08eccab78f97d2
Brian Gough 3 лет назад
Родитель
Сommit
45ca0f796c

+ 91 - 5
services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js

@@ -271,7 +271,7 @@ const read1name = function (TokeniseResult, k) {
     // handle names like FOO_BAR
     let delimiterName = ''
     let j, tok
-    for (j = k + 2, tok; (tok = Tokens[j]); j++) {
+    for (j = k + 2; (tok = Tokens[j]); j++) {
       if (tok[1] === 'Text') {
         const str = text.substring(tok[2], tok[3])
         if (!str.match(/^\S*$/)) {
@@ -302,7 +302,7 @@ const read1filename = function (TokeniseResult, k) {
 
   let fileName = ''
   let j, tok
-  for (j = k + 1, tok; (tok = Tokens[j]); j++) {
+  for (j = k + 1; (tok = Tokens[j]); j++) {
     if (tok[1] === 'Text') {
       const str = text.substring(tok[2], tok[3])
       if (!str.match(/^\S*$/)) {
@@ -322,6 +322,54 @@ const read1filename = function (TokeniseResult, k) {
   }
 }
 
+const readOptionalLabel = function (TokeniseResult, k) {
+  // read a label my_label:text..
+  const Tokens = TokeniseResult.tokens
+  const text = TokeniseResult.text
+
+  const params = Tokens[k + 1]
+
+  // Quick check for arguments like [label]
+  if (params && params[1] === 'Text') {
+    const paramNum = text.substring(params[2], params[3])
+    if (paramNum.match(/^(\[[^\]]*\])*\s*$/)) {
+      return k + 1 // got it
+    }
+  }
+
+  let label = ''
+  let j, tok
+  for (j = k + 1; (tok = Tokens[j]); j++) {
+    if (tok[1] === '{') {
+      // unclosed label
+      break
+    } else if (tok[1] === 'Text') {
+      const str = text.substring(tok[2], tok[3])
+      label = label + str
+      if (str.match(/\]/)) {
+        // breaking due to ]
+        break
+      }
+    } else if (tok[1] === '_') {
+      label = label + tok[1]
+    } else {
+      break // breaking due to unrecognised token
+    }
+  }
+
+  if (label.length === 0) {
+    return null
+  } else if (label.length > 0 && /^\[[^\]]*\]\s*$/.test(label)) {
+    // make sure the label is of the form [label]
+    return j - 1 // advance past these tokens
+  } else {
+    // invalid label
+    const e = new Error('Invalid label')
+    e.pos = j + 1
+    return e
+  }
+}
+
 const readOptionalParams = function (TokeniseResult, k) {
   // read an optional parameter [N] where N is a number, used
   // for \newcommand{\foo}[2]... meaning 2 parameters
@@ -596,20 +644,34 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
   const nextGroupMathModeStack = [] // tracking all nextGroupMathModes
   let seenUserDefinedBeginEquation = false // if we have seen macros like \beq
   let seenUserDefinedEndEquation = false // if we have seen macros like \eeq
+  let seenInfiniteLoop = false // if we have seen an infinite loop in the linter
 
   // Iterate over the tokens, looking for environments to match
   //
   // Push environment command found (\begin, \end) onto the
   // Environments array.
 
-  for (let i = 0, len = Tokens.length; i < len; i++) {
+  for (let i = 0, len = Tokens.length, lastPos = -1; i < len; i++) {
     const token = Tokens[i]
     // const line = token[0]
     const type = token[1]
     // const start = token[2]
     // const end = token[3]
     const seq = token[4]
-
+    if (i > lastPos) {
+      // advanced successfully through the tokens
+      lastPos = i
+    } else {
+      // we're not moving forward, so force the parsing to advance
+      if (!seenInfiniteLoop)
+        console.error('infinite loop in linter detected', lastPos, i, token)
+      lastPos = lastPos + 1
+      i = lastPos + 1
+      seenInfiniteLoop = true
+      if (i >= len) {
+        break
+      }
+    }
     if (type === '{') {
       // handle open group as a type of environment
       Environments.push({
@@ -667,7 +729,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
           if (open && open[1] === '{' && delimiter && delimiter[1] === 'Text') {
             let delimiterName = ''
             let j, tok
-            for (j = i + 2, tok; (tok = Tokens[j]); j++) {
+            for (j = i + 2; (tok = Tokens[j]); j++) {
               if (tok[1] === 'Text') {
                 const str = text.substring(tok[2], tok[3])
                 if (!str.match(/^\S*$/)) {
@@ -973,6 +1035,30 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
           i = newPos
         }
         nextGroupMathMode = false
+      } else if (seq === 'hyperref') {
+        // try to read any optional params [LABEL].... allowing for
+        // underscores, advance if found
+        let newPos = readOptionalLabel(TokeniseResult, i)
+        if (newPos instanceof Error) {
+          TokenErrorFromTo(
+            Tokens[i + 1],
+            Tokens[Math.min(newPos.pos, len - 1)],
+            'invalid hyperref label'
+          )
+          i = newPos.pos
+        } else if (newPos == null) {
+          /* do nothing */
+        } else {
+          i = newPos
+        }
+        // try to read parameter {....}, advance if found
+        newPos = readDefinition(TokeniseResult, i)
+        if (newPos === null) {
+          /* do nothing */
+        } else {
+          i = newPos
+        }
+        nextGroupMathMode = false
       } else if (seq === 'resizebox') {
         // try to read any optional params [BAR]...., advance if found
         let newPos = readOptionalGeneric(TokeniseResult, i)

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

@@ -444,6 +444,56 @@ describe('LatexLinter', function () {
     assert.equal(errors.length, 0)
   })
 
+  it('should accept a plain hyperref command', function () {
+    const { errors } = Parse('\\hyperref{http://www.overleaf.com/}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept a hyperref command with underscores in the url ', function () {
+    const { errors } = Parse('\\hyperref{http://www.overleaf.com/my_page.html}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept a hyperref command with category, name and text arguments ', function () {
+    const { errors } = Parse(
+      '\\hyperref{http://www.overleaf.com/}{category}{name}{text}'
+    )
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept an underscore in a hyperref label', function () {
+    const { errors } = Parse('\\hyperref[foo_bar]{foo bar}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should reject a $ in a hyperref label', function () {
+    const { errors } = Parse('\\hyperref[foo$bar]{foo bar}')
+    assert.equal(errors.length, 1)
+  })
+
+  it('should reject an unclosed hyperref label', function () {
+    const { errors } = Parse('\\hyperref[foo_bar{foo bar}')
+    assert.equal(errors.length, 2)
+    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 () {
+    const { errors } = Parse('{\\hyperref{hello}}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept a hyperref command without an optional argument and multiple other arguments', function () {
+    const { errors } = Parse('{\\hyperref{}{}{fig411}}')
+    assert.equal(errors.length, 0)
+  })
+
+  it('should accept a hyperref command without an optional argument in an unclosed group', function () {
+    const { errors } = Parse('{\\hyperref{}{}{fig411}')
+    assert.equal(errors.length, 1)
+    assert.equal(errors[0].text, 'unclosed group {')
+  })
+
   // %novalidate
   // %begin novalidate
   // %end novalidate