Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
_build/
.myst.session.db
94 changes: 94 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# `web/` — MyST rendering (MIC-84 vertical slice)

A [mystmd](https://mystmd.org) site that renders the book, replacing the legacy
Sphinx → `.fjson` → re-parse-into-React (`nextgen`) pipeline. This directory is
the **vertical slice** from [MIC-125](https://linear.app/mtiller/issue/MIC-125):
one chapter (`content/first-order.md`) converted end-to-end to prove the
pipeline before bulk conversion.

## Quick start

```bash
cd web
npm install
npm start # dev server at http://localhost:3000
npm run build # static HTML into _build/html
```

Node 20+ (CI uses 26).

## Layout

| Path | Purpose |
|------|---------|
| `myst.yml` | Project + site config; registers the figure plugin |
| `content/first-order.md` | The converted chapter (was `text/source/behavior/equations/first_order.rst`) |
| `content/_plots/*.svg` | Static plots. In the real pipeline these come from the specs DSL → DVC → matplotlib chain (MIC-87). For the slice they were fetched from the live site. |
| `public/cases/<id>.json` | **Structured figure contract** (model name, stopTime, tol, ncp, mods, vars) — emitted by `_generate_casedata` (MIC-87). |
| `plugins/mbe-figure.mjs` | Custom `mbe-figure` directive (see below) |
| `public/mbe-island.{js,css}` | Client-side interactive island — ready but not yet wired (see "next step") |

## What the slice proves (working today)

`npm run build` produces a static site containing the chapter with:

- prose, headings, cross-references, admonitions;
- math via KaTeX (`$...$`, `$$...$$`);
- Modelica source via `{literalinclude}` (with `:lines:` / `:emphasize-lines:`);
- all four static plots rendered through the `mbe-figure` directive.

### The `mbe-figure` directive

Replaces the legacy `.. plot::` + `:class: interactive` + `nextgen`
`interactiveInjector`. That injector recovered a model id by string-slicing an
image `src` (`src.slice(16, src.length-4)`). Here the id is explicit and the
figure is backed by the **structured contract** in `public/cases/<id>.json` —
the same data the old build already generated and then discarded.

```markdown
```{mbe-figure} FO
:interactive:
:caption: Simulation of FirstOrder
```
```

**Byte-verify done:** the directive's id maps to the case `name` exactly as the
specs DSL declares —
`FO → ModelicaByExample.BasicEquations.SimpleExample.FirstOrder`,
`FOE → …FirstOrderExperiment` (`text/specs.py:13,24`). The new contract is
provably correct.

## Interactive island (next step — needs a theme component)

The "adjust parameters" island is **not yet hydrated in the built site.**
mystmd's book-theme sanitizes raw HTML (strips `data-*` and `<script>`) and does
not auto-serve a `public/` dir, so JS + data can't be drop-injected. The
supported path is a **custom mystmd theme component** registered for a figure
node, which can bundle its JS and receive the contract as props.

Groundwork already in place for that component:

- interactive figures are tagged `mbe-figure interactive mbe-plot-<id>` (class
survives sanitization), so the component/script can find them and read the id;
- `public/cases/<id>.json` holds the contract;
- `public/mbe-island.{js,css}` implements the progressive-enhancement panel
(static plot stays as the base case; panel shows model + experiment params;
"Re-run" disabled pending the live sim API, MIC-86).

This step is intentionally paired with Jony's figure-interaction design.

## Adding another chapter

1. Convert `text/source/<...>.rst` → `content/<name>.md` (`rst-to-myst` + cleanup
of directives: `.. plot::` → `{mbe-figure}`, `:ref:` → `[](#target)`,
`.. topic::` → `{admonition}`).
2. Add the chapter to `toc:` in `myst.yml`.
3. Drop its plots in `content/_plots/` and any interactive contracts in
`public/cases/`.

## Not in this slice (deferred)

- Bulk conversion of the other 91 chapters.
- i18n / gettext catalogs (MIC-92) — slice is English-only.
- Live simulation API (MIC-86).
- Final visual design (Jony); slice uses the default book-theme.
71 changes: 71 additions & 0 deletions web/SPIKE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Spike: how to render an interactive figure island in mystmd

**Question:** the interactive figure needs client-side behavior (read the case
contract, show editable params, eventually re-simulate). How does that plug into
a mystmd site? **Answer: two viable mechanisms, with a real tradeoff — a
decision is needed.** This spike establishes the landscape and demonstrates one
working path.

## What does NOT work (ruled out)

- **Raw HTML from the directive.** mystmd's book-theme sanitizes raw HTML: it
strips `data-*` attributes and `<script>` tags. An embedded `<div data-case>`
survives only as a bare `<div class>` (verified). So the contract can't ride
in as HTML attributes, and inline scripts vanish.
- **A config hook to add a page script.** The theme exposes only
`analytics_google` / `analytics_plausible` — no general "add script/head"
option. `allowDangerousHtml` exists but only inside mystmd's internal AST→HTML
libraries; it isn't a user knob and wouldn't execute scripts through the
theme's React render anyway.

What *does* survive sanitization: **class names**. So the directive encodes the
plot id as `mbe-plot-<id>` on the figure, and the contract is published as a
static file at `/cases/<id>.json`.

## Two viable mechanisms

### Option A — post-build injection (demonstrated here)

A Node step (`scripts/postbuild.mjs`) runs after `myst build --html`: it copies
the island assets into the built site and injects `<link>`/`<script>` into each
page *after* mystmd (bypassing sanitization). The island (`public/mbe-island.js`)
finds `.mbe-figure.interactive`, reads the id from the class, fetches
`/cases/<id>.json`, and renders the panel.

- ✅ Lightweight; no theme to maintain; wired into `npm run build` today.
- ✅ Verified end-to-end in the static build (tags injected, assets served,
figures targetable, contract fetchable).
- ⚠️ The built site hydrates as a React (Remix) app, so the island mounts after
`load` and appends to figure nodes React has settled. Mitigated with a
post-load + retry schedule; robust for content pages that don't re-render, but
it is enhancement-on-top-of-React, not part of React's tree.
- ⚠️ Does **not** run under `myst start` (dev preview) — only the built site.

### Option B — custom mystmd theme (robust, heavier)

Point `myst.yml` `site.template` at a **local theme** (mystmd supports a template
path) that extends `@myst-theme/book` and registers a React renderer for the
`mbe-figure` node. The island becomes part of React's tree.

- ✅ Idiomatic; robust against hydration; works in dev and build alike.
- ✅ Natural home for Jony's skeleton as a real component receiving the contract
as props.
- ⚠️ The published book-theme is a **pre-built Remix app** (no source in the
fetched template), so this means forking `myst-templates/book-theme`, adding
the component, and building/maintaining a theme package.

## Recommendation

- **For the slice / preview:** Option A. It's in place now and lets Michael see
a working island without a theme commitment.
- **For production:** Option B, *if* interactivity must work in dev and be fully
hydration-safe. The generic component (Jony's skeleton: `data-state` root,
no disclosure toggle, params from first paint, static-plot base + canvas
overlay, `data-skin`×`data-mode` tokens) is authored once and drops into
whichever mechanism is chosen — the skeleton is mechanism-agnostic.

**Decision needed from Michael:** ship the island via post-build (A) for now, or
invest in a local theme (B) for a dev-and-prod, hydration-safe component? The
current `mbe-island.js` is a mechanism-proof stub (still has a toggle; the real
build follows Jony's no-toggle skeleton) — intentionally minimal until the
mechanism is chosen.
Loading