diff --git a/services/clsi/app/js/QuartoRunner.js b/services/clsi/app/js/QuartoRunner.js index ebade99ca7..e3ea8af972 100644 --- a/services/clsi/app/js/QuartoRunner.js +++ b/services/clsi/app/js/QuartoRunner.js @@ -54,21 +54,28 @@ function _buildQuartoCommand(mainFile) { // // We do NOT pass --to or --output: let the YAML frontmatter decide the // output format (typst → output.pdf, revealjs → output.html, etc.). - // --embed-resources makes HTML output self-contained (ignored for typst). - // After render we rename the produced file to the canonical output name - // expected by the rest of the pipeline. + // + // We do NOT pass --embed-resources. A self-contained single-file HTML + // breaks reveal.js plugins that load/store resources at runtime (e.g. + // chalkboard, multiplex) and is slow to transfer. Instead Quarto emits + // the HTML plus a sibling "_files/" asset directory; the HTML + // references it with relative paths. Both the html and the asset dir are + // served from the same .../output/ path, so the relative links resolve. + // + // After render we rename the produced top-level file to output.pdf or + // output.html. The asset directory keeps its "_files" name; the + // renamed output.html still points at it via the unchanged relative refs. + // + // The extension merge (cp -rn, no-clobber so user extensions win) and the + // trailing semicolon (so a missing /opt/quarto-extensions doesn't abort) + // are kept. mv uses relative paths because LocalCommandRunner.replace() + // only substitutes the FIRST $COMPILE_DIR and the shell CWD is the dir. const inputPath = `$COMPILE_DIR/${mainFile}` const baseName = mainFile.replace(/\.[^/.]+$/, '') // strip extension - // LocalCommandRunner.replace() only replaces the FIRST $COMPILE_DIR - // occurrence in the shell string, so the mv commands use relative paths - // instead — the shell CWD is already set to the compile directory. - // Merge pre-installed extensions into the compile dir before rendering. - // -n (no-clobber) ensures project-uploaded extensions take precedence. - // The semicolon means a missing /opt/quarto-extensions dir doesn't abort. const cmd = `mkdir -p _extensions && ` + `cp -rn /opt/quarto-extensions/_extensions/. _extensions/ 2>/dev/null; ` + - `quarto render ${inputPath} --embed-resources 2>&1 && ` + + `quarto render ${inputPath} 2>&1 && ` + `(mv ${baseName}.pdf output.pdf 2>/dev/null || ` + `mv ${baseName}.html output.html 2>/dev/null)` return ['/bin/sh', '-c', cmd] 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 87bfc642d1..f135e083ab 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -505,6 +505,15 @@ export const LocalCompileProvider: FC = ({ // set the PDF context if (data.status === 'success') { setPdfFile(handleOutputFiles(outputFiles, projectId, data)) + } else { + // For PDF output we intentionally keep the last good PDF visible + // next to the error log (standard Overleaf behaviour). But for an + // HTML deck rendered in an iframe, leaving the previous deck up is + // confusing — it looks like the failed compile succeeded. So drop + // a stale HTML output on any non-success status. + setPdfFile(prev => + prev?.path === 'output.html' ? undefined : prev + ) } setFileList(buildFileList(outputFiles, data))