bib-log-parser.js 6.5 KB

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