output-files.js 5.4 KB

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