bib-log-parser.js 7.3 KB

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