latex-log-parser.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // Define some constants
  2. const LOG_WRAP_LIMIT = 79
  3. const LATEX_WARNING_REGEX = /^LaTeX Warning: (.*)$/
  4. const HBOX_WARNING_REGEX = /^(Over|Under)full \\(v|h)box/
  5. const PACKAGE_WARNING_REGEX = /^(Package \b.+\b Warning:.*)$/
  6. // This is used to parse the line number from common latex warnings
  7. const LINES_REGEX = /lines? ([0-9]+)/
  8. // This is used to parse the package name from the package warnings
  9. const PACKAGE_REGEX = /^Package (\b.+\b) Warning/
  10. const FILE_LINE_ERROR_REGEX = /^([./].*):(\d+): (.*)/
  11. const STATE = {
  12. NORMAL: 0,
  13. ERROR: 1,
  14. }
  15. export default class LatexParser {
  16. constructor(text, options) {
  17. this.state = STATE.NORMAL
  18. options = options || {}
  19. this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
  20. this.ignoreDuplicates = options.ignoreDuplicates
  21. this.data = []
  22. this.fileStack = []
  23. this.currentFileList = this.rootFileList = []
  24. this.openParens = 0
  25. this.log = new LogText(text)
  26. }
  27. parse() {
  28. while ((this.currentLine = this.log.nextLine()) !== false) {
  29. if (this.state === STATE.NORMAL) {
  30. if (this.currentLineIsError()) {
  31. this.state = STATE.ERROR
  32. this.currentError = {
  33. line: null,
  34. file: this.currentFilePath,
  35. level: 'error',
  36. message: this.currentLine.slice(2),
  37. content: '',
  38. raw: this.currentLine + '\n',
  39. }
  40. } else if (this.currentLineIsFileLineError()) {
  41. this.state = STATE.ERROR
  42. this.parseFileLineError()
  43. } else if (this.currentLineIsRunawayArgument()) {
  44. this.parseRunawayArgumentError()
  45. } else if (this.currentLineIsWarning()) {
  46. this.parseSingleWarningLine(LATEX_WARNING_REGEX)
  47. } else if (this.currentLineIsHboxWarning()) {
  48. this.parseHboxLine()
  49. } else if (this.currentLineIsPackageWarning()) {
  50. this.parseMultipleWarningLine()
  51. } else {
  52. this.parseParensForFilenames()
  53. }
  54. }
  55. if (this.state === STATE.ERROR) {
  56. this.currentError.content += this.log
  57. .linesUpToNextMatchingLine(/^l\.[0-9]+/)
  58. .join('\n')
  59. this.currentError.content += '\n'
  60. this.currentError.content += this.log
  61. .linesUpToNextWhitespaceLine()
  62. .join('\n')
  63. this.currentError.content += '\n'
  64. this.currentError.content += this.log
  65. .linesUpToNextWhitespaceLine()
  66. .join('\n')
  67. this.currentError.raw += this.currentError.content
  68. const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
  69. if (lineNo && this.currentError.line === null) {
  70. this.currentError.line = parseInt(lineNo[1], 10)
  71. }
  72. this.data.push(this.currentError)
  73. this.state = STATE.NORMAL
  74. }
  75. }
  76. return this.postProcess(this.data)
  77. }
  78. currentLineIsError() {
  79. return this.currentLine[0] === '!'
  80. }
  81. currentLineIsFileLineError() {
  82. return FILE_LINE_ERROR_REGEX.test(this.currentLine)
  83. }
  84. currentLineIsRunawayArgument() {
  85. return this.currentLine.match(/^Runaway argument/)
  86. }
  87. currentLineIsWarning() {
  88. return !!this.currentLine.match(LATEX_WARNING_REGEX)
  89. }
  90. currentLineIsPackageWarning() {
  91. return !!this.currentLine.match(PACKAGE_WARNING_REGEX)
  92. }
  93. currentLineIsHboxWarning() {
  94. return !!this.currentLine.match(HBOX_WARNING_REGEX)
  95. }
  96. parseFileLineError() {
  97. const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
  98. this.currentError = {
  99. line: result[2],
  100. file: result[1],
  101. level: 'error',
  102. message: result[3],
  103. content: '',
  104. raw: this.currentLine + '\n',
  105. }
  106. }
  107. parseRunawayArgumentError() {
  108. this.currentError = {
  109. line: null,
  110. file: this.currentFilePath,
  111. level: 'error',
  112. message: this.currentLine,
  113. content: '',
  114. raw: this.currentLine + '\n',
  115. }
  116. this.currentError.content += this.log
  117. .linesUpToNextWhitespaceLine()
  118. .join('\n')
  119. this.currentError.content += '\n'
  120. this.currentError.content += this.log
  121. .linesUpToNextWhitespaceLine()
  122. .join('\n')
  123. this.currentError.raw += this.currentError.content
  124. const lineNo = this.currentError.raw.match(/l\.([0-9]+)/)
  125. if (lineNo) {
  126. this.currentError.line = parseInt(lineNo[1], 10)
  127. }
  128. return this.data.push(this.currentError)
  129. }
  130. parseSingleWarningLine(prefixRegex) {
  131. const warningMatch = this.currentLine.match(prefixRegex)
  132. if (!warningMatch) {
  133. return
  134. }
  135. const warning = warningMatch[1]
  136. const lineMatch = warning.match(LINES_REGEX)
  137. const line = lineMatch ? parseInt(lineMatch[1], 10) : null
  138. this.data.push({
  139. line,
  140. file: this.currentFilePath,
  141. level: 'warning',
  142. message: warning,
  143. raw: warning,
  144. })
  145. }
  146. parseMultipleWarningLine() {
  147. // Some package warnings are multiple lines, let's parse the first line
  148. let warningMatch = this.currentLine.match(PACKAGE_WARNING_REGEX)
  149. if (!warningMatch) {
  150. return
  151. }
  152. // Something strange happened, return early
  153. const warningLines = [warningMatch[1]]
  154. let lineMatch = this.currentLine.match(LINES_REGEX)
  155. let line = lineMatch ? parseInt(lineMatch[1], 10) : null
  156. const packageMatch = this.currentLine.match(PACKAGE_REGEX)
  157. const packageName = packageMatch[1]
  158. // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
  159. const prefixRegex = new RegExp(
  160. '(?:\\(' + packageName + '\\))*[\\s]*(.*)',
  161. 'i'
  162. )
  163. // After every warning message there's a blank line, let's use it
  164. while ((this.currentLine = this.log.nextLine())) {
  165. lineMatch = this.currentLine.match(LINES_REGEX)
  166. line = lineMatch ? parseInt(lineMatch[1], 10) : line
  167. warningMatch = this.currentLine.match(prefixRegex)
  168. warningLines.push(warningMatch[1])
  169. }
  170. const rawMessage = warningLines.join(' ')
  171. this.data.push({
  172. line,
  173. file: this.currentFilePath,
  174. level: 'warning',
  175. message: rawMessage,
  176. raw: rawMessage,
  177. })
  178. }
  179. parseHboxLine() {
  180. const lineMatch = this.currentLine.match(LINES_REGEX)
  181. const line = lineMatch ? parseInt(lineMatch[1], 10) : null
  182. this.data.push({
  183. line,
  184. file: this.currentFilePath,
  185. level: 'typesetting',
  186. message: this.currentLine,
  187. raw: this.currentLine,
  188. })
  189. }
  190. // Check if we're entering or leaving a new file in this line
  191. parseParensForFilenames() {
  192. const pos = this.currentLine.search(/\(|\)/)
  193. if (pos !== -1) {
  194. const token = this.currentLine[pos]
  195. this.currentLine = this.currentLine.slice(pos + 1)
  196. if (token === '(') {
  197. const filePath = this.consumeFilePath()
  198. if (filePath) {
  199. this.currentFilePath = filePath
  200. const newFile = {
  201. path: filePath,
  202. files: [],
  203. }
  204. this.fileStack.push(newFile)
  205. this.currentFileList.push(newFile)
  206. this.currentFileList = newFile.files
  207. } else {
  208. this.openParens++
  209. }
  210. } else if (token === ')') {
  211. if (this.openParens > 0) {
  212. this.openParens--
  213. } else {
  214. if (this.fileStack.length > 1) {
  215. this.fileStack.pop()
  216. const previousFile = this.fileStack[this.fileStack.length - 1]
  217. this.currentFilePath = previousFile.path
  218. this.currentFileList = previousFile.files
  219. }
  220. }
  221. }
  222. // else {
  223. // Something has gone wrong but all we can do now is ignore it :(
  224. // }
  225. // Process the rest of the line
  226. this.parseParensForFilenames()
  227. }
  228. }
  229. consumeFilePath() {
  230. // Our heuristic for detecting file names are rather crude
  231. // A file may not contain a ')' in it
  232. // To be a file path it must have at least one /
  233. if (!this.currentLine.match(/^\/?([^ )]+\/)+/)) {
  234. return false
  235. }
  236. let endOfFilePath = this.currentLine.search(/ |\)/)
  237. // handle the case where there is a space in a filename
  238. while (endOfFilePath !== -1 && this.currentLine[endOfFilePath] === ' ') {
  239. const partialPath = this.currentLine.slice(0, endOfFilePath)
  240. // consider the file matching done if the space is preceded by a file extension (e.g. ".tex")
  241. if (/\.\w+$/.test(partialPath)) {
  242. break
  243. }
  244. // advance to next space or ) or end of line
  245. const remainingPath = this.currentLine.slice(endOfFilePath + 1)
  246. // consider file matching done if current path is followed by any of "()[]
  247. if (/^\s*["()[\]]/.test(remainingPath)) {
  248. break
  249. }
  250. const nextEndOfPath = remainingPath.search(/[ "()[\]]/)
  251. if (nextEndOfPath === -1) {
  252. endOfFilePath = -1
  253. } else {
  254. endOfFilePath += nextEndOfPath + 1
  255. }
  256. }
  257. let path
  258. if (endOfFilePath === -1) {
  259. path = this.currentLine
  260. this.currentLine = ''
  261. } else {
  262. path = this.currentLine.slice(0, endOfFilePath)
  263. this.currentLine = this.currentLine.slice(endOfFilePath)
  264. }
  265. return path
  266. }
  267. postProcess(data) {
  268. const all = []
  269. const errors = []
  270. const warnings = []
  271. const typesetting = []
  272. const hashes = []
  273. const hashEntry = entry => entry.raw
  274. let i = 0
  275. while (i < data.length) {
  276. if (this.ignoreDuplicates && hashes.indexOf(hashEntry(data[i])) > -1) {
  277. i++
  278. continue
  279. }
  280. if (data[i].level === 'error') {
  281. errors.push(data[i])
  282. } else if (data[i].level === 'typesetting') {
  283. typesetting.push(data[i])
  284. } else if (data[i].level === 'warning') {
  285. warnings.push(data[i])
  286. }
  287. all.push(data[i])
  288. hashes.push(hashEntry(data[i]))
  289. i++
  290. }
  291. return {
  292. errors,
  293. warnings,
  294. typesetting,
  295. all,
  296. files: this.rootFileList,
  297. }
  298. }
  299. }
  300. const LogText = class LogText {
  301. constructor(text) {
  302. this.text = text.replace(/(\r\n)|\r/g, '\n')
  303. // Join any lines which look like they have wrapped.
  304. const wrappedLines = this.text.split('\n')
  305. this.lines = [wrappedLines[0]]
  306. let i = 1
  307. while (i < wrappedLines.length) {
  308. // If the previous line is as long as the wrap limit then
  309. // append this line to it.
  310. // Some lines end with ... when LaTeX knows it's hit the limit
  311. // These shouldn't be wrapped.
  312. if (
  313. wrappedLines[i - 1].length === LOG_WRAP_LIMIT &&
  314. wrappedLines[i - 1].slice(-3) !== '...'
  315. ) {
  316. this.lines[this.lines.length - 1] += wrappedLines[i]
  317. } else {
  318. this.lines.push(wrappedLines[i])
  319. }
  320. i++
  321. }
  322. this.row = 0
  323. }
  324. nextLine() {
  325. this.row++
  326. if (this.row >= this.lines.length) {
  327. return false
  328. } else {
  329. return this.lines[this.row]
  330. }
  331. }
  332. rewindLine() {
  333. this.row--
  334. }
  335. linesUpToNextWhitespaceLine() {
  336. return this.linesUpToNextMatchingLine(/^ *$/)
  337. }
  338. linesUpToNextMatchingLine(match) {
  339. const lines = []
  340. let nextLine = this.nextLine()
  341. if (nextLine !== false) {
  342. lines.push(nextLine)
  343. }
  344. while (nextLine !== false && !nextLine.match(match) && nextLine !== false) {
  345. nextLine = this.nextLine()
  346. if (nextLine !== false) {
  347. lines.push(nextLine)
  348. }
  349. }
  350. return lines
  351. }
  352. }