Skip to content

strawman attempt at a static-first runestone multiplechoice interactive - #3163

Draft
StevenClontz wants to merge 2 commits into
PreTeXtBook:masterfrom
StevenClontz:StevenClontz/20260818/static-first-runestone
Draft

strawman attempt at a static-first runestone multiplechoice interactive#3163
StevenClontz wants to merge 2 commits into
PreTeXtBook:masterfrom
StevenClontz:StevenClontz/20260818/static-first-runestone

Conversation

@StevenClontz

@StevenClontz StevenClontz commented Aug 19, 2026

Copy link
Copy Markdown
Member

WIP draft PR based upon discussions with @oscarlevin in mathtech discord.

Following is the LLM summary of this proposal. I will review and update before marking as ready to review by others. A known deficiency as of this edit: this only touches exercises with choices, but there are several other semantically-described <exercise/>s that should have improved print-ready HTML for use in worksheet/handout print previews.

Static-first, JS-hydrated multiple-choice exercises

Summary

A <worksheet> is meant to be printed, but a multiple-choice <exercise>
(one with a <choices> block) was rendered straight to a Runestone-JS-only
skeleton — a bare <ul data-component="multiplechoice"> of answer/feedback
<li>s that only became a real question once Runestone's JS enhanced it at
runtime. Printing a worksheet goes through a client-side-only "print
preview" pass that swaps stylesheets and reflows content, but never touches
the DOM inside an exercise, and there's no XSLT-time "print mode" to branch
on, since screen and print share one DOM. The result: a printed worksheet
either showed nothing where the question belonged, or leaked the raw,
unstyled skeleton.

This PR reworks multiple-choice rendering, site-wide, into a static-first,
progressively-enhanced
pattern:

  • XSLT now always emits genuine, readable HTML — a statement plus a lettered
    list of choices — annotated with just enough data (data-correct, a
    .choice-feedback block) to rebuild the interactive widget later.
  • On a normal page load, JS reads that static HTML, rebuilds the exact
    data-component skeleton Runestone expects, and explicitly hands it to
    Runestone via window.runestoneComponents.renderOneComponent() — the same
    entry point knowl.js already uses for interactives injected after page
    load, since Runestone's own automatic scan only ever sees markup that
    existed when it ran.
  • On a print-preview page, or for any reader without JS, that hydration step
    never runs, so the plain static HTML is simply what's shown.

Worksheets get a working printed/no-JS fallback as the direct result of a
general fix, rather than a worksheet-specific special case — and every
multiple-choice exercise anywhere in a book now degrades gracefully without
JS, for free.

Before / after

Regular (interactive) view — unchanged from a reader's perspective:
Runestone's full widget, radio buttons, and "Check Me" button.

download

Print preview view — previously a dead or broken skeleton; now a clean,
readable, lettered list:

download

How it works

Before After
XSLT output <ul data-component="multiplechoice"> skeleton, unusable without JS Real <statement> + lettered <ol> of choices, readable as-is
Interactivity Runestone's automatic page-load scan enhanced the skeleton in place JS rebuilds the skeleton from the static HTML, then explicitly calls renderOneComponent()
Print preview No special handling — showed the broken skeleton Hydration is skipped entirely; static HTML is what prints
No-JS fallback Broken, unstyled skeleton with feedback text exposed Same readable static HTML as print preview

Files changed

  • xsl/pretext-runestone.xsl — the multiple-choice templates now
    hand-author static HTML (statement, lettered <ol> of choices,
    data-correct, a .choice-feedback block gated by the same
    $b-has-solution switch used everywhere else) instead of the
    JS-dependent skeleton.
  • xsl/pretext-html.xsl — forwards the already-computed
    $b-has-solution flag into the template above.
  • js/pretext_add_on.js (+ regenerated js/dist/pretext-core.js) —
    new hydrateMultipleChoice(), called from the existing DOMContentLoaded
    handler whenever the page isn't a print preview; rebuilds the interactive
    skeleton in place and hands it to Runestone via renderOneComponent().

Testing

Verified end-to-end against a real build (Saxon + the modified core, served
locally) with a headless-browser check:

  • Interactive page: hydration runs, Runestone fully initializes the
    widget (class="... runestone-component-ready"), radio buttons render,
    MathJax typesets, and clicking an answer + "Check Me" correctly reveals
    the matching feedback ("✔️ Correct, 7 is prime.").
  • Print preview page (?printpreview=<id>): hydration is skipped, no
    data-component anywhere, no radio buttons — just the plain lettered
    list.
  • JS disabled: same plain, readable list as print preview — no blank or
    broken output.
  • Solution-visibility publisher switches (common/exercise-worksheet,
    exercise-divisional, etc.) correctly gate whether .choice-feedback
    starts hidden, reusing the existing mechanism rather than inventing a new
    one.
  • Zero console/page errors in either view.

Scope

Multiple-choice (choices) only, matching the original ask. Other
Runestone interactivity types (truefalse, parson, matching, etc.) are
unchanged and out of scope for this PR.

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.

1 participant