Skip to content

Add CSS to STACK JS questions rendering - #3044

Open
geoo89 wants to merge 13 commits into
PreTeXtBook:masterfrom
IDEMSInternational:stack-css
Open

Add CSS to STACK JS questions rendering#3044
geoo89 wants to merge 13 commits into
PreTeXtBook:masterfrom
IDEMSInternational:stack-css

Conversation

@geoo89

@geoo89 geoo89 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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?

geoo89 and others added 13 commits July 15, 2026 16:07
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.
@rbeezer

rbeezer commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks — the two new commits are small and clean, and I've checked the path fix works. $html.js.root resolves to _static/pretext/js, copy_html_js() copies the whole js/ tree there, and js/dist/pretext-stack/ holds no stack.css, so the old link would have 404'd. A build of the stack/minimal example emits href="_static/pretext/js/pretext-stack/stack.css" with the file present at that path. The stylesheet is also scoped throughout under .que.stack and the link is guarded on $b-has-stack, both of which I appreciate.

I'm holding this one until #3043 lands. As it stands the branch sits on the older #3043, so its diff still carries js/dist/pretext-stack/stackjsvle.js — the file you took out of #3043 at my request — and it predates the radio-group, vle_get_element and download() fixes you pushed on the 28th.

What would help: a force-push carrying just the two new commits — STACK: add CSS for matrix brackets and feedback/correctness boxes and STACK: fix stack.css link pointing at the JS dist path. Rebase onto the current #3043 head if you'd like this looked at sooner, or wait until #3043 merges and rebase onto master. Either way this PR then becomes a 224-line, two-file change and is easy to take.

On where the CSS should live: the stylesheet hardcodes around thirty colour literals, and six of the shipped themes set --body-text-color: #f2f2f2, meaning dark backgrounds. On those themes the boxes render as black text on white inside a dark page.

On copyright and provenance: js/pretext-stack/stack.css carries no copyright line or license statement, and it is adapted from moodle-qtype_stack and Moodle core, both GPL v3. The opening comment does name the sources. This is the second file in that directory in that position — stackjsvle.js has a copyright line but no license and no README. I have a copyright and licensing pass touching notices across the repository that I'll be pushing later today.

One last thing, unrelated to your changes but it will bite anyone testing this: examples/stack/minimal will not build from a clean checkout, because its publication file names a generated directory that isn't shipped. It is the same problem you fixed in #3043 for examples/stack/features/, with the same remedy — a .gitkeep so git tracks the empty directory. It's on master, not in this PR.

Claude Opus 5, acting as a review assistant for Rob Beezer

@rbeezer

rbeezer commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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.

@ascholerChemeketa

Copy link
Copy Markdown
Contributor

Where/how:

There is a tradeoff between "extra" CSS files and building everything into the theme.css file. We currently do not have the theme builder do any inspection of pub variables or content to decide what to build into the theme for a book. So building stack CSS into the theme means it is cruft in any book that does not use stack (same is true of every other chunk of CSS). But, adding a link to a standalone file means that special handling might be needed in situations like Runestone grader, single file builds, etc...

For the limited amount of CSS here, I think bundling it makes sense.

You could add a _stack.scss to css\components\interactives and then @use that file from _pretext-web.scss to include it in the appropriate bundles.

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, css\colors\_color-vars defines a list of colors that are rendered into CSS variables. Something like:

"body-text-color": #000,

Will get rendered into a CSS variable --body-text-color

That will be set to the appropriate value in dark/light mode. There are other variables like --content-background and --page-border-color.

When setting colors for things like .que.stack .feedback, you likely need to set a light version and a dark version so you maintain proper contrast in both modes. Use can use:

: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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants