define(function() { // Define some constants var LOG_WRAP_LIMIT = 79; var LATEX_WARNING_REGEX = /^LaTeX Warning: (.*)$/; var HBOX_WARNING_REGEX = /^(Over|Under)full \\(v|h)box/; var PACKAGE_WARNING_REGEX = /^(Package \b.+\b Warning:.*)$/; // This is used to parse the line number from common latex warnings var LINES_REGEX = /lines? ([0-9]+)/; // This is used to parse the package name from the package warnings var PACKAGE_REGEX = /^Package (\b.+\b) Warning/; var LogText = function(text) { this.text = text.replace(/(\r\n)|\r/g, "\n"); // Join any lines which look like they have wrapped. var wrappedLines = this.text.split("\n"); this.lines = [wrappedLines[0]]; for (var i = 1; i < wrappedLines.length; i++) { // 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]); } } this.row = 0; }; (function() { this.nextLine = function() { this.row++; if (this.row >= this.lines.length) { return false; } else { return this.lines[this.row]; } }; this.rewindLine = function() { this.row--; }; this.linesUpToNextWhitespaceLine = function() { return this.linesUpToNextMatchingLine(/^ *$/); }; this.linesUpToNextMatchingLine = function(match) { var lines = []; var 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; } }).call(LogText.prototype); var state = { NORMAL : 0, ERROR : 1 }; var LatexParser = function(text, options) { this.log = new LogText(text); 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; }; (function() { this.parse = function() { 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.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; var lineNo = this.currentError.raw.match(/l\.([0-9]+)/); if (lineNo) { this.currentError.line = parseInt(lineNo[1], 10); } this.data.push(this.currentError); this.state = state.NORMAL; } } return this.postProcess(this.data); }; this.currentLineIsError = function() { return this.currentLine[0] == "!"; }; this.currentLineIsWarning = function() { return !!(this.currentLine.match(LATEX_WARNING_REGEX)); }; this.currentLineIsPackageWarning = function () { return !!(this.currentLine.match(PACKAGE_WARNING_REGEX)); }; this.currentLineIsHboxWarning = function() { return !!(this.currentLine.match(HBOX_WARNING_REGEX)); }; this.parseSingleWarningLine = function(prefix_regex) { var warningMatch = this.currentLine.match(prefix_regex); if (!warningMatch) return; var warning = warningMatch[1]; var lineMatch = warning.match(LINES_REGEX); var line = lineMatch ? parseInt(lineMatch[1], 10) : null; this.data.push({ line : line, file : this.currentFilePath, level : "warning", message : warning, raw : warning }); }; this.parseMultipleWarningLine = function() { // Some package warnings are multiple lines, let's parse the first line var warningMatch = this.currentLine.match(PACKAGE_WARNING_REGEX); if (!warningMatch) return; // Something strange happened, return early var warning_lines = [warningMatch[1]]; var lineMatch = this.currentLine.match(LINES_REGEX); var line = lineMatch ? parseInt(lineMatch[1], 10) : null; var packageMatch = this.currentLine.match(PACKAGE_REGEX); var packageName = packageMatch[1]; // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings var 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) warning_lines.push(warningMatch[1]); } var raw_message = warning_lines.join(' '); this.data.push({ line : line, file : this.currentFilePath, level : "warning", message : raw_message, raw : raw_message }); }; this.parseHboxLine = function() { var lineMatch = this.currentLine.match(LINES_REGEX); var line = lineMatch ? parseInt(lineMatch[1], 10) : null; this.data.push({ line : 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 this.parseParensForFilenames = function() { var pos = this.currentLine.search(/\(|\)/); if (pos != -1) { var token = this.currentLine[pos]; this.currentLine = this.currentLine.slice(pos + 1); if (token == "(") { var filePath = this.consumeFilePath(); if (filePath) { this.currentFilePath = filePath; var 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(); var 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(); } }; this.consumeFilePath = function() { // Our heuristic for detecting file names are rather crude // A file may not contain a space, or ) in it // To be a file path it must have at least one / if (!this.currentLine.match(/^\/?([^ \)]+\/)+/)) { return false; } var endOfFilePath = this.currentLine.search(/ |\)/); var path; if (endOfFilePath == -1) { path = this.currentLine; this.currentLine = ""; } else { path = this.currentLine.slice(0, endOfFilePath); this.currentLine = this.currentLine.slice(endOfFilePath); } return path; }; this.postProcess = function(data) { var all = []; var errors = []; var warnings = []; var typesetting = []; var hashes = []; function hashEntry(entry) { return entry.raw; } for (var i = 0; i < data.length; i++) { if (this.ignoreDuplicates && hashes.indexOf(hashEntry(data[i])) > -1) { 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])); } return { errors : errors, warnings : warnings, typesetting : typesetting, all : all, files : this.rootFileList } } }).call(LatexParser.prototype); LatexParser.parse = function(text, options) { return (new LatexParser(text, options)).parse() }; return LatexParser; });