bib-log-parser.js 7.0 KB

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