diff --git a/services/web/frontend/js/features/pdf-preview/util/output-files.js b/services/web/frontend/js/features/pdf-preview/util/output-files.ts similarity index 65% rename from services/web/frontend/js/features/pdf-preview/util/output-files.js rename to services/web/frontend/js/features/pdf-preview/util/output-files.ts index 0903ec10e3..dedbaa9a33 100644 --- a/services/web/frontend/js/features/pdf-preview/util/output-files.js +++ b/services/web/frontend/js/features/pdf-preview/util/output-files.ts @@ -1,10 +1,17 @@ import HumanReadableLogs from '../../../ide/human-readable-logs/HumanReadableLogs' -import BibLogParser from '../../../ide/log-parser/bib-log-parser' +import BibLogParser, { + BibLogEntry, +} from '../../../ide/log-parser/bib-log-parser' import { enablePdfCaching } from './pdf-caching-flags' import { debugConsole } from '@/utils/debugging' import { dirname, findEntityByPath } from '@/features/file-tree/util/path' import '@/utils/readable-stream-async-iterator-polyfill' import { EDITOR_SESSION_ID } from '@/features/pdf-preview/util/metrics' +import { LogEntry } from './types' +import { CompileResponseData, PDFFile } from '@ol-types/compile' +import { LatexLogEntry } from '@/ide/log-parser/latex-log-parser' +import { Annotation } from '@ol-types/annotation' +import { Folder } from '@ol-types/folder' // Warnings that may disappear after a second LaTeX pass const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/ @@ -12,7 +19,11 @@ const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/ const MAX_LOG_SIZE = 1024 * 1024 // 1MB const MAX_BIB_LOG_SIZE_PER_FILE = MAX_LOG_SIZE -export function handleOutputFiles(outputFiles, projectId, data) { +export function handleOutputFiles( + outputFiles: Map, + projectId: string, + data: CompileResponseData +): PDFFile | null { const outputFile = outputFiles.get('output.pdf') if (!outputFile) return null @@ -34,10 +45,7 @@ export function handleOutputFiles(outputFiles, projectId, data) { params.set('enable_pdf_caching', 'true') } - outputFile.pdfUrl = `${buildURL( - outputFile, - data.pdfDownloadDomain - )}?${params}` + outputFile.pdfUrl = `${buildURL(outputFile, data.pdfDownloadDomain)}?${params}` if (data.fromCache) { outputFile.pdfDownloadUrl = outputFile.downloadURL @@ -53,33 +61,58 @@ export function handleOutputFiles(outputFiles, projectId, data) { let nextEntryId = 1 -function generateEntryKey() { +function generateEntryKey(): string { return 'compile-log-entry-' + nextEntryId++ } -export const handleLogFiles = async (outputFiles, data, signal) => { - const result = { +type LogResult = { + log: string | null + logEntries: { + errors: LogEntry[] + warnings: LogEntry[] + typesetting: LogEntry[] + all: LogEntry[] + } +} +export async function handleLogFiles( + outputFiles: Map, + data: CompileResponseData, + signal: AbortSignal +): Promise { + const result: LogResult = { log: null, logEntries: { + all: [], errors: [], warnings: [], typesetting: [], }, } - function accumulateResults(newEntries, type) { - for (const key in result.logEntries) { + function accumulateResults( + newEntries: { + errors?: (LatexLogEntry | BibLogEntry)[] + warnings?: (LatexLogEntry | BibLogEntry)[] + typesetting?: (LatexLogEntry | BibLogEntry)[] + all?: (LatexLogEntry | BibLogEntry)[] + }, + type?: string + ) { + for (const key of Object.keys(result.logEntries) as Array< + keyof typeof result.logEntries + >) { if (newEntries[key]) { for (const entry of newEntries[key]) { if (type) { - entry.type = newEntries.type + // Type casting as we are mutating LatexLogEntry | BibLogEntry into a LogEntry + ;(entry as LogEntry).type = type } if (entry.file) { entry.file = normalizeFilePath(entry.file) } - entry.key = generateEntryKey() + ;(entry as LogEntry).key = generateEntryKey() } - result.logEntries[key].push(...newEntries[key]) + result.logEntries[key].push(...(newEntries[key] as LogEntry[])) } } } @@ -111,7 +144,7 @@ export const handleLogFiles = async (outputFiles, data, signal) => { } } - const blgFiles = [] + const blgFiles: PDFFile[] = [] for (const [filename, file] of outputFiles) { if (filename.endsWith('.blg')) { @@ -143,15 +176,15 @@ export const handleLogFiles = async (outputFiles, data, signal) => { return result } -/** - * @typedef {import('../../../../../types/annotation').Annotation} Annotation - * @returns {Record} - */ -export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) { - const rootDocDirname = dirname(fileTreeData, rootDocId) +export function buildLogEntryAnnotations( + entries: LogEntry[], + fileTreeData: Folder, + rootDocId?: string | null +): Record { + const rootDocDirname = rootDocId ? dirname(fileTreeData, rootDocId) : null - const logEntryAnnotations = {} - const seenLine = {} + const logEntryAnnotations: Record = {} + const seenLine: Record = {} for (const entry of entries) { if (entry.file) { @@ -164,12 +197,12 @@ export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) { logEntryAnnotations[entity._id] = [] } - const annotation = { + const annotation: Annotation = { id: entry.key, entryIndex: logEntryAnnotations[entity._id].length, // used for maintaining the order of items on the same line - row: entry.line - 1, + row: (entry.line || 1) - 1, type: entry.level === 'error' ? 'error' : 'warning', - text: entry.message, + text: entry.message ?? '', source: 'compile', // NOTE: this is used in Ace for filtering the annotations ruleId: entry.ruleId, command: entry.command, @@ -177,9 +210,9 @@ export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) { // set firstOnLine for the first non-typesetting annotation on a line if (entry.level !== 'typesetting') { - if (!seenLine[entry.line]) { + if (!seenLine[entry.line || 0]) { annotation.firstOnLine = true - seenLine[entry.line] = true + seenLine[entry.line || 0] = true } } @@ -191,8 +224,10 @@ export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) { return logEntryAnnotations } -export const buildRuleCounts = (entries = []) => { - const counts = {} +export const buildRuleCounts = ( + entries: LogEntry[] = [] +): Record => { + const counts: Record = {} for (const entry of entries) { const key = `${entry.level}_${entry.ruleId}` counts[key] = counts[key] ? counts[key] + 1 : 1 @@ -200,16 +235,17 @@ export const buildRuleCounts = (entries = []) => { return counts } -export const buildRuleDeltas = (ruleCounts, previousRuleCounts) => { - const counts = {} +export const buildRuleDeltas = ( + ruleCounts: Record, + previousRuleCounts: Record +): Record => { + const counts: Record = {} - // keys that are defined in the current log entries for (const [key, value] of Object.entries(ruleCounts)) { const previousValue = previousRuleCounts[key] ?? 0 counts[`delta_${key}`] = value - previousValue } - // keys that are no longer defined in the current log entries for (const [key, value] of Object.entries(previousRuleCounts)) { if (!(key in ruleCounts)) { counts[key] = 0 @@ -220,7 +256,7 @@ export const buildRuleDeltas = (ruleCounts, previousRuleCounts) => { return counts } -function buildURL(file, pdfDownloadDomain) { +function buildURL(file: PDFFile, pdfDownloadDomain?: string): string { if (file.build && pdfDownloadDomain) { // Downloads from the compiles domain must include a build id. // The build id is used implicitly for access control. @@ -230,13 +266,15 @@ function buildURL(file, pdfDownloadDomain) { return `${window.origin}${file.url}` } -function normalizeFilePath(path, rootDocDirname) { +function normalizeFilePath( + path: string, + rootDocDirname?: string | null +): string { path = path.replace(/\/\//g, '/') path = path.replace( /^.*\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, '' ) - path = path.replace(/^\/compile\//, '') if (rootDocDirname) { @@ -246,11 +284,15 @@ function normalizeFilePath(path, rootDocDirname) { return path } -function isTransientWarning(warning) { - return TRANSIENT_WARNING_REGEX.test(warning.message) +function isTransientWarning(warning: LatexLogEntry): boolean { + return TRANSIENT_WARNING_REGEX.test(warning.message || '') } -async function fetchFileWithSizeLimit(url, signal, maxSize) { +async function fetchFileWithSizeLimit( + url: string, + signal: AbortSignal, + maxSize: number +): Promise { let result = '' try { const abortController = new AbortController() @@ -267,11 +309,13 @@ async function fetchFileWithSizeLimit(url, signal, maxSize) { throw new Error('Failed to fetch log file') } - const reader = response.body.pipeThrough(new TextDecoderStream()) - for await (const chunk of reader) { - result += chunk - if (result.length > maxSize) { - abortController.abort() + const reader = response.body?.pipeThrough(new TextDecoderStream()) + if (reader) { + for await (const chunk of reader) { + result += chunk + if (result.length > maxSize) { + abortController.abort() + } } } } catch (e) { diff --git a/services/web/frontend/js/features/pdf-preview/util/types.ts b/services/web/frontend/js/features/pdf-preview/util/types.ts index 39c72193c9..6cae7a07a0 100644 --- a/services/web/frontend/js/features/pdf-preview/util/types.ts +++ b/services/web/frontend/js/features/pdf-preview/util/types.ts @@ -14,6 +14,7 @@ export type LogEntry = { type?: string messageComponent?: React.ReactNode contentDetails?: string[] + command?: string } export type ErrorLevel = diff --git a/services/web/frontend/js/ide/log-parser/bib-log-parser.ts b/services/web/frontend/js/ide/log-parser/bib-log-parser.ts index f6d1a9d05d..3b0662124b 100644 --- a/services/web/frontend/js/ide/log-parser/bib-log-parser.ts +++ b/services/web/frontend/js/ide/log-parser/bib-log-parser.ts @@ -64,7 +64,7 @@ const parserReducer = function (maxErrors: number | null) { } } -type BibLogEntry = { +export type BibLogEntry = { file: string level: string message: string diff --git a/services/web/frontend/js/ide/log-parser/latex-log-parser.ts b/services/web/frontend/js/ide/log-parser/latex-log-parser.ts index 8ad5cad231..785732586c 100644 --- a/services/web/frontend/js/ide/log-parser/latex-log-parser.ts +++ b/services/web/frontend/js/ide/log-parser/latex-log-parser.ts @@ -19,7 +19,7 @@ export type LatexParserOptions = { ignoreDuplicates?: boolean } -type LatexLogEntry = { +export type LatexLogEntry = { line: string | number | null file: string | undefined level: 'error' | 'warning' | 'typesetting' diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 64dc4ca207..25e292e0bf 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -25,7 +25,7 @@ import { buildRuleDeltas, handleLogFiles, handleOutputFiles, -} from '../../features/pdf-preview/util/output-files' +} from '@/features/pdf-preview/util/output-files' import { useProjectContext } from './project-context' import { useEditorContext } from './editor-context' import { buildFileList } from '../../features/pdf-preview/util/file-list' @@ -81,7 +81,7 @@ export type CompileContext = { logEntryAnnotations?: Record outputFilesArchive?: string pdfDownloadUrl?: string - pdfFile?: PdfFile + pdfFile?: PdfFile | null pdfUrl?: string pdfViewer?: string position?: PdfScrollPosition @@ -167,7 +167,7 @@ export const LocalCompileProvider: FC = ({ const { pdfViewer, syntaxValidation } = userSettings // low level details for metrics - const [pdfFile, setPdfFile] = useState() + const [pdfFile, setPdfFile] = useState() // the project is considered to be "uncompiled" if a doc has changed, or finished saving, since the last compile started. const [uncompiled, setUncompiled] = useState(false) @@ -296,7 +296,7 @@ export const LocalCompileProvider: FC = ({ }, [compiling]) const _buildLogEntryAnnotations = useCallback( - (entries: any) => + (entries: LogEntry[]) => buildLogEntryAnnotations(entries, fileTreeData, lastCompileRootDocId), [fileTreeData, lastCompileRootDocId] ) diff --git a/services/web/types/annotation.ts b/services/web/types/annotation.ts index e8727476bf..889eb5af59 100644 --- a/services/web/types/annotation.ts +++ b/services/web/types/annotation.ts @@ -6,6 +6,6 @@ export type Annotation = { ruleId?: string id: string entryIndex: number - firstOnLine: boolean + firstOnLine?: boolean command?: string }