Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/stepper-opaque-thumbnail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sourceacademy/common-stepper": patch
---

Add an `image` `SyntaxTemplatePart` (renders a node's data-URL property as an inline `<img>`, 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.
14 changes: 13 additions & 1 deletion src/common/stepper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<img>`; 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
Expand All @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions src/web/stepper/src/SubstVisualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,25 @@ function renderNode(
<span key={key}>{part.parts.map((p, i) => renderPart(p, i))}</span>
) : null;
}
if ("unless" in part) {
return node[part.unless] ? null : (
<span key={key}>{part.parts.map((p, i) => renderPart(p, i))}</span>
);
}
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 (
<img
key={key}
className={classNames("stepper-opaque-thumbnail", cls(part.cls))}
src={src}
alt={alt == null ? "" : String(alt)}
title={alt == null ? undefined : String(alt)}
/>
);
}
return null;
};
return <span>{template.map((part, i) => renderPart(part, i))}</span>;
Expand Down
10 changes: 10 additions & 0 deletions src/web/stepper/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading