|
|
|
|
@@ -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) {
|