From 8f129bfd6a5cc933aefe10e45015ee57a3d0f4f9 Mon Sep 17 00:00:00 2001 From: "Michael M. Tiller" Date: Wed, 1 Jul 2026 12:37:39 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20mystmd=20vertical=20slice=20?= =?UTF-8?q?=E2=80=94=20first-order=20chapter=20(MIC-125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the MIC-84 decision (mystmd) as a one-chapter vertical slice in a new /web directory, standing alongside the existing Sphinx/nextgen build without touching it. Working end-to-end (npm run build → static HTML): - first_order.rst converted to MyST Markdown (content/first-order.md): prose, headings, cross-refs, admonitions, KaTeX math, and Modelica source via {literalinclude} (:lines:/:emphasize-lines:). - Custom {mbe-figure} directive (plugins/mbe-figure.mjs) renders all four static plots and replaces the legacy .. plot:: + :class: interactive + nextgen interactiveInjector (which recovered the model id by slicing an image src). Figures are backed by the structured contract in public/cases/.json — the data _generate_casedata already emits. - Byte-verified: FO -> ...SimpleExample.FirstOrder, FOE -> ...FirstOrderExperiment (matches text/specs.py). The new data contract is provably correct. Deferred (documented in web/README.md): - Interactive island hydration: mystmd sanitizes raw HTML and doesn't auto-serve public/, so it needs a theme-component hook. Groundwork is in place (sanitizer-safe mbe-plot- class marker, contract JSON, public/mbe-island.{js,css}); paired with Jony's figure-interaction design. - Bulk chapter conversion, i18n (MIC-92), live sim API (MIC-86), final design. Co-Authored-By: Claude Opus 4.8 --- web/.gitignore | 3 + web/README.md | 94 +++ web/content/_plots/FO.svg | 930 ++++++++++++++++++++++++++ web/content/_plots/FOE.svg | 1075 ++++++++++++++++++++++++++++++ web/content/_plots/FOI.svg | 1262 ++++++++++++++++++++++++++++++++++++ web/content/_plots/FOS.svg | 1121 ++++++++++++++++++++++++++++++++ web/content/first-order.md | 215 ++++++ web/myst.yml | 14 + web/package-lock.json | 25 + web/package.json | 14 + web/plugins/mbe-figure.mjs | 87 +++ web/public/cases/FO.json | 18 + web/public/cases/FOE.json | 18 + web/public/mbe-island.css | 68 ++ web/public/mbe-island.js | 101 +++ 15 files changed, 5045 insertions(+) create mode 100644 web/.gitignore create mode 100644 web/README.md create mode 100644 web/content/_plots/FO.svg create mode 100644 web/content/_plots/FOE.svg create mode 100644 web/content/_plots/FOI.svg create mode 100644 web/content/_plots/FOS.svg create mode 100644 web/content/first-order.md create mode 100644 web/myst.yml create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/plugins/mbe-figure.mjs create mode 100644 web/public/cases/FO.json create mode 100644 web/public/cases/FOE.json create mode 100644 web/public/mbe-island.css create mode 100644 web/public/mbe-island.js diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 00000000..5741e49e --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +_build/ +.myst.session.db diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..fd07ac3f --- /dev/null +++ b/web/README.md @@ -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/.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/.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 `'; + +function injectInto(file) { + let html = fs.readFileSync(file, 'utf-8'); + if (html.includes('/mbe-island.js')) return false; // already injected + if (html.includes('')) { + html = html.replace('', `${TAGS}`); + } else { + html = html + TAGS; + } + fs.writeFileSync(file, html); + return true; +} + +function walkHtml(dir) { + let n = 0; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) n += walkHtml(p); + else if (entry.name.endsWith('.html')) n += injectInto(p) ? 1 : 0; + } + return n; +} + +const injected = walkHtml(HTML_DIR); +console.log(`postbuild: island assets copied; injected tags into ${injected} page(s).`);