From 5c377c9c6684bc6b60c4f732c7fbdf31e2d4a8ee Mon Sep 17 00:00:00 2001 From: "Michael M. Tiller" Date: Wed, 1 Jul 2026 17:11:10 -0400 Subject: [PATCH 01/12] feat(web-astro): Astro + Starlight bake-off slice (MIC-126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the first-order chapter on Astro + Starlight, parallel to the mystmd slice (web/), to compare platforms for MIC-84. The decisive contrast is the interactive figure. In mystmd it can't be a drop-in component (no MDX; renderer plugin unimplemented; theme sanitizes raw HTML) so it needs a theme fork or post-build hack. Here it's a native MDX component: Verified in dist/first-order/index.html: - structured contract preserved as data-case on the figure (mystmd stripped it) — Astro doesn't sanitize component output; - island diff --git a/web-astro/src/content.config.ts b/web-astro/src/content.config.ts new file mode 100644 index 00000000..6a7b7a02 --- /dev/null +++ b/web-astro/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/web-astro/src/content/docs/first-order.mdx b/web-astro/src/content/docs/first-order.mdx new file mode 100644 index 00000000..be63a507 --- /dev/null +++ b/web-astro/src/content/docs/first-order.mdx @@ -0,0 +1,109 @@ +--- +title: Simple First Order System +description: First-order systems in Modelica (MIC-84 Astro+Starlight slice) +--- + +import SimFigure from '../../components/SimFigure.astro'; +import { Code } from '@astrojs/starlight/components'; +import firstOrder from '../../../../ModelicaByExample/BasicEquations/SimpleExample/FirstOrder.mo?raw'; +import firstOrderDocumented from '../../../../ModelicaByExample/BasicEquations/SimpleExample/FirstOrderDocumented.mo?raw'; +import firstOrderInitial from '../../../../ModelicaByExample/BasicEquations/SimpleExample/FirstOrderInitial.mo?raw'; +import firstOrderSteady from '../../../../ModelicaByExample/BasicEquations/SimpleExample/FirstOrderSteady.mo?raw'; +import firstOrderExperiment from '../../../../ModelicaByExample/BasicEquations/SimpleExample/FirstOrderExperiment.mo?raw'; + +export const body = (s) => s.split('\n').slice(1).join('\n').trimEnd(); // ":lines: 2-" + +Let us consider an extremely simple differential equation: + +$$\dot{x} = (1-x)$$ + +Looking at this equation, we see there is only one variable, $x$. This +equation can be represented in Modelica as follows: + + + +This code starts with the keyword `model` which is used to indicate the start +of the model definition. The `model` keyword is followed by the model name, +`FirstOrder`. This, in turn, is followed by a declaration of all the variables +we are interested in. + +Since the variable $x$ in our equation is clearly meant to be a continuous real +valued variable, its declaration in Modelica takes the form `Real x;`. The +`Real` type is just one of the types we can use. + +Once all the variables have been declared, we can begin including the equations +that describe the behavior of our model. In this case, we can use the `der` +operator to represent the time derivative of `x`. Thus, + +```text +der(x) = (1-x) +``` + +is equivalent to: + +$$\dot{x} = (1-x)$$ + +Unlike most programming languages, we don't approach code like this as a +"program" to be executed one instruction after another. Instead, we use a +Modelica compiler to transform this model into something we can simulate, +producing a solution trajectory like this: + + + +This gives you the first hint at one of the compelling aspects of using a +modeling language to describe mathematical behavior: we didn't need to describe +*how* to solve the differential equation. The focus is entirely on behavior. + +## Adding Some Documentation + +Now that we've solved this simple equation, let's make the model a bit more +readable. Consider the following model: + + + +The quoted text blocks are **not** comments — they are "descriptive strings" +that can only appear in specific places to document the elements they are +attached to. + +## Initialization + +Modelica lets us describe initialization as well as behavior. To make the +initial value of `x` be _2_, we add an `initial equation` section: + + + +The resulting trajectory is quite different: + + + +Mathematically this model is: + +$$\dot{x} = (1-x);\; x(0) = 2$$ + +We can also constrain the *derivative* at the start. To require $\dot{x}(0)=0$: + + + +Simulating this system gives: + + + +## Experimental Conditions + +A model developer can associate experimental conditions with a model using an +`annotation`. The `experiment` annotation stores things like start time, stop +time, and tolerance — information about *how* to simulate, not about behavior: + + + +The following trajectory was simulated using these conditions: + + + +The trajectory terminates at 8 seconds because the simulator used the +`experiment` annotation to determine how long to run the simulation. + +:::note[Annotation Support] +The `experiment` annotation is widely supported. But in general, a tool is free +to ignore any or all annotations. +::: diff --git a/web-astro/src/content/docs/index.mdx b/web-astro/src/content/docs/index.mdx new file mode 100644 index 00000000..9a6cdc5a --- /dev/null +++ b/web-astro/src/content/docs/index.mdx @@ -0,0 +1,11 @@ +--- +title: Modelica by Example +description: Astro + Starlight vertical slice (MIC-84 bake-off) +--- + +This is the **Astro + Starlight** vertical slice for the MIC-84 platform bake-off +(parallel to the mystmd slice in `../web`). + +See [Simple First Order System](/first-order/) for the converted chapter, which +exercises prose, math, code sourced from the `.mo` files, and the interactive +figure island. diff --git a/web-astro/src/styles/skins.css b/web-astro/src/styles/skins.css new file mode 100644 index 00000000..9663d00d --- /dev/null +++ b/web-astro/src/styles/skins.css @@ -0,0 +1,107 @@ +/* MIC-84 — interactive figure skins + structure. + * + * Tokens are Jony's verbatim specimen (A/B/C × light/dark), split into two + * orthogonal axes: data-skin (typography/shape/density) × data-mode (color). + * Set both on any ancestor (page or the figure root). Skin A / light is + * the fallthrough. Structural CSS below consumes only these tokens, so a skin + * swap never touches structure or JS. + * ------------------------------------------------------------------------- */ + +/* ---- Base defaults (skin A / light) ---- */ +:root { + --font-body: 'Source Serif 4', Georgia, serif; + --font-head: 'Inter', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --measure: 66ch; --radius: 6px; --control-radius: 6px; + --shadow: 0 1px 2px rgba(30,25,15,.07); + --bg: #fff; --surface: #fff; --ink: #111; --muted: #666; --rule: #ddd; + --accent: #2A6F97; --accent-ink: #fff; + --code-bg: #f4efe6; --code-ink: #2b2a26; --ckw: #2A6F97; --cst: #7a6b4f; --ccm: #9a927f; + --prey: #1f77b4; --pred: #ff7f0e; + --fig-pad: 16px; --param-row-gap: 8px; --line: 1.6; +} + +/* ============ SKIN A · Textbook ============ */ +[data-skin=a] { --font-body:'Source Serif 4',Georgia,serif; --font-head:'Inter',system-ui,sans-serif; --font-mono:'JetBrains Mono',monospace; + --measure:66ch; --radius:6px; --control-radius:6px; --fig-pad:16px; --param-row-gap:8px; --line:1.6; } +[data-skin=a][data-mode=light] { --bg:#FAF8F3; --surface:#fff; --ink:#1C1B19; --muted:#6B6558; --rule:#E4DDCE; + --accent:#2A6F97; --accent-ink:#fff; --code-bg:#F4EFE6; --code-ink:#2B2A26; --ckw:#1F6F97; --cst:#8a6d3b; --ccm:#a99f89; + --shadow:0 1px 2px rgba(30,25,15,.07); --prey:#1f77b4; --pred:#ef7d1a; } +[data-skin=a][data-mode=dark] { --bg:#201E1A; --surface:#29261F; --ink:#EDE7DA; --muted:#A79E8C; --rule:#3A362E; + --accent:#8FBEDC; --accent-ink:#12100c; --code-bg:#17150F; --code-ink:#E8E2D3; --ckw:#8FBEDC; --cst:#D8B98A; --ccm:#8a8271; + --shadow:0 1px 2px rgba(0,0,0,.4); --prey:#5FA8D3; --pred:#FF9E45; } + +/* ============ SKIN B · Docs ============ */ +[data-skin=b] { --font-body:'Inter',system-ui,sans-serif; --font-head:'Inter',system-ui,sans-serif; --font-mono:'JetBrains Mono',monospace; + --measure:72ch; --radius:10px; --control-radius:6px; --fig-pad:14px; --param-row-gap:7px; --line:1.55; } +[data-skin=b][data-mode=light] { --bg:#fff; --surface:#fff; --ink:#0F172A; --muted:#64748B; --rule:#E2E8F0; + --accent:#4F46E5; --accent-ink:#fff; --code-bg:#0D1117; --code-ink:#E6EDF3; --ckw:#79C0FF; --cst:#A5D6FF; --ccm:#8B949E; + --shadow:0 2px 8px rgba(15,23,42,.08); --prey:#2563eb; --pred:#f59e0b; } +[data-skin=b][data-mode=dark] { --bg:#0D1117; --surface:#161B22; --ink:#E6EDF3; --muted:#8B949E; --rule:#21262D; + --accent:#7C83FF; --accent-ink:#0b0e14; --code-bg:#010409; --code-ink:#E6EDF3; --ckw:#79C0FF; --cst:#A5D6FF; --ccm:#8B949E; + --shadow:0 2px 10px rgba(0,0,0,.4); --prey:#6BA6FF; --pred:#F5A742; } + +/* ============ SKIN C · Journal ============ */ +[data-skin=c] { --font-body:'Newsreader',Georgia,serif; --font-head:'Newsreader',Georgia,serif; --font-mono:'IBM Plex Mono',monospace; + --measure:60ch; --radius:2px; --control-radius:2px; --shadow:none; --fig-pad:14px; --param-row-gap:6px; --line:1.5; } +[data-skin=c][data-mode=light] { --bg:#fff; --surface:#fff; --ink:#101010; --muted:#565049; --rule:#DADADA; + --accent:#7B2D26; --accent-ink:#fff; --code-bg:#F7F5F2; --code-ink:#1a1a1a; --ckw:#7B2D26; --cst:#4d6b5a; --ccm:#9a9a9a; + --prey:#1f5c8b; --pred:#c56a1c; } +[data-skin=c][data-mode=dark] { --bg:#14110F; --surface:#1B1815; --ink:#ECE7E1; --muted:#A79E95; --rule:#322C28; + --accent:#C98A80; --accent-ink:#171210; --code-bg:#0E0C0A; --code-ink:#ECE7E1; --ckw:#C98A80; --cst:#9CBFA8; --ccm:#877f76; + --prey:#63A3CE; --pred:#E39A55; } + +/* ------------------------------------------------------------------------- */ +/* Structure — consumes only tokens above. */ +.mbe-figure { + margin: 1.75rem 0; + font-family: var(--font-body); +} +.mbe-figure__static { max-width: 100%; height: auto; display: block; margin: 0 auto; } +.mbe-figure__caption { font-family: var(--font-head); font-size: .9rem; color: var(--muted); margin-top: .5rem; text-align: center; } + +.mbe-figure.interactive { + border: 1px solid var(--rule); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow); + padding: var(--fig-pad); +} +.mbe-figure__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; } +.mbe-figure__label { font-family: var(--font-head); font-weight: 600; color: var(--ink); } +.mbe-figure__badge { font-family: var(--font-head); font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; + color: var(--accent-ink); background: var(--accent); border-radius: 999px; padding: .1rem .5rem; } + +/* Two-column by default; collapses to stacked at narrow width via container + * query on the figure itself — same DOM, no variant. */ +.mbe-figure__body { display: grid; grid-template-columns: 186px 1fr; gap: 1rem; align-items: start; } +.mbe-figure { container-type: inline-size; } +@container (max-width: 520px) { + .mbe-figure__body { grid-template-columns: 1fr; } +} + +.mbe-figure__params-title { font-family: var(--font-head); font-size: .8rem; font-weight: 600; color: var(--muted); + text-transform: uppercase; letter-spacing: .03em; margin-bottom: .5rem; } +.mbe-figure__param-list { display: flex; flex-direction: column; gap: var(--param-row-gap); } +.mbe-figure__param { display: flex; flex-direction: column; gap: .15rem; } +.mbe-param__label { font-size: .8rem; color: var(--muted); font-family: var(--font-mono); } +.mbe-param__input { font-family: var(--font-mono); font-size: .85rem; padding: .25rem .4rem; + border: 1px solid var(--rule); border-radius: var(--control-radius); background: var(--bg); color: var(--ink); } +.mbe-param__input.is-invalid { border-color: #c0392b; } + +.mbe-figure__param-list--readonly { display: grid; grid-template-columns: max-content 1fr; gap: .25rem .75rem; margin: 0; } +.mbe-figure__param-list--readonly dt { font-size: .8rem; color: var(--muted); font-family: var(--font-head); } +.mbe-figure__param-list--readonly dd { margin: 0; font-family: var(--font-mono); font-size: .82rem; color: var(--ink); } + +.mbe-figure__actions { margin-top: .6rem; display: flex; flex-direction: column; gap: .35rem; } +.mbe-figure__hint { font-size: .72rem; color: var(--muted); } +.mbe-figure__run { font-family: var(--font-head); font-size: .85rem; align-self: flex-start; + background: var(--accent); color: var(--accent-ink); border: none; border-radius: var(--control-radius); + padding: .35rem .8rem; cursor: pointer; } +.mbe-figure__run[disabled] { opacity: .5; cursor: not-allowed; } + +.mbe-figure__plot { position: relative; } +.mbe-figure__status { font-size: .75rem; color: var(--muted); margin-top: .35rem; min-height: 1em; text-align: center; } + +.mbe-figure[data-state=simulating] .mbe-figure__static { opacity: .5; } +.mbe-figure[data-state=error] { border-color: #c0392b; } From 3dd79320f2856e1317f3f40ad1b103bc1a35cef0 Mon Sep 17 00:00:00 2001 From: "Michael M. Tiller" Date: Wed, 1 Jul 2026 18:41:06 -0400 Subject: [PATCH 02/12] =?UTF-8?q?docs+i18n(web-astro):=20Sphinx=E2=86=92As?= =?UTF-8?q?tro=20feature=20map=20+=20po4a=20gettext-reuse=20scaffold=20(MI?= =?UTF-8?q?C-126,=20MIC-92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FEATURE-MAP.md: 1:1 mapping of every Sphinx feature the book uses (by count) to Astro/Starlight. Native/one-plugin for the bulk (code, literalinclude, math, figures, admonitions, toctree, search, conditional, raw); component for interactive figures (done). Two real work items flagged: :ref: cross-refs + auto-numbering (310 uses -> custom remark plugin, heaviest gap) and i18n. Decisions, not work: index->search, todo->drop, print export (MIC-85). i18n/ — gettext-reuse strategy so NO human re-translation is needed: - migrate-gettext.mjs: one-time RST-markup -> Markdown transform of existing .po files (msgid + msgstr). Proven on cn/first_order (fully translated, 35 units): 20 prose units match exactly + untouched, 15 marked-up re-keyed, 25 msgstrs rewritten, 0 re-translations. :math:/literal/\ -escapes handled; :ref: with translated link text is a known fuzzy (ties to the xref plugin). - po4a.cfg + README: forward workflow keeping one English source + per-lang .po overlays (today's gettext model), generating translated MDX for Starlight as build artifacts. msgmerge reconciles; near-misses = fuzzy confirm, not re-translate. Platform-independent across MDX frameworks. Co-Authored-By: Claude Opus 4.8 --- web-astro/FEATURE-MAP.md | 54 +++++++++++++ web-astro/i18n/README.md | 89 +++++++++++++++++++++ web-astro/i18n/migrate-gettext.mjs | 124 +++++++++++++++++++++++++++++ web-astro/i18n/po4a.cfg | 29 +++++++ 4 files changed, 296 insertions(+) create mode 100644 web-astro/FEATURE-MAP.md create mode 100644 web-astro/i18n/README.md create mode 100644 web-astro/i18n/migrate-gettext.mjs create mode 100644 web-astro/i18n/po4a.cfg diff --git a/web-astro/FEATURE-MAP.md b/web-astro/FEATURE-MAP.md new file mode 100644 index 00000000..54bd8a2c --- /dev/null +++ b/web-astro/FEATURE-MAP.md @@ -0,0 +1,54 @@ +# Sphinx → Astro/Starlight feature parity map + +Every Sphinx feature the book actually uses (counted across `text/source/**/*.rst`) +mapped to its Astro + Starlight equivalent. Status legend: + +- **Native** — built into Astro/Starlight, no extra work. +- **Plugin** — an npm remark/rehype plugin + config (not a fork). +- **Component** — an MDX component we author (like `SimFigure`). +- **Gap** — no drop-in; needs a custom plugin or a product decision. Flagged. + +## Directives + +| Sphinx directive | Uses | Astro/Starlight equivalent | Status | Notes | +|---|---:|---|---|---| +| `code-block` | 293 | Fenced ` ```lang ` via Expressive Code | **Native** | Shipped with Starlight. Modelica has no Shiki grammar → renders as plain text (add a TextMate grammar later; mystmd is no better). | +| `literalinclude` | 285 | `?raw` import + `` | **Native** | Demonstrated in the slice — code stays sourced from the `.mo` files. `:lines:`/`:emphasize-lines:` = a slice/mark helper. | +| `index` | 232 | Pagefind full-text search (built in) | **Gap→decision** | Starlight ships site search, which largely supersedes a hand-maintained index. A literal back-of-book index page would need a small remark plugin. Recommend: drop in favor of search unless a print index is required. | +| `math` (block) / `:math:` (inline) | 106 / 404 | `remark-math` + `rehype-katex` | **Plugin** | Wired in the slice; renders via KaTeX. | +| `plot` + `:class: interactive` | 104 | `` MDX component | **Component** | The bake-off centerpiece — native MDX component, no fork. | +| `figure` | 80 | Markdown image / `
` component | **Native** | Rendering is native; **auto-numbering** ties to the cross-ref gap below. | +| `toctree` | 21 | Starlight `sidebar` (manual or `autogenerate`) | **Native** | Different model — nav is site config, not in-content. | +| `topic` | 18 | `:::note` / `