[web] Convert LaTeX linter to Typescript (#29483)

GitOrigin-RevId: ec79e95db03bbea60da01eb8b35989978e1ec33a
This commit is contained in:
Mathias Jakobsen
2025-11-10 10:42:32 +00:00
committed by Copybot
parent cd335419ff
commit 0110ac4715
4 changed files with 316 additions and 99 deletions

View File

@@ -6,7 +6,7 @@ export type LintError = {
startPos: number
endPos: number
pos: number
suppressIfEditing: boolean
suppressIfEditing?: boolean
type: 'info' | 'warning' | 'error'
text: string
}

View File

@@ -5,7 +5,7 @@ import { mergeCompatibleOverlappingDiagnostics } from './merge-overlapping-diagn
const lintWorker = new Worker(
/* webpackChunkName: "latex-linter-worker" */
new URL('./latex-linter.worker.js', import.meta.url),
new URL('./latex-linter.worker.ts', import.meta.url),
{ type: 'module' }
)

View File

@@ -1,6 +1,45 @@
const Tokenise = function (text) {
const Tokens = []
const Comments = []
import { LintError } from './errors-to-diagnostics'
type ControlSymbolToken = [
number,
string,
number,
number,
string,
'control-symbol',
]
type ControlSequenceToken = [number, string, number, number, string]
type SpecialCharacterToken = [number, string, number, number]
type TextToken = [number, 'Text', number, number]
type Token =
| ControlSymbolToken
| ControlSequenceToken
| SpecialCharacterToken
| TextToken
type Comment = [number, number, number]
type InvalidTokeniseResult = never[]
type TokeniseResult = {
tokens: Token[]
comments: Comment[]
linePosition: number[]
lineNumber: number
text: string
}
class TokeniserError extends Error {
public pos: number
constructor(message: string, pos: number) {
super(message)
this.pos = pos
}
}
const Tokenise = function (
text: string
): TokeniseResult | InvalidTokeniseResult {
const Tokens: Token[] = []
const Comments: Comment[] = []
let pos = -1
const SPECIAL = /[\\{}$&#^_~%]/g // match TeX special characters
const NEXTCS = /[^a-zA-Z]/g // match characters which aren't part of a TeX control sequence
@@ -168,9 +207,21 @@ const Tokenise = function (text) {
}
}
type LintOptions = {
allowStar?: boolean
allowCustomDelimiter?: boolean
suppressIfEditing?: boolean
mathMode?: boolean
errorAtStart?: boolean
}
// Functions for consuming TeX arguments
const read1arg = function (TokeniseResult, k, options) {
const read1arg = function (
TokeniseResult: TokeniseResult,
k: number,
options?: LintOptions
) {
// read an argument FOO to a either form of command
// \newcommand\FOO...
// \newcommand{\FOO}...
@@ -215,7 +266,7 @@ const read1arg = function (TokeniseResult, k, options) {
}
}
const readLetDefinition = function (TokeniseResult, k) {
const readLetDefinition = function (TokeniseResult: TokeniseResult, k: number) {
// read a let command (the equals sign is optional)
// \let\foo=\bar
// \let\foo=TOKEN
@@ -247,7 +298,7 @@ const readLetDefinition = function (TokeniseResult, k) {
}
}
const read1name = function (TokeniseResult, k) {
const read1name = function (TokeniseResult: TokeniseResult, k: number) {
// read an environemt name FOO in
// \newenvironment{FOO}...
const Tokens = TokeniseResult.tokens
@@ -295,7 +346,7 @@ const read1name = function (TokeniseResult, k) {
}
}
const read1filename = function (TokeniseResult, k) {
const read1filename = function (TokeniseResult: TokeniseResult, k: number) {
// read an filename foo_bar.tex
const Tokens = TokeniseResult.tokens
const text = TokeniseResult.text
@@ -322,7 +373,10 @@ const read1filename = function (TokeniseResult, k) {
}
}
const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) {
const readOptionalArgumentWithUnderscores = function (
TokeniseResult: TokeniseResult,
k: number
) {
// read a label my_label:text..
const Tokens = TokeniseResult.tokens
const text = TokeniseResult.text
@@ -369,8 +423,7 @@ const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) {
if (groupDepth !== 0) {
const missing = groupDepth > 0 ? '{' : '}'
// mismatched braces
const e = new Error(`Unmatched ${missing} in label`)
e.pos = j + 1
const e = new TokeniserError(`Unmatched ${missing} in label`, j + 1)
return e
}
@@ -381,13 +434,15 @@ const readOptionalArgumentWithUnderscores = function (TokeniseResult, k) {
return j - 1 // advance past these tokens
} else {
// invalid label
const e = new Error('Invalid label')
e.pos = j + 1
const e = new TokeniserError('Invalid label', j + 1)
return e
}
}
const readOptionalParams = function (TokeniseResult, k) {
const readOptionalParams = function (
TokeniseResult: TokeniseResult,
k: number
) {
// read an optional parameter [N] where N is a number, used
// for \newcommand{\foo}[2]... meaning 2 parameters
const Tokens = TokeniseResult.tokens
@@ -435,7 +490,10 @@ const readOptionalParams = function (TokeniseResult, k) {
return null
}
const readOptionalGeneric = function (TokeniseResult, k) {
const readOptionalGeneric = function (
TokeniseResult: TokeniseResult,
k: number
) {
// read an optional parameter [foo]
const Tokens = TokeniseResult.tokens
const text = TokeniseResult.text
@@ -453,7 +511,7 @@ const readOptionalGeneric = function (TokeniseResult, k) {
return null
}
const readOptionalDef = function (TokeniseResult, k) {
const readOptionalDef = function (TokeniseResult: TokeniseResult, k: number) {
// skip over the optional arguments of a definition
// \def\foo#1.#2(#3){this is the macro #1 #2 #3}
// start looking at text immediately after \def command
@@ -482,7 +540,7 @@ const readOptionalDef = function (TokeniseResult, k) {
return null
}
const readDefinition = function (TokeniseResult, k) {
const readDefinition = function (TokeniseResult: TokeniseResult, k: number) {
// read a definition as in
// \newcommand{\FOO}{DEFN}
// \newcommand{\FOO} {DEF} (optional whitespace)
@@ -532,7 +590,7 @@ const readDefinition = function (TokeniseResult, k) {
return null
}
const readVerb = function (TokeniseResult, k) {
const readVerb = function (TokeniseResult: TokeniseResult, k: number) {
// read a verbatim argument
// \verb@foo@
// \verb*@foo@
@@ -577,7 +635,11 @@ const readVerb = function (TokeniseResult, k) {
return null
}
const readUrl = function (TokeniseResult, k, options) {
const readUrl = function (
TokeniseResult: TokeniseResult,
k: number,
options?: LintOptions
) {
// read a url argument
// \url|https://example.com|
// \url{https://example.com}
@@ -642,7 +704,10 @@ const readUrl = function (TokeniseResult, k, options) {
return null
}
const InterpretTokens = function (TokeniseResult, ErrorReporter) {
const InterpretTokens = function (
TokeniseResult: TokeniseResult,
ErrorReporter: ErrorReporterInstance
) {
const Tokens = TokeniseResult.tokens
// var linePosition = TokeniseResult.linePosition
// var lineNumber = TokeniseResult.lineNumber
@@ -652,7 +717,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
const TokenError = ErrorReporter.TokenError
const Environments = new EnvHandler(TokeniseResult, ErrorReporter)
let nextGroupMathMode = null // if the next group should have
let nextGroupMathMode: null | false | undefined = null // if the next group should have
// math mode on(=true) or
// off(=false) (for \hbox), or
// unknown(=undefined) or inherit
@@ -774,14 +839,14 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
// We're looking at an invalid environment command, read as far as we can in the sequence
// "{" "CHAR" "CHAR" "CHAR" ... to report an error for as much of the command as we can,
// bail out when we hit a space/newline.
let endToken = null
let endToken: SpecialCharacterToken | TextToken | null = null
if (open && open[1] === '{') {
endToken = open // we've got a {
endToken = open as SpecialCharacterToken // we've got a {
if (delimiter && delimiter[1] === 'Text') {
endToken = delimiter.slice() // we've got some text following the {
endToken = (delimiter as TextToken).slice() as TextToken // we've got some text following the {
const start = endToken[2]
const end = endToken[3]
let j
let j: number
for (j = start; j < end; j++) {
const char = text[j]
if (
@@ -821,7 +886,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
// try to read any optional params [LABEL].... allowing for
// underscores, advance if found
let newPos = readOptionalArgumentWithUnderscores(TokeniseResult, i)
if (newPos instanceof Error) {
if (newPos instanceof TokeniserError) {
TokenErrorFromTo(
Tokens[i + 1],
Tokens[Math.min(newPos.pos, len - 1)],
@@ -1010,7 +1075,7 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
nextToken[5] === 'control-symbol'
) {
// control symbol
char = nextToken[4]
char = (nextToken as ControlSymbolToken)[4]
} else if (nextToken && nextToken[1] === '\\') {
char = 'unknown'
}
@@ -1244,11 +1309,46 @@ const InterpretTokens = function (TokeniseResult, ErrorReporter) {
return Environments
}
const DocumentTree = function (TokeniseResult) {
type Delimiter = {
mathMode?: Delimiter | null | false
token: Token
closeToken?: Token
name?: string
command?: string
verbatim?: boolean
}
type DocumentTreeNode = {
startDelimiter?: Delimiter
endDelimiter?: Delimiter
children: DocumentTreeNode[]
}
type DocumentTreeInstance = {
openEnv: (this: DocumentTreeInstance, startDelimiter: Delimiter) => void
closeEnv: (endDelimiter?: Delimiter) => Delimiter | null | undefined
getNthPreviousNode: (n: number) => DocumentTreeNode | null
getCurrentNode: (this: DocumentTreeInstance) => DocumentTreeNode
getCurrentDelimiter: (this: DocumentTreeInstance) => Delimiter | undefined
getPreviousDelimiter: (
this: DocumentTreeInstance
) => Delimiter | undefined | null
getDepth: () => number
getContexts: () => any[]
}
type DocumentTreeConstructor = {
new (TokeniseResult: TokeniseResult): DocumentTreeInstance
}
const DocumentTree: DocumentTreeConstructor = function (
this: DocumentTreeInstance,
TokeniseResult: TokeniseResult
) {
// Each environment and scope becomes and an entry in the tree, and can have
// child entries, e.g. an 'array' inside an 'equation' inside a 'document' environment.
// Entries can have multiple adjacent children.
const tree = {
const tree: DocumentTreeNode = {
children: [],
}
// The stack is just for easily moving up and down the tree. Popping off the stack
@@ -1263,46 +1363,46 @@ const DocumentTree = function (TokeniseResult) {
}
currentNode.children.push(newNode)
stack.push(newNode)
}
} satisfies DocumentTreeInstance['openEnv']
this.closeEnv = function (endDelimiter) {
if (stack.length === 1) {
// Can't close root element
return null
}
const currentNode = stack.pop()
const currentNode = stack.pop()!
currentNode.endDelimiter = endDelimiter
return currentNode.startDelimiter
}
} satisfies DocumentTreeInstance['closeEnv']
this.getNthPreviousNode = function (n) {
const offset = stack.length - n - 1
if (offset < 0) return null
return stack[offset]
}
} satisfies DocumentTreeInstance['getNthPreviousNode']
this.getCurrentNode = function () {
return this.getNthPreviousNode(0)
}
return this.getNthPreviousNode(0)!
} satisfies DocumentTreeInstance['getCurrentNode']
this.getCurrentDelimiter = function () {
return this.getCurrentNode().startDelimiter
}
} satisfies DocumentTreeInstance['getCurrentDelimiter']
this.getPreviousDelimiter = function () {
const node = this.getNthPreviousNode(1)
if (!node) return null
return node.startDelimiter
}
} satisfies DocumentTreeInstance['getPreviousDelimiter']
this.getDepth = function () {
return stack.length - 1 // Root node doesn't count
}
} satisfies DocumentTreeInstance['getDepth']
this.getContexts = function () {
const linePosition = TokeniseResult.linePosition
function tokenToRange(token) {
function tokenToRange(token: Token) {
const line = token[0]
const start = token[2]
let end = token[3]
@@ -1323,9 +1423,9 @@ const DocumentTree = function (TokeniseResult) {
}
}
function getContextsFromNode(node) {
function getContextsFromNode(node: DocumentTreeNode) {
if (node.startDelimiter && node.startDelimiter.mathMode) {
const context = {
const context: Context = {
type: 'math',
range: {
start: tokenToRange(node.startDelimiter.token).start,
@@ -1338,7 +1438,7 @@ const DocumentTree = function (TokeniseResult) {
}
return [context]
} else {
let contexts = []
let contexts: Context[] = []
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
contexts = contexts.concat(getContextsFromNode(child))
@@ -1348,10 +1448,51 @@ const DocumentTree = function (TokeniseResult) {
}
return getContextsFromNode(tree)
}
} satisfies DocumentTreeInstance['getContexts']
} as unknown as DocumentTreeConstructor
type Position = {
row: number
column: number
}
const EnvHandler = function (TokeniseResult, ErrorReporter) {
type Range = {
start: Position
end?: Position
}
type Context = {
type: 'math' | 'text'
range: Range
}
type EnvHandlerInstance = {
getDocument: () => DocumentTreeInstance
push: (this: EnvHandlerInstance, newDelimiter: Delimiter) => void
_endVerbatim: (this: EnvHandlerInstance, thisDelimiter: Delimiter) => void
_end: (this: EnvHandlerInstance, thisDelimiter: Delimiter) => void
_beginMathMode: (this: EnvHandlerInstance, thisDelimiter: Delimiter) => void
_toggleMathMode: (this: EnvHandlerInstance, thisDelimiter: Delimiter) => void
getMathMode: () => Delimiter | false | null | undefined
insideGroup: () => boolean | null
resetMathMode: () => void
setDelimiterProps: (this: EnvHandlerInstance, delimiter: Delimiter) => void
checkAndUpdateState: (this: EnvHandlerInstance, delimiter: Delimiter) => void
close: () => void
}
type EnvHandlerConstructor = {
new (
TokeniseResult: TokeniseResult,
ErrorReporter: ErrorReporterInstance
): EnvHandlerInstance
}
const EnvHandler: EnvHandlerConstructor = function (
this: EnvHandlerInstance,
TokeniseResult: TokeniseResult,
ErrorReporter: ErrorReporterInstance
) {
// Loop through the Environments array keeping track of the state,
// pushing and popping environments onto the state[] array for each
// \begin and \end command
@@ -1362,19 +1503,19 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
const delimiters = []
const document = new DocumentTree(TokeniseResult)
let documentClosed = null
let documentClosed: Delimiter | null = null
let inVerbatim = false
const verbatimRanges = []
const verbatimRanges: { start: number; end: number }[] = []
this.getDocument = function () {
return document
}
} satisfies EnvHandlerInstance['getDocument']
this.push = function (newDelimiter) {
this.setDelimiterProps(newDelimiter)
this.checkAndUpdateState(newDelimiter)
delimiters.push(newDelimiter)
}
} satisfies EnvHandlerInstance['push']
this._endVerbatim = function (thisDelimiter) {
const lastDelimiter = document.getCurrentDelimiter()
@@ -1388,9 +1529,9 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
end: thisDelimiter.token[2],
})
}
}
} satisfies EnvHandlerInstance['_endVerbatim']
const invalidEnvs = []
const invalidEnvs: Delimiter[] = []
this._end = function (thisDelimiter) {
// check if environment or group is closed correctly
@@ -1427,7 +1568,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
} else if (
invalidEnvs.length > 0 &&
(i = indexOfClosingEnvInArray(invalidEnvs, thisDelimiter) > -1)
(i = indexOfClosingEnvInArray(invalidEnvs, thisDelimiter)) > -1
) {
// got a match on an invalid env, so try to continue
invalidEnvs.splice(i, 1)
@@ -1468,9 +1609,9 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
}
} while (retry === true)
}
} satisfies EnvHandlerInstance['_end']
const CLOSING_DELIMITER = {
const CLOSING_DELIMITER: Record<string, string> = {
'{': '}',
left: 'right',
'[': ']',
@@ -1479,7 +1620,10 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
$$: '$$',
}
const closedBy = function (lastDelimiter, thisDelimiter) {
const closedBy = function (
lastDelimiter: Delimiter | undefined,
thisDelimiter: Delimiter
) {
if (!lastDelimiter) {
return false
} else if (thisDelimiter.command === 'end') {
@@ -1488,6 +1632,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
lastDelimiter.name === thisDelimiter.name
)
} else if (
lastDelimiter.command &&
thisDelimiter.command === CLOSING_DELIMITER[lastDelimiter.command]
) {
return true
@@ -1496,7 +1641,10 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
}
const indexOfClosingEnvInArray = function (delimiters, thisDelimiter) {
const indexOfClosingEnvInArray = function (
delimiters: Delimiter[],
thisDelimiter: Delimiter
): number {
for (let i = 0, n = delimiters.length; i < n; i++) {
if (closedBy(delimiters[i], thisDelimiter)) {
return i
@@ -1505,15 +1653,15 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
return -1
}
const delimiterPrecedence = function (delimiter) {
const openScore = {
const delimiterPrecedence = function (delimiter: Delimiter) {
const openScore: Record<string, number> = {
'{': 1,
left: 2,
$: 3,
$$: 4,
begin: 4,
}
const closeScore = {
const closeScore: Record<string, number> = {
'}': 1,
right: 2,
$: 3,
@@ -1527,8 +1675,8 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
}
const getName = function (delimiter) {
const description = {
const getName = function (delimiter?: Delimiter | null) {
const description: Record<string, string> = {
'{': 'open group {',
'}': 'close group }',
'[': 'open display math \\[',
@@ -1540,12 +1688,12 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
left: '\\left',
right: '\\right',
}
if (delimiter.command === 'begin' || delimiter.command === 'end') {
if (delimiter?.command === 'begin' || delimiter?.command === 'end') {
return '\\' + delimiter.command + '{' + delimiter.name + '}'
} else if (delimiter.command in description) {
} else if (delimiter?.command && delimiter.command in description) {
return description[delimiter.command]
} else {
return delimiter.command
return delimiter?.command
}
}
@@ -1553,7 +1701,10 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
const UNCLOSED_GROUP = 2
const UNCLOSED_ENV = 3
const reportError = function (lastDelimiter, thisDelimiter) {
const reportError = function (
lastDelimiter: Delimiter,
thisDelimiter: Delimiter
) {
if (!lastDelimiter) {
// unexpected close, nothing was open!
if (documentClosed) {
@@ -1620,7 +1771,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
thisDelimiter.mathMode = thisDelimiter
document.openEnv(thisDelimiter)
}
} satisfies EnvHandlerInstance['_beginMathMode']
this._toggleMathMode = function (thisDelimiter) {
// math environments use the same for begin and end.
@@ -1638,7 +1789,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
document.openEnv(thisDelimiter)
}
}
}
} satisfies EnvHandlerInstance['_toggleMathMode']
this.getMathMode = function () {
// return the current mathmode.
@@ -1649,7 +1800,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
} else {
return null
}
}
} satisfies EnvHandlerInstance['getMathMode']
this.insideGroup = function () {
const currentDelimiter = document.getCurrentDelimiter()
@@ -1658,7 +1809,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
} else {
return null
}
}
} satisfies EnvHandlerInstance['insideGroup']
const resetMathMode = function () {
// Wind back the current environment stack removing everything
@@ -1673,11 +1824,13 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
} else {
// return
}
}
} satisfies EnvHandlerInstance['resetMathMode']
this.resetMathMode = resetMathMode
const getNewMathMode = function (currentMathMode, thisDelimiter) {
const getNewMathMode = function (
currentMathMode: Delimiter | null | false | undefined,
thisDelimiter: Delimiter
) {
// look at math mode and transitions
//
// We have several cases
@@ -1794,18 +1947,18 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
) {
this._toggleMathMode(thisDelimiter)
}
}
} satisfies EnvHandlerInstance['checkAndUpdateState']
this.close = function () {
// If there is anything left in the state at this point, there
// were unclosed environments or groups.
while (document.getDepth() > 0) {
const thisDelimiter = document.closeEnv()
if (thisDelimiter.command === '{') {
if (thisDelimiter?.command === '{') {
// Note that having an unclosed group does not stop
// compilation in TeX but we will highlight it as an error
ErrorFrom(thisDelimiter, 'unclosed group {', { type: 'warning' })
} else {
} else if (thisDelimiter) {
ErrorFrom(thisDelimiter, 'unclosed ' + getName(thisDelimiter))
}
}
@@ -1829,7 +1982,7 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
}
}
}
}
} satisfies EnvHandlerInstance['close']
this.setDelimiterProps = function (delimiter) {
const name = delimiter.name
@@ -1842,22 +1995,74 @@ const EnvHandler = function (TokeniseResult, ErrorReporter) {
) {
delimiter.verbatim = true
}
}
} satisfies EnvHandlerInstance['setDelimiterProps']
} as unknown as EnvHandlerConstructor
type TokenError = LintError & {
row: number
column: number
start_row: number
start_col: number
end_row: number
end_col: number
mathMode?: boolean
ignore?: boolean
}
type ErrorType = 'error' | 'warning' | 'info'
type ErrorOptions = {
errorAtStart?: boolean
type?: ErrorType
suppressIfEditing?: boolean
mathMode?: boolean
}
type ErrorReporterInstance = {
errors: TokenError[]
tokenErrors: TokenError[]
filterMath: boolean
getErrors(this: ErrorReporterInstance): TokenError[]
TokenError(token: Token, message: string, options?: ErrorOptions): void
TokenErrorFromTo(
fromToken: Token,
toToken: Token,
message: string,
options?: ErrorOptions
): void
EnvErrorFromTo(
fromEnv: Delimiter,
toEnv: Delimiter,
message: string,
options?: ErrorOptions
): void
EnvErrorTo(toEnv: Delimiter, message: string, options?: ErrorOptions): void
EnvErrorFrom(
delimiter: Delimiter,
message: string,
options?: ErrorOptions
): void
}
interface ErrorReporterConstructor {
new (TokeniseResult: TokeniseResult): ErrorReporterInstance
}
// Error reporting functions for tokens and environments
const ErrorReporter = function (TokeniseResult) {
const ErrorReporter: ErrorReporterConstructor = function (
this: ErrorReporterInstance,
TokeniseResult: TokeniseResult
) {
// const text = TokeniseResult.text
const linePosition = TokeniseResult.linePosition
const lineNumber = TokeniseResult.lineNumber
const errors = []
const tokenErrors = []
const errors: TokenError[] = []
const tokenErrors: TokenError[] = []
this.errors = errors
this.tokenErrors = tokenErrors
this.filterMath = false
function pos(row, column) {
function pos(row: number, column: number) {
return linePosition[row] + column
}
@@ -1896,7 +2101,7 @@ const ErrorReporter = function (TokeniseResult) {
} else {
return allErrors
}
}
} satisfies ErrorReporterInstance['getErrors']
// Report an error in a single token
@@ -1928,8 +2133,7 @@ const ErrorReporter = function (TokeniseResult) {
suppressIfEditing: options.suppressIfEditing,
mathMode: options.mathMode,
})
}
} satisfies ErrorReporterInstance['TokenError']
// Report an error over a range (from, to)
this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
@@ -1963,7 +2167,7 @@ const ErrorReporter = function (TokeniseResult) {
suppressIfEditing: options.suppressIfEditing,
mathMode: options.mathMode,
})
}
} satisfies ErrorReporterInstance['TokenErrorFromTo']
this.EnvErrorFromTo = function (fromEnv, toEnv, message, options) {
if (!options) {
@@ -2002,7 +2206,7 @@ const ErrorReporter = function (TokeniseResult) {
suppressIfEditing: options.suppressIfEditing,
mathMode: options.mathMode,
})
}
} satisfies ErrorReporterInstance['EnvErrorFromTo']
// Report an error up to a given environment (from the beginning of the document)
@@ -2034,7 +2238,7 @@ const ErrorReporter = function (TokeniseResult) {
mathMode: options.mathMode,
}
errors.push(err)
}
} satisfies ErrorReporterInstance['EnvErrorTo']
// Report an error from a given environment (up to the end of the document)
@@ -2064,11 +2268,18 @@ const ErrorReporter = function (TokeniseResult) {
text: message,
mathMode: options.mathMode,
})
}
}
} satisfies ErrorReporterInstance['EnvErrorFrom']
} as unknown as ErrorReporterConstructor
const Parse = function (text) {
type ParseResult = {
errors: TokenError[]
contexts: Context[]
}
const Parse = function (text: string): ParseResult {
const TokeniseResult = Tokenise(text)
if (!isValidTokeniseResult(TokeniseResult)) {
return { errors: [], contexts: [] }
}
const Reporter = new ErrorReporter(TokeniseResult)
const Environments = InterpretTokens(TokeniseResult, Reporter)
Environments.close()
@@ -2078,20 +2289,26 @@ const Parse = function (text) {
}
}
let latestLintResult = null
function isValidTokeniseResult(
result: InvalidTokeniseResult | TokeniseResult
): result is TokeniseResult {
return (result as TokeniseResult).tokens !== undefined
}
let latestLintResult: { text: string; workerResult: ParseResult } | null = null
// Define an onmessage handler if this file is loaded in a Worker context
if (typeof onmessage !== 'undefined') {
onmessage = function (event) {
if (event.data && event.type === 'message') {
let workerResult = {}
let workerResult: Record<never, never> | ParseResult = {}
const text = event.data.text
if (latestLintResult && latestLintResult.text === text) {
workerResult = latestLintResult.workerResult
} else {
try {
workerResult = Parse(event.data.text)
latestLintResult = { text, workerResult }
latestLintResult = { text, workerResult: workerResult as ParseResult }
} catch (err) {
console.error('error in linting', err)
workerResult = { errors: [], contexts: [] }
@@ -2105,10 +2322,10 @@ if (typeof onmessage !== 'undefined') {
export default class LintWorker {
// unused vars kept to document the interface
// eslint-disable-next-line @typescript-eslint/no-unused-vars
postMessage(message) {}
postMessage(message: string) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addEventListener(eventName, listener) {}
Parse(text) {
addEventListener(eventName: string, listener: () => void) {}
Parse(text: string) {
return Parse(text)
}
}

View File

@@ -1,5 +1,5 @@
import { assert } from 'chai'
import LintWorker from '../../../../../../frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js'
import LintWorker from '../../../../../../frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker'
import { errorsToDiagnostics } from '../../../../../../frontend/js/features/source-editor/languages/latex/linter/errors-to-diagnostics'
import { Diagnostic } from '@codemirror/lint'
import { mergeCompatibleOverlappingDiagnostics } from '../../../../../../frontend/js/features/source-editor/languages/latex/linter/merge-overlapping-diagnostics'
@@ -587,8 +587,8 @@ describe('LatexLinter', function () {
assert.equal(contexts[0].type, 'math')
assert.equal(contexts[0].range.start.row, 1)
assert.equal(contexts[0].range.start.column, 0)
assert.equal(contexts[0].range.end.row, 5)
assert.equal(contexts[0].range.end.column, 2)
assert.equal(contexts[0].range.end?.row, 5)
assert.equal(contexts[0].range.end?.column, 2)
})
it('should remove error when cursor is inside incomplete command', function () {