diff --git a/services/web/frontend/js/features/source-editor/languages/latex/linter/errors-to-diagnostics.ts b/services/web/frontend/js/features/source-editor/languages/latex/linter/errors-to-diagnostics.ts index c2e9646bf5..2de14074f3 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/linter/errors-to-diagnostics.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/linter/errors-to-diagnostics.ts @@ -6,7 +6,7 @@ export type LintError = { startPos: number endPos: number pos: number - suppressIfEditing: boolean + suppressIfEditing?: boolean type: 'info' | 'warning' | 'error' text: string } diff --git a/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.ts b/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.ts index 2cdfb387b5..d093360afc 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.ts @@ -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' } ) 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.ts similarity index 85% rename from services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.js rename to services/web/frontend/js/features/source-editor/languages/latex/linter/latex-linter.worker.ts index f1d86e369f..bb8589406b 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.ts @@ -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 = { '{': '}', 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 = { '{': 1, left: 2, $: 3, $$: 4, begin: 4, } - const closeScore = { + const closeScore: Record = { '}': 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 = { '{': '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 | 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) } } 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 c3f2db2383..ea4c7e8e15 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 @@ -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 () {