diff --git a/services/clsi/app/js/QuartoRunner.js b/services/clsi/app/js/QuartoRunner.js index e3ea8af972..268a941872 100644 --- a/services/clsi/app/js/QuartoRunner.js +++ b/services/clsi/app/js/QuartoRunner.js @@ -42,7 +42,9 @@ function runQuarto(compileName, options, callback) { // error object; merge it so _writeLogOutput can persist it. const combined = output || (error ? { stdout: error.stdout || '' } : null) _writeLogOutput(compileName, directory, combined, () => - callback(null, combined) + _appendMissingResourceWarnings(directory, () => + callback(null, combined) + ) ) } ) @@ -96,6 +98,65 @@ function _writeLogOutput(compileName, directory, output, callback) { }) } +// Quarto's HTML/RevealJS output is NOT self-contained (we deliberately dropped +// --embed-resources so reveal plugins like chalkboard work). A side effect is +// that pandoc no longer tries to fetch referenced media, so a missing image or +// video produces no compile-time warning — it just renders broken in the +// browser. To restore that feedback, scan the produced output.html for local +// media references and emit a [WARNING] for any that don't exist on disk. The +// [WARNING] prefix is understood by the Quarto/Typst log parser on the web +// side, so these surface in the Warnings tab like any other. +// +// Only HTML output is scanned: PDF output (Typst) already hard-errors on a +// missing image, so it needs no extra check. +function _appendMissingResourceWarnings(directory, callback) { + const htmlFile = Path.join(directory, 'output.html') + fs.readFile(htmlFile, 'utf8', (err, html) => { + if (err) return callback() // no HTML output (e.g. a PDF compile) + + const missing = _extractLocalMediaRefs(html).filter(ref => { + try { + return !fs.existsSync(Path.join(directory, decodeURIComponent(ref))) + } catch { + return false + } + }) + if (missing.length === 0) return callback() + + const warnings = + missing + .map( + ref => + `[WARNING] Missing resource: ${ref} (referenced in the document ` + + `but not found in the project — it will appear broken)` + ) + .join('\n') + '\n' + fs.appendFile(Path.join(directory, 'output.log'), '\n' + warnings, () => + callback() + ) + }) +} + +// Pull local media references (img/video/audio/iframe src, poster, RevealJS +// data-background-*) out of the rendered HTML. External URLs, data URIs and +// in-page anchors are ignored; Quarto's own generated assets (under +// _files/) exist on disk, so they never get flagged. +function _extractLocalMediaRefs(html) { + const refs = new Set() + const attrRegex = + /(?:src|poster|data-background-image|data-background-video)\s*=\s*["']([^"']+)["']/gi + let match + while ((match = attrRegex.exec(html)) !== null) { + const url = match[1].trim() + if (!url) continue + // Skip absolute URLs, protocol-relative, data/blob URIs and anchors. + if (/^(?:[a-z]+:|\/\/|\/|#|data:|blob:)/i.test(url)) continue + const clean = url.split(/[?#]/)[0] // drop query string / fragment + if (clean) refs.add(clean) + } + return [...refs] +} + function isRunning(compileName) { return ProcessTable[compileName] != null }