mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #27819 from overleaf/mj-human-readable-logs-typescript
[web] Convert logs parsing to typescript GitOrigin-RevId: 7a338740db50c8a3a0b70dd2212083f17348d4f1
This commit is contained in:
committed by
Copybot
parent
e166b09d46
commit
114a2af181
@@ -1,14 +1,17 @@
|
||||
import LatexLogParser from '../log-parser/latex-log-parser'
|
||||
import LatexLogParser, {
|
||||
LatexParserOptions,
|
||||
ParseResult,
|
||||
} from '../log-parser/latex-log-parser'
|
||||
import ruleset from './HumanReadableLogsRules'
|
||||
|
||||
export default {
|
||||
parse(rawLog, options) {
|
||||
parse(rawLog: string | ParseResult, options: LatexParserOptions) {
|
||||
const parsedLogEntries =
|
||||
typeof rawLog === 'string'
|
||||
? new LatexLogParser(rawLog, options).parse()
|
||||
: rawLog
|
||||
|
||||
const seenErrorTypes = {} // keep track of types of errors seen
|
||||
const seenErrorTypes: Record<string, boolean> = {} // keep track of types of errors seen
|
||||
|
||||
for (const entry of parsedLogEntries.all) {
|
||||
const ruleDetails = ruleset.find(rule =>
|
||||
@@ -28,9 +31,11 @@ export default {
|
||||
}
|
||||
|
||||
if (ruleDetails.contentRegex) {
|
||||
const match = entry.content.match(ruleDetails.contentRegex)
|
||||
if (match) {
|
||||
entry.contentDetails = match.slice(1)
|
||||
if (entry.content != null) {
|
||||
const match = entry.content.match(ruleDetails.contentRegex)
|
||||
if (match) {
|
||||
entry.contentDetails = match.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,14 +79,14 @@ export default {
|
||||
}
|
||||
|
||||
// filter out the suppressed errors (from the array entries in parsedLogEntries)
|
||||
for (const [key, errors] of Object.entries(parsedLogEntries)) {
|
||||
if (typeof errors === 'object' && errors.length > 0) {
|
||||
parsedLogEntries[key] = Array.from(errors).filter(
|
||||
for (const type of ['errors', 'warnings', 'typesetting'] as const) {
|
||||
const errors = parsedLogEntries[type]
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
parsedLogEntries[type] = Array.from(errors).filter(
|
||||
err => !err.suppressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLogEntries
|
||||
},
|
||||
}
|
||||
@@ -172,26 +172,28 @@ const hints: { [ruleId: string]: LogHint } = {
|
||||
},
|
||||
hint_undefined_control_sequence: {
|
||||
formattedContent: details => {
|
||||
if (details?.length && packageSuggestionsForCommands.has(details[0])) {
|
||||
if (details?.length) {
|
||||
const command = details[0]
|
||||
const suggestion = packageSuggestionsForCommands.get(command)
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The <code>{command}</code>{' '}
|
||||
command won't work unless you include
|
||||
<code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
if (suggestion) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The{' '}
|
||||
<code>{command}</code> command won't work unless you include
|
||||
<code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -215,29 +217,28 @@ const hints: { [ruleId: string]: LogHint } = {
|
||||
},
|
||||
hint_undefined_environment: {
|
||||
formattedContent: details => {
|
||||
if (
|
||||
details?.length &&
|
||||
packageSuggestionsForEnvironments.has(details[0])
|
||||
) {
|
||||
if (details?.length) {
|
||||
const environment = details[0]
|
||||
const suggestion = packageSuggestionsForEnvironments.get(environment)
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The{' '}
|
||||
<code>{environment}</code> environment won't work unless you
|
||||
include <code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
if (suggestion) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
We think you’ve got a missing package! The{' '}
|
||||
<code>{environment}</code> environment won't work unless you
|
||||
include <code>{suggestion.command}</code> in your{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
|
||||
document preamble
|
||||
</WikiLink>
|
||||
.{' '}
|
||||
<WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
|
||||
Learn more about packages
|
||||
</WikiLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const commandSuggestions = [
|
||||
type Suggestion = [string, { name: string; command: string }]
|
||||
|
||||
const commandSuggestions: Suggestion[] = [
|
||||
[
|
||||
'\\includegraphics',
|
||||
{ name: 'graphicx', command: '\\usepackage{graphicx}' },
|
||||
@@ -30,7 +32,7 @@ const commandSuggestions = [
|
||||
['\\arraybackslash', { name: 'array', command: '\\usepackage{array}' }],
|
||||
]
|
||||
|
||||
const environmentSuggestions = [
|
||||
const environmentSuggestions: Suggestion[] = [
|
||||
['justify', { name: 'ragged2e', command: '\\usepackage{ragged2e}' }],
|
||||
['align', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
['align*', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
|
||||
@@ -13,7 +13,7 @@ interface Rule {
|
||||
contentRegex?: RegExp
|
||||
improvedTitle?: (
|
||||
currentTitle: string,
|
||||
details?: [string]
|
||||
details?: string[]
|
||||
) => string | [string, JSX.Element]
|
||||
package?: string
|
||||
highlightCommand?: (contentDetails: string[]) => string | undefined
|
||||
@@ -65,10 +65,13 @@ const rules: Rule[] = [
|
||||
regexToMatch: /Undefined control sequence/,
|
||||
// Match the last control sequence in the line
|
||||
contentRegex: /^[^\n]*(\\\S+)\s*[\n]/,
|
||||
improvedTitle: (currentTitle: string, details?: [string]) => {
|
||||
if (details?.length && packageSuggestionsForCommands.has(details[0])) {
|
||||
const command = details[0]
|
||||
const suggestion = packageSuggestionsForCommands.get(command)
|
||||
improvedTitle: (currentTitle: string, details?: string[]) => {
|
||||
if (!details?.length) {
|
||||
return currentTitle
|
||||
}
|
||||
const command = details[0]
|
||||
const suggestion = packageSuggestionsForCommands.get(command)
|
||||
if (suggestion) {
|
||||
return [
|
||||
`Is ${suggestion.command} missing?`,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
@@ -87,13 +90,13 @@ const rules: Rule[] = [
|
||||
ruleId: 'hint_undefined_environment',
|
||||
regexToMatch: /LaTeX Error: Environment .+ undefined/,
|
||||
contentRegex: /\\begin\{(\S+)\}/,
|
||||
improvedTitle: (currentTitle: string, details?: [string]) => {
|
||||
if (
|
||||
details?.length &&
|
||||
packageSuggestionsForEnvironments.has(details[0])
|
||||
) {
|
||||
const environment = details[0]
|
||||
const suggestion = packageSuggestionsForEnvironments.get(environment)
|
||||
improvedTitle: (currentTitle: string, details?: string[]) => {
|
||||
if (!details?.length) {
|
||||
return currentTitle
|
||||
}
|
||||
const environment = details[0]
|
||||
const suggestion = packageSuggestionsForEnvironments.get(environment)
|
||||
if (suggestion) {
|
||||
return [
|
||||
`Is ${suggestion.command} missing?`,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
|
||||
@@ -18,12 +18,21 @@ const MESSAGE_LEVELS = {
|
||||
ERROR: 'error',
|
||||
}
|
||||
|
||||
const parserReducer = function (maxErrors) {
|
||||
return function (accumulator, parser) {
|
||||
const consume = function (logText, regex, process) {
|
||||
type Parser<T> = [RegExp, (match: RegExpExecArray) => T]
|
||||
|
||||
const parserReducer = function (maxErrors: number | null) {
|
||||
return function <T>(
|
||||
accumulator: [T[], string],
|
||||
parser: Parser<T>
|
||||
): [T[], string] {
|
||||
const consume = function (
|
||||
logText: string,
|
||||
regex: RegExp,
|
||||
process: (match: RegExpExecArray) => T
|
||||
): [T[], string] {
|
||||
let match
|
||||
let text = logText
|
||||
const result = []
|
||||
const result: T[] = []
|
||||
let iterationCount = 0
|
||||
|
||||
while ((match = regex.exec(text))) {
|
||||
@@ -55,8 +64,22 @@ const parserReducer = function (maxErrors) {
|
||||
}
|
||||
}
|
||||
|
||||
type BibLogEntry = {
|
||||
file: string
|
||||
level: string
|
||||
message: string
|
||||
line: string
|
||||
raw: string
|
||||
}
|
||||
|
||||
export default class BibLogParser {
|
||||
constructor(text, options = {}) {
|
||||
text: string
|
||||
options: Record<string, any>
|
||||
lines: string[]
|
||||
warningParsers: Parser<BibLogEntry>[]
|
||||
errorParsers: Parser<BibLogEntry>[]
|
||||
|
||||
constructor(text: string, options = {}) {
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error('BibLogParser Error: text parameter must be a string')
|
||||
}
|
||||
@@ -156,11 +179,11 @@ export default class BibLogParser {
|
||||
// reduce over the parsers, starting with the log text,
|
||||
const [allWarnings, remainingText] = this.warningParsers.reduce(
|
||||
parserReducer(this.options.maxErrors),
|
||||
[[], this.text]
|
||||
[[] as BibLogEntry[], this.text]
|
||||
)
|
||||
const [allErrors] = this.errorParsers.reduce(
|
||||
parserReducer(this.options.maxErrors),
|
||||
[[], remainingText]
|
||||
[[] as BibLogEntry[], remainingText]
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -173,7 +196,10 @@ export default class BibLogParser {
|
||||
}
|
||||
|
||||
parseBiber() {
|
||||
const result = {
|
||||
const result: Record<
|
||||
'all' | 'errors' | 'warnings' | 'files' | 'typesetting',
|
||||
BibLogEntry[]
|
||||
> = {
|
||||
all: [],
|
||||
errors: [],
|
||||
warnings: [],
|
||||
@@ -186,7 +212,9 @@ export default class BibLogParser {
|
||||
const [fullLine, , messageType, message] = match
|
||||
const newEntry = {
|
||||
file: '',
|
||||
level: MESSAGE_LEVELS[messageType] || 'INFO',
|
||||
level:
|
||||
MESSAGE_LEVELS[messageType as keyof typeof MESSAGE_LEVELS] ||
|
||||
'INFO',
|
||||
message,
|
||||
line: '',
|
||||
raw: fullLine,
|
||||
@@ -14,8 +14,54 @@ const STATE = {
|
||||
ERROR: 1,
|
||||
}
|
||||
|
||||
export type LatexParserOptions = {
|
||||
fileBaseNames?: RegExp[]
|
||||
ignoreDuplicates?: boolean
|
||||
}
|
||||
|
||||
type LatexLogEntry = {
|
||||
line: string | number | null
|
||||
file: string | undefined
|
||||
level: 'error' | 'warning' | 'typesetting'
|
||||
message: string
|
||||
content?: string
|
||||
raw: string
|
||||
ruleId?: string
|
||||
contentDetails?: string[]
|
||||
command?: string
|
||||
suppressed?: boolean
|
||||
}
|
||||
|
||||
type File = {
|
||||
path: string
|
||||
files: File[]
|
||||
}
|
||||
|
||||
export type ParseResult = {
|
||||
all: LatexLogEntry[]
|
||||
errors: LatexLogEntry[]
|
||||
warnings: LatexLogEntry[]
|
||||
typesetting: LatexLogEntry[]
|
||||
files: File[]
|
||||
}
|
||||
export default class LatexParser {
|
||||
constructor(text, options = {}) {
|
||||
state: number
|
||||
fileBaseNames: RegExp[]
|
||||
ignoreDuplicates?: boolean
|
||||
data: LatexLogEntry[]
|
||||
fileStack: File[]
|
||||
rootFileList: File[]
|
||||
currentFileList: File[]
|
||||
openParens: number
|
||||
latexWarningRegex: RegExp
|
||||
packageWarningRegex: RegExp
|
||||
packageRegex: RegExp
|
||||
log: LogText
|
||||
currentLine: string
|
||||
currentFilePath: string | undefined
|
||||
currentError: LatexLogEntry | undefined
|
||||
|
||||
constructor(text: string, options: LatexParserOptions = {}) {
|
||||
this.state = STATE.NORMAL
|
||||
this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
|
||||
this.ignoreDuplicates = options.ignoreDuplicates
|
||||
@@ -26,11 +72,14 @@ export default class LatexParser {
|
||||
this.latexWarningRegex = LATEX_WARNING_REGEX
|
||||
this.packageWarningRegex = PACKAGE_WARNING_REGEX
|
||||
this.packageRegex = PACKAGE_REGEX
|
||||
this.currentLine = ''
|
||||
this.log = new LogText(text)
|
||||
}
|
||||
|
||||
parse() {
|
||||
while ((this.currentLine = this.log.nextLine()) !== false) {
|
||||
parse(): ParseResult {
|
||||
let nextLine: string | false
|
||||
while ((nextLine = this.log.nextLine()) !== false) {
|
||||
this.currentLine = nextLine
|
||||
if (this.state === STATE.NORMAL) {
|
||||
if (this.currentLineIsError()) {
|
||||
this.state = STATE.ERROR
|
||||
@@ -58,6 +107,9 @@ export default class LatexParser {
|
||||
}
|
||||
}
|
||||
if (this.state === STATE.ERROR) {
|
||||
if (!this.currentError) {
|
||||
throw new Error('LatexParser Error: currentError is undefined')
|
||||
}
|
||||
this.currentError.content += this.log
|
||||
.linesUpToNextMatchingLine(/^l\.[0-9]+/)
|
||||
.join('\n')
|
||||
@@ -111,6 +163,9 @@ export default class LatexParser {
|
||||
|
||||
parseFileLineError() {
|
||||
const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
|
||||
if (!result) {
|
||||
throw new Error('LatexParser Error: Unable to extract error from line.')
|
||||
}
|
||||
this.currentError = {
|
||||
line: result[2],
|
||||
file: result[1],
|
||||
@@ -145,7 +200,7 @@ export default class LatexParser {
|
||||
return this.data.push(this.currentError)
|
||||
}
|
||||
|
||||
parseSingleWarningLine(prefixRegex) {
|
||||
parseSingleWarningLine(prefixRegex: RegExp) {
|
||||
const warningMatch = this.currentLine.match(prefixRegex)
|
||||
if (!warningMatch) {
|
||||
return
|
||||
@@ -173,6 +228,11 @@ export default class LatexParser {
|
||||
let lineMatch = this.currentLine.match(LINES_REGEX)
|
||||
let line = lineMatch ? parseInt(lineMatch[1], 10) : null
|
||||
const packageMatch = this.currentLine.match(this.packageRegex)
|
||||
if (!packageMatch) {
|
||||
throw new Error(
|
||||
'LatexParser Error: Unable to extract package name from warning.'
|
||||
)
|
||||
}
|
||||
const packageName = packageMatch[1]
|
||||
// Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
|
||||
const prefixRegex = new RegExp(
|
||||
@@ -180,10 +240,15 @@ export default class LatexParser {
|
||||
'i'
|
||||
)
|
||||
// After every warning message there's a blank line, let's use it
|
||||
while ((this.currentLine = this.log.nextLine())) {
|
||||
let currentLine: string | false
|
||||
while ((currentLine = this.log.nextLine())) {
|
||||
this.currentLine = currentLine
|
||||
lineMatch = this.currentLine.match(LINES_REGEX)
|
||||
line = lineMatch ? parseInt(lineMatch[1], 10) : line
|
||||
warningMatch = this.currentLine.match(prefixRegex)
|
||||
if (!warningMatch) {
|
||||
throw new Error('LatexParser Error: Unable to extract warning message.')
|
||||
}
|
||||
warningLines.push(warningMatch[1])
|
||||
}
|
||||
const rawMessage = warningLines.join(' ')
|
||||
@@ -291,16 +356,19 @@ export default class LatexParser {
|
||||
return path
|
||||
}
|
||||
|
||||
postProcess(data) {
|
||||
const all = []
|
||||
const errorsByLevel = {
|
||||
postProcess(data: LatexLogEntry[]) {
|
||||
const all: LatexLogEntry[] = []
|
||||
const errorsByLevel: Record<
|
||||
'error' | 'warning' | 'typesetting',
|
||||
LatexLogEntry[]
|
||||
> = {
|
||||
error: [],
|
||||
warning: [],
|
||||
typesetting: [],
|
||||
}
|
||||
const hashes = new Set()
|
||||
|
||||
const hashEntry = entry => entry.raw
|
||||
const hashEntry = (entry: LatexLogEntry) => entry.raw
|
||||
|
||||
data.forEach(item => {
|
||||
const hash = hashEntry(item)
|
||||
@@ -326,7 +394,11 @@ export default class LatexParser {
|
||||
}
|
||||
|
||||
class LogText {
|
||||
constructor(text) {
|
||||
text: string
|
||||
lines: string[]
|
||||
row: number
|
||||
|
||||
constructor(text: string) {
|
||||
this.text = text.replace(/(\r\n)|\r/g, '\n')
|
||||
// Join any lines which look like they have wrapped.
|
||||
const wrappedLines = this.text.split('\n')
|
||||
@@ -355,7 +427,7 @@ class LogText {
|
||||
this.row = 0
|
||||
}
|
||||
|
||||
nextLine() {
|
||||
nextLine(): string | false {
|
||||
this.row++
|
||||
if (this.row >= this.lines.length) {
|
||||
return false
|
||||
@@ -368,11 +440,11 @@ class LogText {
|
||||
this.row--
|
||||
}
|
||||
|
||||
linesUpToNextWhitespaceLine(stopAtError) {
|
||||
linesUpToNextWhitespaceLine(stopAtError: boolean = false) {
|
||||
return this.linesUpToNextMatchingLine(/^ *$/, stopAtError)
|
||||
}
|
||||
|
||||
linesUpToNextMatchingLine(match, stopAtError) {
|
||||
linesUpToNextMatchingLine(match: RegExp, stopAtError: boolean = false) {
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
Reference in New Issue
Block a user