latex-log-parser.js 11 KB

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