bib-log-parser.ts 7.1 KB

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