bib-log-parser.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // [fullLine, lineNumber, messageType, message]
  2. const LINE_SPLITTER_REGEX = /^\[(\d+)].*>\s(INFO|WARN|ERROR)\s-\s(.*)$/
  3. const MULTILINE_WARNING_REGEX = /^Warning--(.+)\n--line (\d+) of file (.+)$/m
  4. const SINGLELINE_WARNING_REGEX = /^Warning--(.+)$/m
  5. const MULTILINE_ERROR_REGEX =
  6. /^(.*)---line (\d+) of file (.*)\n([^]+?)\nI'm skipping whatever remains of this entry$/m
  7. const BAD_CROSS_REFERENCE_REGEX =
  8. /^(A bad cross reference---entry ".+?"\nrefers to entry.+?, which doesn't exist)$/m
  9. const MULTILINE_COMMAND_ERROR_REGEX =
  10. /^(.*)\n?---line (\d+) of file (.*)\n([^]+?)\nI'm skipping whatever remains of this command$/m
  11. // Errors hit in BST file have a slightly different format
  12. const BST_ERROR_REGEX = /^(.*?)\nwhile executing---line (\d+) of file (.*)/m
  13. const MESSAGE_LEVELS = {
  14. INFO: 'info',
  15. WARN: 'warning',
  16. ERROR: 'error',
  17. }
  18. const parserReducer = function (maxErrors) {
  19. return function (accumulator, parser) {
  20. const consume = function (logText, regex, process) {
  21. let match
  22. let text = logText
  23. const result = []
  24. const re = regex
  25. let iterationCount = 0
  26. while ((match = re.exec(text))) {
  27. iterationCount += 1
  28. const newEntry = process(match)
  29. // Too many log entries can cause browser crashes
  30. // Construct a too many files error from the last match
  31. if (iterationCount >= maxErrors) {
  32. const level = newEntry.level + 's'
  33. newEntry.message = [
  34. 'Over',
  35. maxErrors,
  36. level,
  37. 'returned. Download raw logs to see full list',
  38. ].join(' ')
  39. newEntry.line = undefined
  40. result.unshift(newEntry)
  41. return [result, '']
  42. }
  43. result.push(newEntry)
  44. text =
  45. match.input.slice(0, match.index) +
  46. match.input.slice(
  47. match.index + match[0].length + 1,
  48. match.input.length
  49. )
  50. }
  51. return [result, text]
  52. }
  53. const [currentErrors, text] = accumulator
  54. const [regex, process] = parser
  55. const [errors, _remainingText] = consume(text, regex, process)
  56. return [currentErrors.concat(errors), _remainingText]
  57. }
  58. }
  59. export default class BibLogParser {
  60. constructor(text, options) {
  61. if (typeof text !== 'string') {
  62. throw new Error('BibLogParser Error: text parameter must be a string')
  63. }
  64. this.text = text.replace(/(\r\n)|\r/g, '\n')
  65. this.options = options || {}
  66. this.lines = text.split('\n')
  67. // each parser is a pair of [regex, processFunction], where processFunction
  68. // describes how to transform the regex mactch into a log entry object.
  69. this.warningParsers = [
  70. [
  71. MULTILINE_WARNING_REGEX,
  72. function (match) {
  73. const [fullMatch, message, lineNumber, fileName] = match
  74. return {
  75. file: fileName,
  76. level: 'warning',
  77. message,
  78. line: lineNumber,
  79. raw: fullMatch,
  80. }
  81. },
  82. ],
  83. [
  84. SINGLELINE_WARNING_REGEX,
  85. function (match) {
  86. const [fullMatch, message] = match
  87. return {
  88. file: '',
  89. level: 'warning',
  90. message,
  91. line: '',
  92. raw: fullMatch,
  93. }
  94. },
  95. ],
  96. ]
  97. this.errorParsers = [
  98. [
  99. MULTILINE_ERROR_REGEX,
  100. function (match) {
  101. const [fullMatch, firstMessage, lineNumber, fileName, secondMessage] =
  102. match
  103. return {
  104. file: fileName,
  105. level: 'error',
  106. message: firstMessage + '\n' + secondMessage,
  107. line: lineNumber,
  108. raw: fullMatch,
  109. }
  110. },
  111. ],
  112. [
  113. BAD_CROSS_REFERENCE_REGEX,
  114. function (match) {
  115. const [fullMatch, message] = match
  116. return {
  117. file: '',
  118. level: 'error',
  119. message,
  120. line: '',
  121. raw: fullMatch,
  122. }
  123. },
  124. ],
  125. [
  126. MULTILINE_COMMAND_ERROR_REGEX,
  127. function (match) {
  128. const [fullMatch, firstMessage, lineNumber, fileName, secondMessage] =
  129. match
  130. return {
  131. file: fileName,
  132. level: 'error',
  133. message: firstMessage + '\n' + secondMessage,
  134. line: lineNumber,
  135. raw: fullMatch,
  136. }
  137. },
  138. ],
  139. [
  140. BST_ERROR_REGEX,
  141. function (match) {
  142. const [fullMatch, firstMessage, lineNumber, fileName] = match
  143. return {
  144. file: fileName,
  145. level: 'error',
  146. message: firstMessage,
  147. line: lineNumber,
  148. raw: fullMatch,
  149. }
  150. },
  151. ],
  152. ]
  153. }
  154. parseBibtex() {
  155. let allErrors
  156. const result = {
  157. all: [],
  158. errors: [],
  159. warnings: [],
  160. files: [], // not used
  161. typesetting: [], // not used
  162. }
  163. // reduce over the parsers, starting with the log text,
  164. let [allWarnings, remainingText] = this.warningParsers.reduce(
  165. parserReducer(this.options.maxErrors),
  166. [[], this.text]
  167. )
  168. ;[allErrors, remainingText] = this.errorParsers.reduce(
  169. parserReducer(this.options.maxErrors),
  170. [[], remainingText]
  171. )
  172. result.warnings = allWarnings
  173. result.errors = allErrors
  174. result.all = allWarnings.concat(allErrors)
  175. return result
  176. }
  177. parseBiber() {
  178. const result = {
  179. all: [],
  180. errors: [],
  181. warnings: [],
  182. files: [], // not used
  183. typesetting: [], // not used
  184. }
  185. this.lines.forEach(function (line) {
  186. const match = line.match(LINE_SPLITTER_REGEX)
  187. if (match) {
  188. const fullLine = match[0]
  189. const messageType = match[2]
  190. const message = match[3]
  191. const newEntry = {
  192. file: '',
  193. level: MESSAGE_LEVELS[messageType] || 'INFO',
  194. message,
  195. line: '',
  196. raw: fullLine,
  197. }
  198. // try extract file, line-number and the 'real' message from lines like:
  199. // BibTeX subsystem: /.../original.bib_123.utf8, line 8, syntax error: it's bad
  200. const lineMatch = newEntry.message.match(
  201. /^BibTeX subsystem: \/.+\/(\w+\.\w+)_.+, line (\d+), (.+)$/
  202. )
  203. if (lineMatch && lineMatch.length === 4) {
  204. const fileName = lineMatch[1]
  205. const lineNumber = lineMatch[2]
  206. const realMessage = lineMatch[3]
  207. newEntry.file = fileName
  208. newEntry.line = lineNumber
  209. newEntry.message = realMessage
  210. }
  211. result.all.push(newEntry)
  212. switch (newEntry.level) {
  213. case 'error':
  214. return result.errors.push(newEntry)
  215. case 'warning':
  216. return result.warnings.push(newEntry)
  217. }
  218. }
  219. })
  220. return result
  221. }
  222. parse() {
  223. const firstLine = this.lines[0]
  224. if (firstLine.match(/^.*INFO - This is Biber.*$/)) {
  225. return this.parseBiber()
  226. } else if (firstLine.match(/^This is BibTeX, Version.+$/)) {
  227. return this.parseBibtex()
  228. } else {
  229. throw new Error(
  230. 'BibLogParser Error: cannot determine whether text is biber or bibtex output'
  231. )
  232. }
  233. }
  234. }