Fix presentation export: Quarto -M uses colon syntax, harden decktape
Build and Deploy Verso / deploy (push) Successful in 8m13s
Build and Deploy Verso / deploy (push) Successful in 8m13s
The standalone-HTML export produced a non-self-contained file (no slide CSS/JS, math or images when opened away from the server) because Quarto's --metadata/-M flag uses KEY:VALUE (colon), not KEY=VALUE. '-M embed-resources=true' silently registered a bogus key and left embed-resources unset. Switch to colon syntax and also embed MathJax (self-contained-math:true) so equations render offline. For the slide PDF, add --disable-dev-shm-usage (the usual cause of Chromium crashing inside a container with a small /dev/shm), and have the export controller return the compile log as text/plain on failure so a failed PDF export shows the real decktape/Chromium error instead of an HTML page the browser saves as 'pdf.htm'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -82,9 +82,18 @@ function _buildQuartoCommand(mainFile, exportMode) {
|
||||
// deck compiles and is fully portable/offline.
|
||||
// - 'pdf-slides': render the deck, then print it to a faithful slide PDF
|
||||
// with decktape (headless Chromium), one slide per page.
|
||||
// NB: Quarto's --metadata/-M flag uses COLON syntax (KEY:VALUE), not "=".
|
||||
// Using "=" makes Quarto register a bogus key literally named
|
||||
// "embed-resources=true" and silently leave the real option unset, so the
|
||||
// deck stays non-self-contained (references a sibling "<base>_files/" dir).
|
||||
// - embed-resources: inline CSS/JS/images into the single .html
|
||||
// - self-contained-math: also inline MathJax (not embedded by default),
|
||||
// so equations render in a fully offline/portable file
|
||||
// - chalkboard: must be off — it is incompatible with embed-resources and
|
||||
// errors the render if left enabled
|
||||
const renderFlags =
|
||||
exportMode === 'html-standalone'
|
||||
? ' -M embed-resources=true -M chalkboard=false'
|
||||
? ' -M embed-resources:true -M self-contained-math:true -M chalkboard:false'
|
||||
: ''
|
||||
|
||||
let tail =
|
||||
@@ -94,8 +103,11 @@ function _buildQuartoCommand(mainFile, exportMode) {
|
||||
if (exportMode === 'pdf-slides') {
|
||||
// After producing output.html, print it to output-slides.pdf. --no-sandbox
|
||||
// is required for Chromium running as a non-root user inside the container.
|
||||
// --no-sandbox: Chromium can't sandbox as a non-root container user.
|
||||
// --disable-dev-shm-usage: containers usually have a tiny /dev/shm, which
|
||||
// makes Chromium crash mid-render; this routes its shared memory to /tmp.
|
||||
tail +=
|
||||
` && decktape --chrome-arg=--no-sandbox "$(pwd)/output.html" output-slides.pdf 2>&1`
|
||||
` && decktape --chrome-arg=--no-sandbox --chrome-arg=--disable-dev-shm-usage "$(pwd)/output.html" output-slides.pdf 2>&1`
|
||||
}
|
||||
|
||||
const cmd =
|
||||
|
||||
@@ -27,6 +27,29 @@ const FORMATS = {
|
||||
},
|
||||
}
|
||||
|
||||
// Best-effort fetch of the export compile's output.log so export failures can
|
||||
// show the underlying error (e.g. decktape/Chromium output). Never throws.
|
||||
async function _fetchLog(projectId, userId, clsiServerId, buildId) {
|
||||
if (!buildId) return ''
|
||||
try {
|
||||
const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId
|
||||
const stream = await ClsiManager.promises.getOutputFileStream(
|
||||
projectId,
|
||||
compileAsUser,
|
||||
clsiServerId,
|
||||
buildId,
|
||||
'output.log'
|
||||
)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) chunks.push(chunk)
|
||||
const text = Buffer.concat(chunks).toString('utf8')
|
||||
// Keep it bounded — the tail holds the relevant error.
|
||||
return text.length > 8000 ? text.slice(-8000) : text
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function exportPresentation(req, res) {
|
||||
const projectId = req.params.Project_id
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
@@ -41,12 +64,18 @@ async function exportPresentation(req, res) {
|
||||
})
|
||||
|
||||
if (!buildId || !outputFiles?.some(f => f.path === format.file)) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
`Export failed: the project did not produce ${format.file} (compile status: ${status}). ` +
|
||||
`This export is only available for RevealJS presentations.`
|
||||
)
|
||||
// The expected artefact wasn't produced. Surface the compile log (which
|
||||
// for the PDF path includes decktape/Chromium's own stderr) as plain
|
||||
// text so the failure is diagnosable, instead of returning an HTML page
|
||||
// the browser saves as "pdf.htm".
|
||||
const log = await _fetchLog(projectId, userId, clsiServerId, buildId)
|
||||
res.status(400).type('text/plain')
|
||||
return res.send(
|
||||
`Export failed: the project did not produce ${format.file} ` +
|
||||
`(compile status: ${status}). This export is only available for ` +
|
||||
`RevealJS presentations.\n\n` +
|
||||
(log ? `--- compile log ---\n${log}` : '(no compile log available)')
|
||||
)
|
||||
}
|
||||
|
||||
const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId
|
||||
|
||||
Reference in New Issue
Block a user