| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- // Define some constants
- const LOG_WRAP_LIMIT = 79
- const LATEX_WARNING_REGEX = /^LaTeX Warning: (.*)$/
- const HBOX_WARNING_REGEX = /^(Over|Under)full \\(v|h)box/
- const PACKAGE_WARNING_REGEX = /^(Package \b.+\b Warning:.*)$/
- // This is used to parse the line number from common latex warnings
- const LINES_REGEX = /lines? ([0-9]+)/
- // This is used to parse the package name from the package warnings
- const PACKAGE_REGEX = /^Package (\b.+\b) Warning/
- const FILE_LINE_ERROR_REGEX = /^([./].*):(\d+): (.*)/
- const STATE = {
- NORMAL: 0,
- ERROR: 1,
- }
- export default class LatexParser {
- constructor(text, options) {
- this.state = STATE.NORMAL
- options = options || {}
- this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
- this.ignoreDuplicates = options.ignoreDuplicates
- this.data = []
- this.fileStack = []
- this.currentFileList = this.rootFileList = []
- this.openParens = 0
- this.log = new LogText(text)
- }
- parse() {
- while ((this.currentLine = this.log.nextLine()) !== false) {
- if (this.state === STATE.NORMAL) {
- if (this.currentLineIsError()) {
- this.state = STATE.ERROR
- this.currentError = {
- line: null,
- file: this.currentFilePath,
- level: 'error',
- message: this.currentLine.slice(2),
- content: '',
- raw: this.currentLine + '\n',
- }
- } else if (this.currentLineIsFileLineError()) {
- this.state = STATE.ERROR
- this.parseFileLineError()
- } else if (this.currentLineIsRunawayArgument()) {
- this.parseRunawayArgumentError()
- } else if (this.currentLineIsWarning()) {
- this.parseSingleWarningLine(LATEX_WARNING_REGEX)
- } else if (this.currentLineIsHboxWarning()) {
- this.parseHboxLine()
- } else if (this.currentLineIsPackageWarning()) {
- this.parseMultipleWarningLine()
- } else {
- this.parseParensForFilenames()
- }
- }
- if (this.state === STATE.ERROR) {
- this.currentError.content += this.log
- .linesUpToNextMatchingLine(/^l\.[0-9]+/)
- .join('\n')
- this.currentError.content += '\n'
- this.currentError.content += this.log
- .linesUpToNextWhitespaceLine()
- .join('\n')
- this.currentError.content += '\n'
- this.currentError.content += this.log
- .linesUpToNextWhitespaceLine()
- .join('\n')
- this.currentError.raw += this.currentError.content
- const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
- if (lineNo && this.currentError.line === null) {
- this.currentError.line = parseInt(lineNo[1], 10)
- }
- this.data.push(this.currentError)
- this.state = STATE.NORMAL
- }
- }
- return this.postProcess(this.data)
- }
- currentLineIsError() {
- return this.currentLine[0] === '!'
- }
- currentLineIsFileLineError() {
- return FILE_LINE_ERROR_REGEX.test(this.currentLine)
- }
- currentLineIsRunawayArgument() {
- return this.currentLine.match(/^Runaway argument/)
- }
- currentLineIsWarning() {
- return !!this.currentLine.match(LATEX_WARNING_REGEX)
- }
- currentLineIsPackageWarning() {
- return !!this.currentLine.match(PACKAGE_WARNING_REGEX)
- }
- currentLineIsHboxWarning() {
- return !!this.currentLine.match(HBOX_WARNING_REGEX)
- }
- parseFileLineError() {
- const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
- this.currentError = {
- line: result[2],
- file: result[1],
- level: 'error',
- message: result[3],
- content: '',
- raw: this.currentLine + '\n',
- }
- }
- parseRunawayArgumentError() {
- this.currentError = {
- line: null,
- file: this.currentFilePath,
- level: 'error',
- message: this.currentLine,
- content: '',
- raw: this.currentLine + '\n',
- }
- this.currentError.content += this.log
- .linesUpToNextWhitespaceLine()
- .join('\n')
- this.currentError.content += '\n'
- this.currentError.content += this.log
- .linesUpToNextWhitespaceLine()
- .join('\n')
- this.currentError.raw += this.currentError.content
- const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
- if (lineNo) {
- this.currentError.line = parseInt(lineNo[1], 10)
- }
- return this.data.push(this.currentError)
- }
- parseSingleWarningLine(prefixRegex) {
- const warningMatch = this.currentLine.match(prefixRegex)
- if (!warningMatch) {
- return
- }
- const warning = warningMatch[1]
- const lineMatch = warning.match(LINES_REGEX)
- const line = lineMatch ? parseInt(lineMatch[1], 10) : null
- this.data.push({
- line,
- file: this.currentFilePath,
- level: 'warning',
- message: warning,
- raw: warning,
- })
- }
- parseMultipleWarningLine() {
- // Some package warnings are multiple lines, let's parse the first line
- let warningMatch = this.currentLine.match(PACKAGE_WARNING_REGEX)
- if (!warningMatch) {
- return
- }
- // Something strange happened, return early
- const warningLines = [warningMatch[1]]
- let lineMatch = this.currentLine.match(LINES_REGEX)
- let line = lineMatch ? parseInt(lineMatch[1], 10) : null
- const packageMatch = this.currentLine.match(PACKAGE_REGEX)
- const packageName = packageMatch[1]
- // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
- const prefixRegex = new RegExp(
- '(?:\\(' + packageName + '\\))*[\\s]*(.*)',
- 'i'
- )
- // After every warning message there's a blank line, let's use it
- while ((this.currentLine = this.log.nextLine())) {
- lineMatch = this.currentLine.match(LINES_REGEX)
- line = lineMatch ? parseInt(lineMatch[1], 10) : line
- warningMatch = this.currentLine.match(prefixRegex)
- warningLines.push(warningMatch[1])
- }
- const rawMessage = warningLines.join(' ')
- this.data.push({
- line,
- file: this.currentFilePath,
- level: 'warning',
- message: rawMessage,
- raw: rawMessage,
- })
- }
- parseHboxLine() {
- const lineMatch = this.currentLine.match(LINES_REGEX)
- const line = lineMatch ? parseInt(lineMatch[1], 10) : null
- this.data.push({
- line,
- file: this.currentFilePath,
- level: 'typesetting',
- message: this.currentLine,
- raw: this.currentLine,
- })
- }
- // Check if we're entering or leaving a new file in this line
- parseParensForFilenames() {
- const pos = this.currentLine.search(/\(|\)/)
- if (pos !== -1) {
- const token = this.currentLine[pos]
- this.currentLine = this.currentLine.slice(pos + 1)
- if (token === '(') {
- const filePath = this.consumeFilePath()
- if (filePath) {
- this.currentFilePath = filePath
- const newFile = {
- path: filePath,
- files: [],
- }
- this.fileStack.push(newFile)
- this.currentFileList.push(newFile)
- this.currentFileList = newFile.files
- } else {
- this.openParens++
- }
- } else if (token === ')') {
- if (this.openParens > 0) {
- this.openParens--
- } else {
- if (this.fileStack.length > 1) {
- this.fileStack.pop()
- const previousFile = this.fileStack[this.fileStack.length - 1]
- this.currentFilePath = previousFile.path
- this.currentFileList = previousFile.files
- }
- }
- }
- // else {
- // Something has gone wrong but all we can do now is ignore it :(
- // }
- // Process the rest of the line
- this.parseParensForFilenames()
- }
- }
- consumeFilePath() {
- // Our heuristic for detecting file names are rather crude
- // A file may not contain a ')' in it
- // To be a file path it must have at least one /
- if (!this.currentLine.match(/^\/?([^ )]+\/)+/)) {
- return false
- }
- let endOfFilePath = this.currentLine.search(/ |\)/)
- // handle the case where there is a space in a filename
- while (endOfFilePath !== -1 && this.currentLine[endOfFilePath] === ' ') {
- const partialPath = this.currentLine.slice(0, endOfFilePath)
- // consider the file matching done if the space is preceded by a file extension (e.g. ".tex")
- if (/\.\w+$/.test(partialPath)) {
- break
- }
- // advance to next space or ) or end of line
- const remainingPath = this.currentLine.slice(endOfFilePath + 1)
- // consider file matching done if current path is followed by any of "()[]
- if (/^\s*["()[\]]/.test(remainingPath)) {
- break
- }
- const nextEndOfPath = remainingPath.search(/[ "()[\]]/)
- if (nextEndOfPath === -1) {
- endOfFilePath = -1
- } else {
- endOfFilePath += nextEndOfPath + 1
- }
- }
- let path
- if (endOfFilePath === -1) {
- path = this.currentLine
- this.currentLine = ''
- } else {
- path = this.currentLine.slice(0, endOfFilePath)
- this.currentLine = this.currentLine.slice(endOfFilePath)
- }
- return path
- }
- postProcess(data) {
- const all = []
- const errors = []
- const warnings = []
- const typesetting = []
- const hashes = []
- const hashEntry = entry => entry.raw
- let i = 0
- while (i < data.length) {
- if (this.ignoreDuplicates && hashes.indexOf(hashEntry(data[i])) > -1) {
- i++
- continue
- }
- if (data[i].level === 'error') {
- errors.push(data[i])
- } else if (data[i].level === 'typesetting') {
- typesetting.push(data[i])
- } else if (data[i].level === 'warning') {
- warnings.push(data[i])
- }
- all.push(data[i])
- hashes.push(hashEntry(data[i]))
- i++
- }
- return {
- errors,
- warnings,
- typesetting,
- all,
- files: this.rootFileList,
- }
- }
- }
- const LogText = class LogText {
- constructor(text) {
- this.text = text.replace(/(\r\n)|\r/g, '\n')
- // Join any lines which look like they have wrapped.
- const wrappedLines = this.text.split('\n')
- this.lines = [wrappedLines[0]]
- let i = 1
- while (i < wrappedLines.length) {
- // If the previous line is as long as the wrap limit then
- // append this line to it.
- // Some lines end with ... when LaTeX knows it's hit the limit
- // These shouldn't be wrapped.
- if (
- wrappedLines[i - 1].length === LOG_WRAP_LIMIT &&
- wrappedLines[i - 1].slice(-3) !== '...'
- ) {
- this.lines[this.lines.length - 1] += wrappedLines[i]
- } else {
- this.lines.push(wrappedLines[i])
- }
- i++
- }
- this.row = 0
- }
- nextLine() {
- this.row++
- if (this.row >= this.lines.length) {
- return false
- } else {
- return this.lines[this.row]
- }
- }
- rewindLine() {
- this.row--
- }
- linesUpToNextWhitespaceLine() {
- return this.linesUpToNextMatchingLine(/^ *$/)
- }
- linesUpToNextMatchingLine(match) {
- const lines = []
- let nextLine = this.nextLine()
- if (nextLine !== false) {
- lines.push(nextLine)
- }
- while (nextLine !== false && !nextLine.match(match) && nextLine !== false) {
- nextLine = this.nextLine()
- if (nextLine !== false) {
- lines.push(nextLine)
- }
- }
- return lines
- }
- }
|