diff --git a/.changeset/stepper-opaque-thumbnail.md b/.changeset/stepper-opaque-thumbnail.md new file mode 100644 index 0000000..28e7c55 --- /dev/null +++ b/.changeset/stepper-opaque-thumbnail.md @@ -0,0 +1,5 @@ +--- +"@sourceacademy/common-stepper": patch +--- + +Add an `image` `SyntaxTemplatePart` (renders a node's data-URL property as an inline ``, e.g. a rendered thumbnail for an opaque runtime value — DrRacket-style — falling back to nothing when the property is absent) and an `unless` part (the inverse of `when`, for pairing an image with a textual fallback). `web-stepper` renders both generically; a language opts in by using them in its `SyntaxProfile` templates. Both additions are optional and backward-compatible. diff --git a/src/common/stepper/src/index.ts b/src/common/stepper/src/index.ts index d7e5f42..63f1050 100644 --- a/src/common/stepper/src/index.ts +++ b/src/common/stepper/src/index.ts @@ -118,6 +118,16 @@ export type StepperTokenClass = "operator" | "identifier" | "literal" | "conditi * - `{ block }` — render the `node[block]` array as an indented suite (one statement per line). * - `{ lines }` — render the `node[lines]` array one-per-line without extra indentation (the root). * - `{ when, parts }` — render `parts` only when `node[when]` is present (e.g. an optional `else`). + * - `{ unless, parts }` — render `parts` only when `node[unless]` is absent/falsy; the inverse of + * `when`, for an "otherwise" branch (e.g. a value that renders one way when a field is present and + * another way when it isn't — see `image` below). + * - `{ image, altProp?, cls? }` — render `node[image]` as an inline ``; renders nothing when + * the property is absent/falsy, or when it isn't a `data:` URL (e.g. `"data:image/png;base64,..."`) + * — only self-contained data URLs are rendered, never a live network URL, so a module can't turn a + * stepper render into a request to an arbitrary host. `altProp` optionally names another (possibly + * dotted) node property to use as the image's `alt`/`title` text. Lets a language display an opaque + * runtime value as a small picture inline in a step — e.g. a rendered thumbnail a module attaches + * to a graphics object — DrRacket-style, rather than only ever as text. */ export type SyntaxTemplatePart = | string @@ -127,7 +137,9 @@ export type SyntaxTemplatePart = | { list: string; sep: string; prefix?: string; cls?: StepperTokenClass } | { block: string } | { lines: string } - | { when: string; parts: SyntaxTemplatePart[] }; + | { when: string; parts: SyntaxTemplatePart[] } + | { unless: string; parts: SyntaxTemplatePart[] } + | { image: string; altProp?: string; cls?: StepperTokenClass }; /** * Declares a node type as a "function value" in the substitution model and where to read its name. diff --git a/src/web/stepper/src/SubstVisualizer.tsx b/src/web/stepper/src/SubstVisualizer.tsx index 3513a43..ea574fb 100644 --- a/src/web/stepper/src/SubstVisualizer.tsx +++ b/src/web/stepper/src/SubstVisualizer.tsx @@ -834,6 +834,25 @@ function renderNode( {part.parts.map((p, i) => renderPart(p, i))} ) : null; } + if ("unless" in part) { + return node[part.unless] ? null : ( + {part.parts.map((p, i) => renderPart(p, i))} + ); + } + if ("image" in part) { + const src = readNodeProp(node, part.image); + if (typeof src !== "string" || !src.startsWith("data:")) return null; + const alt = part.altProp === undefined ? undefined : readNodeProp(node, part.altProp); + return ( + {alt + ); + } return null; }; return {template.map((part, i) => renderPart(part, i))}; diff --git a/src/web/stepper/src/styles.ts b/src/web/stepper/src/styles.ts index 8f7046a..801e73f 100644 --- a/src/web/stepper/src/styles.ts +++ b/src/web/stepper/src/styles.ts @@ -52,6 +52,16 @@ const STEPPER_CSS = ` font: 16px/normal 'Inconsolata', 'Consolas', monospace; } +/* An opaque value's rendered thumbnail (see the image SyntaxTemplatePart) — shown inline at text + * height, DrRacket-style, rather than expanding the line. */ +.sa-substituter .stepper-opaque-thumbnail, +.stepper-popover .stepper-opaque-thumbnail { + height: 1.4em; + width: auto; + vertical-align: middle; + border-radius: 3px; +} + .sa-substituter .stepper-mu-term, .stepper-popover .stepper-mu-term { font-weight: bold;