latex-log-parser.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. define(function() {
  2. // Define some constants
  3. var LOG_WRAP_LIMIT = 79;
  4. var LATEX_WARNING_REGEX = /^LaTeX Warning: (.*)$/;
  5. var HBOX_WARNING_REGEX = /^(Over|Under)full \\(v|h)box/;
  6. var PACKAGE_WARNING_REGEX = /^(Package \b.+\b Warning:.*)$/;
  7. // This is used to parse the line number from common latex warnings
  8. var LINES_REGEX = /lines? ([0-9]+)/;
  9. // This is used to parse the package name from the package warnings
  10. var PACKAGE_REGEX = /^Package (\b.+\b) Warning/;
  11. var LogText = function(text) {
  12. this.text = text.replace(/(\r\n)|\r/g, "\n");
  13. // Join any lines which look like they have wrapped.
  14. var wrappedLines = this.text.split("\n");
  15. this.lines = [wrappedLines[0]];
  16. for (var i = 1; i < wrappedLines.length; i++) {
  17. // If the previous line is as long as the wrap limit then
  18. // append this line to it.
  19. // Some lines end with ... when LaTeX knows it's hit the limit
  20. // These shouldn't be wrapped.
  21. if (wrappedLines[i-1].length == LOG_WRAP_LIMIT && wrappedLines[i-1].slice(-3) != "...") {
  22. this.lines[this.lines.length - 1] += wrappedLines[i];
  23. } else {
  24. this.lines.push(wrappedLines[i]);
  25. }
  26. }
  27. this.row = 0;
  28. };
  29. (function() {
  30. this.nextLine = function() {
  31. this.row++;
  32. if (this.row >= this.lines.length) {
  33. return false;
  34. } else {
  35. return this.lines[this.row];
  36. }
  37. };
  38. this.rewindLine = function() {
  39. this.row--;
  40. };
  41. this.linesUpToNextWhitespaceLine = function() {
  42. return this.linesUpToNextMatchingLine(/^ *$/);
  43. };
  44. this.linesUpToNextMatchingLine = function(match) {
  45. var lines = [];
  46. var nextLine = this.nextLine();
  47. if (nextLine !== false) {
  48. lines.push(nextLine);
  49. }
  50. while (nextLine !== false && !nextLine.match(match) && nextLine !== false) {
  51. nextLine = this.nextLine();
  52. if (nextLine !== false) {
  53. lines.push(nextLine);
  54. }
  55. }
  56. return lines;
  57. }
  58. }).call(LogText.prototype);
  59. var state = {
  60. NORMAL : 0,
  61. ERROR : 1
  62. };
  63. var LatexParser = function(text, options) {
  64. this.log = new LogText(text);
  65. this.state = state.NORMAL;
  66. options = options || {};
  67. this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/];
  68. this.ignoreDuplicates = options.ignoreDuplicates;
  69. this.data = [];
  70. this.fileStack = [];
  71. this.currentFileList = this.rootFileList = [];
  72. this.openParens = 0;
  73. };
  74. (function() {
  75. this.parse = function() {
  76. while ((this.currentLine = this.log.nextLine()) !== false) {
  77. if (this.state == state.NORMAL) {
  78. if (this.currentLineIsError()) {
  79. this.state = state.ERROR;
  80. this.currentError = {
  81. line : null,
  82. file : this.currentFilePath,
  83. level : "error",
  84. message : this.currentLine.slice(2),
  85. content : "",
  86. raw : this.currentLine + "\n"
  87. }
  88. } else if (this.currentLineIsWarning()) {
  89. this.parseSingleWarningLine(LATEX_WARNING_REGEX);
  90. } else if (this.currentLineIsHboxWarning()) {
  91. this.parseHboxLine();
  92. } else if (this.currentLineIsPackageWarning()) {
  93. this.parseMultipleWarningLine();
  94. } else {
  95. this.parseParensForFilenames();
  96. }
  97. }
  98. if (this.state == state.ERROR) {
  99. this.currentError.content += this.log.linesUpToNextMatchingLine(/^l\.[0-9]+/).join("\n");
  100. this.currentError.content += "\n";
  101. this.currentError.content += this.log.linesUpToNextWhitespaceLine().join("\n");
  102. this.currentError.content += "\n";
  103. this.currentError.content += this.log.linesUpToNextWhitespaceLine().join("\n");
  104. this.currentError.raw += this.currentError.content;
  105. var lineNo = this.currentError.raw.match(/l\.([0-9]+)/);
  106. if (lineNo) {
  107. this.currentError.line = parseInt(lineNo[1], 10);
  108. }
  109. this.data.push(this.currentError);
  110. this.state = state.NORMAL;
  111. }
  112. }
  113. return this.postProcess(this.data);
  114. };
  115. this.currentLineIsError = function() {
  116. return this.currentLine[0] == "!";
  117. };
  118. this.currentLineIsWarning = function() {
  119. return !!(this.currentLine.match(LATEX_WARNING_REGEX));
  120. };
  121. this.currentLineIsPackageWarning = function () {
  122. return !!(this.currentLine.match(PACKAGE_WARNING_REGEX));
  123. };
  124. this.currentLineIsHboxWarning = function() {
  125. return !!(this.currentLine.match(HBOX_WARNING_REGEX));
  126. };
  127. this.parseSingleWarningLine = function(prefix_regex) {
  128. var warningMatch = this.currentLine.match(prefix_regex);
  129. if (!warningMatch) return;
  130. var warning = warningMatch[1];
  131. var lineMatch = warning.match(LINES_REGEX);
  132. var line = lineMatch ? parseInt(lineMatch[1], 10) : null;
  133. this.data.push({
  134. line : line,
  135. file : this.currentFilePath,
  136. level : "warning",
  137. message : warning,
  138. raw : warning
  139. });
  140. };
  141. this.parseMultipleWarningLine = function() {
  142. // Some package warnings are multiple lines, let's parse the first line
  143. var warningMatch = this.currentLine.match(PACKAGE_WARNING_REGEX);
  144. if (!warningMatch) return; // Something strange happened, return early
  145. var warning_lines = [warningMatch[1]];
  146. var lineMatch = this.currentLine.match(LINES_REGEX);
  147. var line = lineMatch ? parseInt(lineMatch[1], 10) : null;
  148. var packageMatch = this.currentLine.match(PACKAGE_REGEX);
  149. var packageName = packageMatch[1];
  150. // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
  151. var prefixRegex = new RegExp("(?:\\(" + packageName + "\\))*[\\s]*(.*)", "i");
  152. // After every warning message there's a blank line, let's use it
  153. while (!!(this.currentLine = this.log.nextLine())) {
  154. lineMatch = this.currentLine.match(LINES_REGEX);
  155. line = lineMatch ? parseInt(lineMatch[1], 10) : line;
  156. warningMatch = this.currentLine.match(prefixRegex)
  157. warning_lines.push(warningMatch[1]);
  158. }
  159. var raw_message = warning_lines.join(' ');
  160. this.data.push({
  161. line : line,
  162. file : this.currentFilePath,
  163. level : "warning",
  164. message : raw_message,
  165. raw : raw_message
  166. });
  167. };
  168. this.parseHboxLine = function() {
  169. var lineMatch = this.currentLine.match(LINES_REGEX);
  170. var line = lineMatch ? parseInt(lineMatch[1], 10) : null;
  171. this.data.push({
  172. line : line,
  173. file : this.currentFilePath,
  174. level : "typesetting",
  175. message : this.currentLine,
  176. raw : this.currentLine
  177. });
  178. };
  179. // Check if we're entering or leaving a new file in this line
  180. this.parseParensForFilenames = function() {
  181. var pos = this.currentLine.search(/\(|\)/);
  182. if (pos != -1) {
  183. var token = this.currentLine[pos];
  184. this.currentLine = this.currentLine.slice(pos + 1);
  185. if (token == "(") {
  186. var filePath = this.consumeFilePath();
  187. if (filePath) {
  188. this.currentFilePath = filePath;
  189. var newFile = {
  190. path : filePath,
  191. files : []
  192. };
  193. this.fileStack.push(newFile);
  194. this.currentFileList.push(newFile);
  195. this.currentFileList = newFile.files;
  196. } else {
  197. this.openParens++;
  198. }
  199. } else if (token == ")") {
  200. if (this.openParens > 0) {
  201. this.openParens--;
  202. } else {
  203. if (this.fileStack.length > 1) {
  204. this.fileStack.pop();
  205. var previousFile = this.fileStack[this.fileStack.length - 1];
  206. this.currentFilePath = previousFile.path;
  207. this.currentFileList = previousFile.files;
  208. }
  209. // else {
  210. // Something has gone wrong but all we can do now is ignore it :(
  211. // }
  212. }
  213. }
  214. // Process the rest of the line
  215. this.parseParensForFilenames();
  216. }
  217. };
  218. this.consumeFilePath = function() {
  219. // Our heuristic for detecting file names are rather crude
  220. // A file may not contain a space, or ) in it
  221. // To be a file path it must have at least one /
  222. if (!this.currentLine.match(/^\/?([^ \)]+\/)+/)) {
  223. return false;
  224. }
  225. var endOfFilePath = this.currentLine.search(/ |\)/);
  226. var path;
  227. if (endOfFilePath == -1) {
  228. path = this.currentLine;
  229. this.currentLine = "";
  230. } else {
  231. path = this.currentLine.slice(0, endOfFilePath);
  232. this.currentLine = this.currentLine.slice(endOfFilePath);
  233. }
  234. return path;
  235. };
  236. this.postProcess = function(data) {
  237. var all = [];
  238. var errors = [];
  239. var warnings = [];
  240. var typesetting = [];
  241. var hashes = [];
  242. function hashEntry(entry) {
  243. return entry.raw;
  244. }
  245. for (var i = 0; i < data.length; i++) {
  246. if (this.ignoreDuplicates && hashes.indexOf(hashEntry(data[i])) > -1) {
  247. continue;
  248. }
  249. if (data[i].level == "error") {
  250. errors.push(data[i]);
  251. } else if (data[i].level == "typesetting") {
  252. typesetting.push(data[i]);
  253. } else if (data[i].level == "warning") {
  254. warnings.push(data[i]);
  255. }
  256. all.push(data[i]);
  257. hashes.push(hashEntry(data[i]));
  258. }
  259. return {
  260. errors : errors,
  261. warnings : warnings,
  262. typesetting : typesetting,
  263. all : all,
  264. files : this.rootFileList
  265. }
  266. }
  267. }).call(LatexParser.prototype);
  268. LatexParser.parse = function(text, options) {
  269. return (new LatexParser(text, options)).parse()
  270. };
  271. return LatexParser;
  272. });