Add CSS to STACK JS questions rendering - #3044
Conversation
vle_reset_question_registry() clears every iframe belonging to a question boundary, which is only safe when the whole question body is about to be re-rendered (send()). validate() and answer() only patch a single validation span or the score/feedback elements, so calling it there was orphaning any other live iframe in the same question (e.g. a Parsons/drag-and-drop/JSXgraph widget) from the postMessage system, since its registry entries were deleted without the iframe actually being recreated.
Adds a STACK question's id attribute to the <div> HTML output element. createQuestionBlocks() generated qprefix as "q" + positional index, discarding the id the XSL already assigns to every "que stack" div (from @label/@xml:id, required at build time). Using the existing id keeps the prefix stable and traceable to the source exercise instead of depending on page order.
create_iframe() added allow-same-origin to the sandbox for "non-evil"
iframes to (supposedly) make <base href> resolve relative URLs. Since
these are srcdoc iframes, allow-same-origin instead makes them
same-origin with the embedding textbook page itself (not the STACK
API), and combined with allow-scripts that gives iframe content full
read/write access to our DOM, cookies, etc -- a complete sandbox
escape.
The Moodle source this file was ported from explicitly warns against
this on the same line ("UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!
That would defeat the whole point of this.") and never included it.
The <base href> injection this was meant to support already existed
and worked before allow-same-origin was added, so it was unnecessary.
The STACK API restarts its iframe-id numbering on every render/
validate/grade call, so two different STACK questions on the same
page can easily get iframes with the identical API-assigned id (e.g.
both "0"), and the served iframe content self-reports that same id in
its postMessage traffic regardless of what DOM id we give it.
The old code used that raw id directly as the key into the global
IFRAMES/IFRAME_TO_BOUNDARY registries, so activating a second question
silently overwrote the first question's registry entry. Going back to
the first question and interacting with its (still live) iframe then
routed its messages to the second question's boundary instead -- e.g.
dragging pieces in one Parsons question after visiting another would
write the answer into the wrong question's hidden input, leaving the
original question empty at submit time ("Please enter valid answers
for all parts of the question").
Fix: identify the sender of each postMessage via e.source (the actual
window that sent it, which the browser guarantees is unique) instead
of the API-supplied msg.src. create_iframe() now mints its own
collision-proof key for internal bookkeeping, keeps the API's raw id
around separately (IFRAME_RAW_ID) since the iframe's own script still
needs to see it echoed back as "tgt", and a WeakMap resolves incoming
messages' e.source back to that key.
The STACK API accepts a question definition as either a Moodle XML quiz bank or a single-question yaml file, but stackapicalls.js only ever tried to DOMParser-parse the fetched qfile as XML -- a .yaml source would fail outright. getQuestionFile() now dispatches on the qfile extension. XML keeps the existing DOM-based extraction (loadQuestionFromXml, renamed from loadQuestionFromFile). For yaml, a STACK yaml file holds exactly one question and the API accepts its raw contents as questionDefinition directly (confirmed against the live API) -- so loadQuestionFromYaml only needs to pull a random variant out of the file's own deployedseed list (block or flow style), matching the random-variant selection already done for XML's "show new example question". Also tightens collectData()'s "did we get a question" check from a string-sentinel comparison to a straightforward truthiness check on the new, format-agnostic questionDefinition field, which incidentally fixes the pre-existing empty-qfile (managed-directories) case sending a request body with questionDefinition: undefined.
…m one Dropdown (and other permutation-dependent) inputs derive their option order from random_permutation(), which is seeded per-request. validate() called collectData() without fixing the seed, so it silently re-picked a new random deployed seed on every keystroke, reinterpreting the submitted option index against a different permutation and showing the wrong option/answer note in the validation feedback. answer() already guarded against this by overwriting data.seed with the stored render seed; validate() now does the same.
The STACK API now proxies plots/files through plot.php instead of linking directly into the plots directory, so a submitted file can't be accessed/run directly. Request the new path by default and fall back to the old direct link if it 404s, since older API deployments don't have plot.php yet. This affects the JS client and static version generation in _stack_download_assets.
js/dist/pretext-stack/*.js are pre-built artifacts produced by script/jsbuilder/ from js/pretext-stack/*.js, and the HTML output loads the dist copies (per $html.js.dir), not the source files directly. The committed dist files predated every STACK JS fix made this session (seed registry, qprefix derivation, sandbox escape, cross-question iframe routing, yaml support, validate() seed pinning), so none of those fixes were actually reachable in a build until now. Regenerated via `npm run build` in script/jsbuilder/.
Previously only a PDF was kept, so an EPUB (which can't display PDF) fell back to a text placeholder for any STACK exercise with a plot. Save the SVG as received and derive both a PDF (LaTeX/print) and a PNG (Kindle) from it, so every output route gets its preferred format with no fallback needed. Fixes PreTeXtBook#3033.
Adds js/pretext-stack/stack.css, styling STACK question input fields and feedback to match the appearance of the STACK API's own sample page (stack-api.maths.ed.ac.uk/sample.php). Sourced from two places: - github.com/maths/moodle-qtype_stack's api/public/api.css, and the real styles.css it proxies via cors.php (confirmed live against the API): matrix input brackets (.matrixsquarebrackets etc, verified via a live /render call), validation/feedback boxes, and other input-field rules. These already match the class names stackapicalls.js assigns (.feedback, .validation, .correct-answer, .stackinputfeedback.empty). - Our own rules for the per-part .correct/.incorrect/.partiallycorrect outcome boxes (confirmed via a live /grade call that the API emits these classes but ships no colour for them -- that normally comes from Moodle's own theme, which isn't available here): red/green/ orange colouring, a big check/cross/± icon at the start of each box, and a forced line break before/after via `display: block; width: fit-content` so they read as their own line without stretching full width. All rules are scoped under .que.stack to match our question container and avoid leaking onto unrelated page content. The stylesheet lives alongside the STACK JS files rather than being generated by the XSL (as e.g. ol-markers.css is): copy_html_js() in pretext/lib/pretext.py copies the whole js/ directory verbatim into _static/pretext/js/ regardless of extension, so it's picked up the same way as stackapicalls.js/stackjsvle.js with no build changes needed. xsl/pretext-html.xsl just gets a <link> to it in the existing stack-js template.
The jsbuilder pipeline (script/jsbuilder/) only bundles JS into js/dist/; stack.css was never a target and ships verbatim via copy_html_js() at the js/ root instead. The stack-js template linked it via $html.js.dir (the .../js/dist/ path) like the JS files, so the stylesheet 404'd and no STACK styling was ever applied in a real build. Point the <link> at $html.js.root instead, matching where copy_html_js() actually puts it.
|
Thanks — the two new commits are small and clean, and I've checked the path fix works. I'm holding this one until #3043 lands. As it stands the branch sits on the older #3043, so its diff still carries What would help: a force-push carrying just the two new commits — On where the CSS should live: the stylesheet hardcodes around thirty colour literals, and six of the shipped themes set On copyright and provenance: One last thing, unrelated to your changes but it will bite anyone testing this: Claude Opus 5, acting as a review assistant for Rob Beezer |
|
We have worked long and hard to put in a comprehensive system for CSS. So I am reluctant to add some non-conforming CSS now on a "temporary" basis. I think we should take the time now to do it right. @ascholerChemeketa - you could visually look at the last two commits here, that is the only new part, and offer your opinion on the best approach. Perhaps when you are back to active development, you would be able to help the STACK folks chart a way through? Somehow, I missed the new work on #3043 - sorry. I'll go there shortly, so take the above AI review in light of that. |
|
Where/how: There is a tradeoff between "extra" CSS files and building everything into the For the limited amount of CSS here, I think bundling it makes sense. You could add a Colors: Either all of the colors play well with dark and light mode (preferrable), or the entire interactive should be rendered in light mode regardless of the overall page (by forcibly setting all background/foreground colors). If you want to play well with the existing themes in dark/light mode, "body-text-color": #000,Will get rendered into a CSS variable That will be set to the appropriate value in dark/light mode. There are other variables like When setting colors for things like :root.dark-mode {
...
}To set dark specific versions of CSS rules or variables that will be used when in dark mode. I'm back as of yesterday, so feel free to ping me for follow ups. |
This makes inputs and feedback in interactive STACK questions stand out a bit more and look prettier.
This PR builds on top of #3043 (only the last two commits are new), but I've made this into a separate PR as I'm not sure where exactly to ship the CSS. Maybe it should be integrated into the whole theming framework, but this seems like a bigger task, so maybe STACK-specific (theme agnostic) CSS is a decent temporary solution?