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).`);