Merge pull request #28675 from overleaf/dp-pdf-preview-output-files-typescript-2

Convert output-files.js to typescript

GitOrigin-RevId: 32eb509f491cfd53de7f1b21b97861ba421566a5
This commit is contained in:
David
2025-09-29 11:32:36 +01:00
committed by Copybot
parent 2bf48b3774
commit 1b5887d97f
6 changed files with 97 additions and 52 deletions

View File

@@ -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<string, PDFFile>,
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<string, PDFFile>,
data: CompileResponseData,
signal: AbortSignal
): Promise<LogResult> {
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<string, Annotation[]>}
*/
export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
const rootDocDirname = dirname(fileTreeData, rootDocId)
export function buildLogEntryAnnotations(
entries: LogEntry[],
fileTreeData: Folder,
rootDocId?: string | null
): Record<string, Annotation[]> {
const rootDocDirname = rootDocId ? dirname(fileTreeData, rootDocId) : null
const logEntryAnnotations = {}
const seenLine = {}
const logEntryAnnotations: Record<string, Annotation[]> = {}
const seenLine: Record<number, boolean> = {}
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<string, number> => {
const counts: Record<string, number> = {}
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<string, number>,
previousRuleCounts: Record<string, number>
): Record<string, number> => {
const counts: Record<string, number> = {}
// 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<string> {
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) {

View File

@@ -14,6 +14,7 @@ export type LogEntry = {
type?: string
messageComponent?: React.ReactNode
contentDetails?: string[]
command?: string
}
export type ErrorLevel =

View File

@@ -64,7 +64,7 @@ const parserReducer = function (maxErrors: number | null) {
}
}
type BibLogEntry = {
export type BibLogEntry = {
file: string
level: string
message: string

View File

@@ -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'

View File

@@ -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<string, Annotation[]>
outputFilesArchive?: string
pdfDownloadUrl?: string
pdfFile?: PdfFile
pdfFile?: PdfFile | null
pdfUrl?: string
pdfViewer?: string
position?: PdfScrollPosition
@@ -167,7 +167,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
const { pdfViewer, syntaxValidation } = userSettings
// low level details for metrics
const [pdfFile, setPdfFile] = useState<PdfFile | undefined>()
const [pdfFile, setPdfFile] = useState<PdfFile | null | undefined>()
// 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<React.PropsWithChildren> = ({
}, [compiling])
const _buildLogEntryAnnotations = useCallback(
(entries: any) =>
(entries: LogEntry[]) =>
buildLogEntryAnnotations(entries, fileTreeData, lastCompileRootDocId),
[fileTreeData, lastCompileRootDocId]
)

View File

@@ -6,6 +6,6 @@ export type Annotation = {
ruleId?: string
id: string
entryIndex: number
firstOnLine: boolean
firstOnLine?: boolean
command?: string
}