latex-log-parser.js 11 KB

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