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
This commit is contained in:
Brian Gough 2023-09-27 08:58:21 +01:00 committed by Copybot
parent 6ffaeb7a92
commit 45ca0f796c
2 changed files with 141 additions and 5 deletions

View file

@ -271,7 +271,7 @@ const read1name = function (TokeniseResult, k) {
// handle names like FOO_BAR // handle names like FOO_BAR
let delimiterName = '' let delimiterName = ''
let j, tok let j, tok
for (j = k + 2, tok; (tok = Tokens[j]); j++) { for (j = k + 2; (tok = Tokens[j]); j++) {
if (tok[1] === 'Text') { if (tok[1] === 'Text') {
const str = text.substring(tok[2], tok[3]) const str = text.substring(tok[2], tok[3])
if (!str.match(/^\S*$/)) { if (!str.match(/^\S*$/)) {
@ -302,7 +302,7 @@ const read1filename = function (TokeniseResult, k) {
let fileName = '' let fileName = ''
let j, tok let j, tok
for (j = k + 1, tok; (tok = Tokens[j]); j++) { for (j = k + 1; (tok = Tokens[j]); j++) {
if (tok[1] === 'Text') { if (tok[1] === 'Text') {
const str = text.substring(tok[2], tok[3]) const str = text.substring(tok[2], tok[3])
if (!str.match(/^\S*$/)) { 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) { const readOptionalParams = function (TokeniseResult, k) {
// read an optional parameter [N] where N is a number, used // read an optional parameter [N] where N is a number, used
// for \newcommand{\foo}[2]... meaning 2 parameters // for \newcommand{\foo}[2]... meaning 2 parameters
@ -596,20 +644,34 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
const nextGroupMathModeStack = [] // tracking all nextGroupMathModes const nextGroupMathModeStack = [] // tracking all nextGroupMathModes
let seenUserDefinedBeginEquation = false // if we have seen macros like \beq let seenUserDefinedBeginEquation = false // if we have seen macros like \beq
let seenUserDefinedEndEquation = false // if we have seen macros like \eeq 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 // Iterate over the tokens, looking for environments to match
// //
// Push environment command found (\begin, \end) onto the // Push environment command found (\begin, \end) onto the
// Environments array. // 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 token = Tokens[i]
// const line = token[0] // const line = token[0]
const type = token[1] const type = token[1]
// const start = token[2] // const start = token[2]
// const end = token[3] // const end = token[3]
const seq = token[4] 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 === '{') { if (type === '{') {
// handle open group as a type of environment // handle open group as a type of environment
Environments.push({ Environments.push({
@ -667,7 +729,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
if (open && open[1] === '{' && delimiter && delimiter[1] === 'Text') { if (open && open[1] === '{' && delimiter && delimiter[1] === 'Text') {
let delimiterName = '' let delimiterName = ''
let j, tok let j, tok
for (j = i + 2, tok; (tok = Tokens[j]); j++) { for (j = i + 2; (tok = Tokens[j]); j++) {
if (tok[1] === 'Text') { if (tok[1] === 'Text') {
const str = text.substring(tok[2], tok[3]) const str = text.substring(tok[2], tok[3])
if (!str.match(/^\S*$/)) { if (!str.match(/^\S*$/)) {
@ -973,6 +1035,30 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
i = newPos i = newPos
} }
nextGroupMathMode = false 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') { } else if (seq === 'resizebox') {
// try to read any optional params [BAR]...., advance if found // try to read any optional params [BAR]...., advance if found
let newPos = readOptionalGeneric(TokeniseResult, i) let newPos = readOptionalGeneric(TokeniseResult, i)

View file

@ -444,6 +444,56 @@ describe('LatexLinter', function () {
assert.equal(errors.length, 0) 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 // %novalidate
// %begin novalidate // %begin novalidate
// %end novalidate // %end novalidate