output-files.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import getMeta from '../../../utils/meta'
  2. import HumanReadableLogs from '../../../ide/human-readable-logs/HumanReadableLogs'
  3. import BibLogParser from '../../../ide/log-parser/bib-log-parser'
  4. import { v4 as uuid } from 'uuid'
  5. // Warnings that may disappear after a second LaTeX pass
  6. const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/
  7. export function handleOutputFiles(outputFiles, projectId, data) {
  8. const outputFile = outputFiles.get('output.pdf')
  9. if (!outputFile) return null
  10. // build the URL for viewing the PDF in the preview UI
  11. const params = new URLSearchParams({
  12. compileGroup: data.compileGroup,
  13. })
  14. if (data.clsiServerId) {
  15. params.set('clsiserverid', data.clsiServerId)
  16. }
  17. if (getMeta('ol-pdfCachingMode') === 'enabled') {
  18. // Tag traffic that uses the pdf caching logic.
  19. params.set('enable_pdf_caching', 'true')
  20. }
  21. outputFile.pdfUrl = `${buildURL(
  22. outputFile,
  23. data.pdfDownloadDomain
  24. )}?${params}`
  25. // build the URL for downloading the PDF
  26. params.set('popupDownload', 'true') // save PDF download as file
  27. outputFile.pdfDownloadUrl = `/download/project/${projectId}/build/${outputFile.build}/output/output.pdf?${params}`
  28. return outputFile
  29. }
  30. export const handleLogFiles = async (outputFiles, data, signal) => {
  31. const result = {
  32. log: null,
  33. logEntries: {
  34. errors: [],
  35. warnings: [],
  36. typesetting: [],
  37. },
  38. }
  39. function accumulateResults(newEntries, type) {
  40. for (const key in result.logEntries) {
  41. if (newEntries[key]) {
  42. for (const entry of newEntries[key]) {
  43. if (type) {
  44. entry.type = newEntries.type
  45. }
  46. if (entry.file) {
  47. entry.file = normalizeFilePath(entry.file)
  48. }
  49. entry.key = uuid()
  50. }
  51. result.logEntries[key].push(...newEntries[key])
  52. }
  53. }
  54. }
  55. const logFile = outputFiles.get('output.log')
  56. if (logFile) {
  57. try {
  58. const response = await fetch(buildURL(logFile, data.pdfDownloadDomain), {
  59. signal,
  60. })
  61. result.log = await response.text()
  62. let { errors, warnings, typesetting } = HumanReadableLogs.parse(
  63. result.log,
  64. {
  65. ignoreDuplicates: true,
  66. oldRegexes:
  67. getMeta('ol-splitTestVariants')?.['latex-log-parser'] !== 'new',
  68. }
  69. )
  70. if (data.status === 'stopped-on-first-error') {
  71. // Hide warnings that could disappear after a second pass
  72. warnings = warnings.filter(warning => !isTransientWarning(warning))
  73. }
  74. accumulateResults({ errors, warnings, typesetting })
  75. } catch (e) {
  76. console.warn(e) // ignore failure to fetch/parse the log file, but log a warning
  77. }
  78. }
  79. const blgFile = outputFiles.get('output.blg')
  80. if (blgFile) {
  81. try {
  82. const response = await fetch(buildURL(blgFile, data.pdfDownloadDomain), {
  83. signal,
  84. })
  85. const log = await response.text()
  86. try {
  87. const { errors, warnings } = new BibLogParser(log, {
  88. maxErrors: 100,
  89. }).parse()
  90. accumulateResults({ errors, warnings }, 'BibTeX:')
  91. } catch (e) {
  92. // BibLog parsing errors are ignored
  93. }
  94. } catch (e) {
  95. console.warn(e) // ignore failure to fetch/parse the log file, but log a warning
  96. }
  97. }
  98. result.logEntries.all = [
  99. ...result.logEntries.errors,
  100. ...result.logEntries.warnings,
  101. ...result.logEntries.typesetting,
  102. ]
  103. return result
  104. }
  105. export function buildLogEntryAnnotations(entries, fileTreeManager) {
  106. const rootDocDirname = fileTreeManager.getRootDocDirname()
  107. const logEntryAnnotations = {}
  108. for (const entry of entries) {
  109. if (entry.file) {
  110. entry.file = normalizeFilePath(entry.file, rootDocDirname)
  111. const entity = fileTreeManager.findEntityByPath(entry.file)
  112. if (entity) {
  113. if (!(entity.id in logEntryAnnotations)) {
  114. logEntryAnnotations[entity.id] = []
  115. }
  116. logEntryAnnotations[entity.id].push({
  117. row: entry.line - 1,
  118. type: entry.level === 'error' ? 'error' : 'warning',
  119. text: entry.message,
  120. source: 'compile', // NOTE: this is used in Ace for filtering the annotations
  121. })
  122. }
  123. }
  124. }
  125. return logEntryAnnotations
  126. }
  127. function buildURL(file, pdfDownloadDomain) {
  128. if (file.build && pdfDownloadDomain) {
  129. // Downloads from the compiles domain must include a build id.
  130. // The build id is used implicitly for access control.
  131. return `${pdfDownloadDomain}${file.url}`
  132. }
  133. // Go through web instead, which uses mongo for checking project access.
  134. return file.url
  135. }
  136. function normalizeFilePath(path, rootDocDirname) {
  137. path = path.replace(
  138. /^.*\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/,
  139. ''
  140. )
  141. path = path.replace(/^\/compile\//, '')
  142. if (rootDocDirname) {
  143. path = path.replace(/^\.\//, rootDocDirname + '/')
  144. }
  145. return path
  146. }
  147. function isTransientWarning(warning) {
  148. return TRANSIENT_WARNING_REGEX.test(warning.message)
  149. }