Editor Component — Feature Inventory
Complete inventory of the author-facing features of the Merlin MyST
Editor, the published component that provides Instructor Studio’s editing
surface, as verified against the live code in the codermerlin.academy-pocs
repository (instructor-studio/component-assemblies/myst-editor/) at POC
build 213 (--poc-version: build 213 · 20260910T123348).
The component is hosted by the Studio’s application shell (js/app.js).
Component features render identically in the Studio preview, the Student Lab
POC and published lessons when all three resolve the component from the
published-components catalog; see the index’s note on the three
surfaces for what changes under a
local-component override.
For the component’s programmatic surface — its public API, its events, and the Studio SDK it consumes from the host — see Formats and Contracts. For the application around it, see Studio Shell.
Contents
- Editing surface and history
- Context menu
- Keyboard shortcuts
- Find and Replace
- Source markers
- Glossary
- Call outs and boxes
- Insert and format operations
- Merlin Terminal embeds
- AI operations (ĀYŌDÈ Intelligence)
- Preview and rendering
- Student mode
Editing surface and history
- Three-pane layout — source
<textarea>(left), draggable 6 px gutter, live preview (right). Student mode collapses to a single preview-only column. - Undo/redo — 300-step history per panel; typing bursts coalesce into a
single undo step (250 ms debounce); selection and cursor are restored on
undo/redo. History resets on panel switch (
setContent), so each panel edits independently. - One operation, one undo step — every menu command, AI-applied result, and programmatic edit flows through a shared programmatic-edit recipe so it is exactly one undo step.
Context menu
Opened by right-click on the source pane. Disabled in student mode.
Edit group
| Item | Behavior |
|---|---|
| Undo | Enabled when history is non-empty |
| Redo | Enabled when the redo stack is non-empty |
| Find… | Opens the find bar |
Text ▸ Format submenu
AST-aware toggles with state markers (✓ applied, ◪ mixed selection), in menu order:
| Item | Markup |
|---|---|
| Bold | **…** |
| Italic | *…* |
| Inline code | `…` (disabled inside code/math context) |
| Strikethrough | ~~…~~ |
| Math | $…$ (disabled inside code/math context) |
| Superscript | {sup}`…` MyST role |
| Subscript | {sub}`…` MyST role |
| Highlight ▸ | Five-colour submenu — see below |
| Link | […](url) — prompts for the URL; toggling off keeps the text |
Text ▸ Format ▸ Highlight submenu
Five per-colour MyST roles, {highlight-<color>}`…`. The role names are
a published-content syntax contract and are never abbreviated.
| Item | Emitted role |
|---|---|
| Yellow | {highlight-yellow}`…` |
| Cyan | {highlight-cyan}`…` |
| Green | {highlight-green}`…` |
| Pink | {highlight-pink}`…` |
| Orange | {highlight-orange}`…` |
- Each entry carries a colour swatch, class-styled (
span.mc-sw.mc-sw-<key>) rather than inline-styled, for CSP posture. - ✓ marks the highlight containing the cursor or selection; clicking it unwraps, clicking a different colour switches in one edit.
- Entries disable without a block and inside literal code or math contexts. A highlight’s own body is deliberately not treated as inline code, so the entries stay enabled inside an existing highlight — which is what makes unwrap and switch reachable.
- With no selection and the cursor outside any highlight, a click is a refused no-op (the same convention as superscript and subscript).
Text ▸ Block submenu
| Item | Behavior |
|---|---|
| Paragraph | Strips the ATX heading prefix from the current line |
| Heading 1–3 | Converts the current line to #/##/### |
| Bulleted list | Toggles a - prefix across the selection’s lines |
| Numbered list | Toggles 1.-style prefixes across the selection’s lines |
| Blockquote | Toggles a > prefix across the selection’s lines |
| Code block | Wraps the block in a ``` fence, or unwraps it |
| Display math | Wraps in $$ … $$, or unwraps |
| Display math (numbered) | Wraps in $$ … $$ (eq:label) — prompts for the label and inserts a sample {eq} reference; converts between numbered and plain |
Text group commands
| Item | Behavior |
|---|---|
| Remove redundant whitespace | Tidies the selection if it contains removable content, otherwise the whole document; a second run is a no-op. Despite the name this pass does more than whitespace — see What the tidy pass actually removes. Always enabled: its precondition is invisible characters, so a scan-driven disabled state would be indistinguishable from a broken item |
| Clear formatting | Strips inline marks overlapping the selection in one pass — bold, italic, inline code, strikethrough, math, links and highlights. {sup}/{sub} roles are deliberately not stripped (a known gap in the component, not an oversight in this document). Disabled when the selection overlaps no clearable mark |
Text ▸ Insert submenu
Disabled while a selection exists — a selection means “format”, not “insert”.
| Item | Behavior |
|---|---|
| Box ▸ Note / Warning | Native :::{note} / :::{warning} admonition — wraps the selection or inserts a sample block |
| Box ▸ Exercise | :::{admonition} Exercise generic admonition |
| Box ▸ Dropdown | :::{admonition} <title> + :class: dropdown — prompts for the summary; renders as a <details> element |
| Horizontal rule | Inserts --- as a block |
| Table | Inserts a sample 2×2 table with a header row |
| Image ▸ URL… | Opens the image dialog (URL, alt text, width, height, scale %, alignment) |
| Image ▸ Upload… | Stub — alerts “Upload is coming soon — for now use the URL option.” |
| Figure ▸ URL… | Image dialog plus caption and reference label; figures are auto-numbered and referenced with {numref} |
| Figure ▸ Upload… | Stub, as above |
| Merlin Terminal ▸ Empty | Inserts a bare :::{merlin-terminal} directive |
| Merlin Terminal ▸ Challenge | Opens a picker over the lesson’s challenges and inserts a challenge-scoped terminal — see Merlin Terminal embeds |
Call outs submenu
Five styled admonition types. The submenu is top-level (a sibling of Insert)
so it stays enabled while a selection exists. The callout-<slug> class
token is the single contract for styling, detection, and menu state.
| Type | Emitted MyST | Accent | Icon |
|---|---|---|---|
| Deep Dive | :::{admonition} Deep Dive + :class: callout-deep-dive |
blue | 🤿 |
| Fact Box | :::{admonition} Fact Box + :class: callout-fact-box |
teal | 📌 |
| Warning | :::{warning} + :class: callout-warning |
gold | ⚠️ |
| Danger | :::{danger} + :class: callout-danger |
red | 🚨 |
| Observe and Ponder | :::{admonition} Observe and Ponder + :class: callout-observe-and-ponder |
purple | 🔍 |
Behavior:
- Wraps the selection, or inserts a sample block when nothing is selected.
- ✓ marks the type the cursor is inside; clicking it unwraps; clicking a different type switches in place.
- All entries disable when the cursor is inside any non-callout
admonition — including a plain, class-less
{warning}/{danger}box — preventing double-wrapping and silent rewrites of Box-authored blocks. - Plain Box admonitions keep their generic styling; only the
callout-*class receives the branded treatment.
ĀYŌDÈ Intelligence submenu
AI text operations on the current selection (requires the Studio SDK; disabled without a selection). Results are shown in a compare modal (original ↔ proposed) with Copy, Replace, Insert Before, Insert After.
| Group | Operations |
|---|---|
| Content | Simplify · Make More Concise · Explain More (RAG) · Increase Technical Depth (RAG) · Add Example (RAG) · Add Analogy (RAG) |
| Clarity | Improve Grammar · Improve Flow · Clarify Terminology · Improve Accessibility |
| Pedagogy | Adapt for Younger Students · Adapt for Older Students · Increase Student Engagement · Socratic Style |
| Style | More Formal · More Neutral · More Conversational · More Enthusiastic |
| Generate | Summary · Key Takeaways · Learning Objectives (RAG) · Practice Questions (RAG) · Challenge Questions (RAG) · Add term to glossary… |
| Custom | Custom… — free-form instruction |
“Add term to glossary…” generates a definition matched to the lesson’s language level and lands it in the glossary define dialog for author review before saving.
Below those twenty-five operations sits a structurally different entry:
| Item | Behavior |
|---|---|
| Generate / Update assessment panel ▸ | Fill in the blank · Multiple choice (single) · Multiple choice (multiple) |
- Enabled with or without a selection — no selection widens the generation source to the whole current panel. Every other Intelligence entry requires a selection.
- The submenu’s own verb is chosen synchronously at menu-build time: “Update”
when the host reports an existing child
assessmentpanel, “Generate” otherwise (and when no SDK is present). - The three question families map 1:1 onto the host’s challenge
evaluationModevalues. Math expression (casEquivalence) is deliberately deferred and omitted. - The component only captures the selection and reports intent; the host owns the payload, the backend call and the review-before-hydrate flow. See ĀYŌDÈ Intelligence Operations.
Glossary submenu
Requires the Studio SDK; needs a non-empty selected term.
| Item | Behavior |
|---|---|
| Add term to glossary… | Opens the define dialog (term + definition); redefines if the term exists |
| Link glossary terms | Re-runs term-ification across all content panels |
Keyboard shortcuts
These are the component’s own bindings. For the application menubar’s accelerators, see Studio Shell.
| Keys | Action |
|---|---|
| Ctrl/Cmd+Z | Undo |
| Ctrl/Cmd+Shift+Z, Ctrl/Cmd+Y | Redo |
| Ctrl/Cmd+F | Open the find bar (focus the query if already open); suppresses the browser’s page search; no-op while a modal is open or in student mode |
| Enter / Shift+Enter (find query) | Next / previous match |
| Enter (replace input) | Replace the current match and advance |
| Escape (in the bar) | Close the bar and refocus the editor at the current match |
| Ctrl/Cmd+Z / Shift+Z / Y (on the bar’s buttons) | Forwarded document undo/redo — the bar’s text inputs keep their native input undo |
Find and Replace
The find bar overlays the top-right of the source pane, inside the component (focus moves between the editor and the bar never leak out of the component host).
Find (row 1):
- Literal search — the query is regex-escaped; case-insensitive by default with an Aa case-sensitivity toggle.
n of mmatch counter; a zero-match state colors the counter and query border red.- ‹ / › navigation with wrap-around; matches become real textarea selections and are also shaded in the backdrop mirror so they remain visible while focus stays in the bar (VS Code model).
- Close (×) or Escape returns focus to the editor at the current match.
- Find has zero write paths — the source is byte-identical after any find session.
Replace (row 2):
- Replacement text is inserted verbatim (no regex metacharacter semantics).
- Replace substitutes the current match, advances to the next, and is one undo step; a byte-identical replacement simply advances (no write, no dirty-mark).
- Replace All substitutes every match as ONE undo step — a single Ctrl/Cmd+Z restores the exact pre-replace source and selection. When the outcome would be byte-identical it is a strict no-op reporting “no changes”; otherwise a transient “N replaced” message shows.
- The match counter recounts through the component’s single
contentchangehook, so typed and programmatic edits (undo, menu operations, AI results) never desynchronize it.
Source markers
An always-on, metric-exact backdrop mirror behind the (transparent-text) textarea marks source the tidy pass has an opinion about. Two families of marker paint there, and they make different promises.
Whitespace markers
| Marker | Meaning |
|---|---|
␠ (U+2420) |
A whitespace character the tidy pass would remove — trailing spaces/tabs/NBSP/Unicode spaces, whitespace-only lines, interior runs of 2+ spaces |
⏎ (U+23CE) |
A line break the tidy pass would remove (blank-line runs beyond one, and the joins made by paragraph unwrapping) |
␠␠⏎ in a distinct deeper shade |
A hard line break (2+ trailing spaces — a forced <br> in MyST). The tidy pass preserves these (normalizing to exactly two), so the distinct shade flags “author decision needed” |
Never marked: ordinary interior spaces, soft wraps, paragraph-ending newlines, the single legitimate blank line between blocks, and anything inside protected spans (code, inline code, math, links, images, directives).
Embedded-HTML markers
Background-only spans with no glyph, since they cover multiple characters at once. The two shades make opposite promises:
| Marker | Meaning |
|---|---|
mc-html (light coral) |
Embedded HTML the tidy pass will remove |
mc-html-open (light warm yellow) |
An unterminated flow construct the tidy pass will not touch until the author closes or deletes it — protect-and-flag |
The second shade is why there is no “after tidying, only hard breaks remain” corollary: an unterminated construct leaves its marker standing after a completed tidy pass, by design.
Mechanics
The markers are display-only — glyphs are absolutely-positioned CSS overlays,
and the source, save, publish, copy and find all see the real characters.
They recompute on the preview’s 160 ms debounce and share one oracle
(wsEdits) with the tidy command and the host’s save-time pipeline, so they
can never disagree with what saving will do. Above 10,000 decorations only
backgrounds paint (no glyphs), to protect performance on pathological pastes.
What the tidy pass actually removes
The name “Remove redundant whitespace” understates the pass. One oracle backs the menu command, the marker layer and the host’s pre-save normalization pipeline, and it makes three kinds of edit:
- Whitespace — trailing whitespace, whitespace-only lines, interior runs of two or more spaces, blank-line runs beyond one; hard line breaks are preserved and normalized to exactly two trailing spaces.
- Paragraph unwrapping — a soft line break inside a paragraph is redundant, so hard-wrapped paragraphs are joined into single lines. Rendering is identical by construction (a soft break is equivalent to a space), but the saved bytes change.
- Embedded HTML stripping — tag tokens are removed with their
human-readable inner text preserved; terminated comments, CDATA,
processing instructions, declarations and closed
<script>/<style>elements are removed whole; unterminated flow constructs are protected and flagged rather than stripped.
All three compose in one idempotent pass. The practical consequence for implementers: because the host runs this oracle before every save and publish, saving a hard-wrapped document or one containing inert HTML deliberately changes its bytes — and therefore its content hash — relative to what the author typed. That is intended behavior, not drift.
Glossary
- The Glossary panel’s MyST
{glossary}directive is the single authoritative store (alphabetized definition list). - Term-ification writes explicit
{term}`…`roles into content panel sources — AST-masked (code, math, links, directive fences are protected), idempotent, and serialized through a single-flight queue. - Define dialog — create or redefine a term; pre-populated by the AI define-term flow when invoked from ĀYŌDÈ Intelligence ▸ Generate.
- Preview navigation —
{term}roles render as styled links (#term-<slug>); clicking navigates to the glossary entry, and back-to-reading restores the originating panel and exact preview scroll position.
The host-side SDK that drives this round trip is documented in Formats and Contracts.
Call outs and boxes
See the Call outs submenu for the five branded types and the Insert submenu for the generic boxes (Note, Warning, Exercise, Dropdown). Key invariants:
- The
callout-<slug>class token is the only styling key; class-less admonitions keep generic styling everywhere (editor preview, Student Lab, published lessons). - All emitted forms are renderer-supported MyST that round-trips byte-identically (no “unhandled” directive rendering).
- Dropdown admonitions render as
<details>/<summary>elements. - Callout styling is CSS, not post-processing. The accent border, tint
background and emoji icon are static rules on
.mc-render .admonition.callout-*; nothing in the render function touches callouts. This is precisely why the class token survives unchanged into Student Lab and published lessons.
Insert and format operations
- Headings/lists/quotes operate line-wise on the selection (no whole-block reserialization).
- Display math: plain (
$$ … $$) and numbered ($$ … $$ (eq:label)) forms; equation numbers render automatically and{eq}`…`references stay synchronized. - Images and figures: dialog-driven
:::{image}/:::{figure}directives with alt/width/height/scale/alignment; figures add a caption and an optional reference label — figure numbers auto-increment and{numref}`…`references stay synchronized. (:scale:is applied by the renderer’s post-processing sincemyst-to-htmlignores it.) - Tables and horizontal rules: sample-block insertion.
- Upload is not implemented. Both Upload… entries alert and insert nothing; drag-and-drop upload is a separate effort.
Merlin Terminal embeds
The editor can embed a live Merlin Terminal — a third published
component — directly in the preview, via a :::{merlin-terminal} directive.
Authoring
| Insert entry | Emits |
|---|---|
| Empty | :::{merlin-terminal} with no options |
| Challenge | :::{merlin-terminal} + :challenge-shell: <challengeRef> |
The Challenge picker lists the current lesson’s challenges through the host SDK, synchronously, at menu-build time; the entry is disabled when the host reports no challenges — or registers no SDK at all, which is how it presents in Student Lab and published-lesson hosts.
:challenge-shell: is a Studio-level authoring option, not a component
option. The published component ignores unknown options, so the preview still
shows a terminal placeholder; at publish the host rewrites it into the
component’s real options. See
Programs and Workbench for that rewrite.
Mounting
- The component is resolved from the public components catalog by name
(
Merlin Terminal) — only the catalog is unauthenticated; the payload itself is JWT-protected, and the token comes from the host’s access-token hook. Without a signed-in host the mount reports “sign in to load the terminal”. - Live mounts are detached into a holding fragment before each
innerHTMLswap and re-inserted afterwards, so an open WebSocket and xterm instance survive a preview re-render. - Only sentinel-bearing placeholders participate, so a hand-typed
```merlin-terminal-embedcode fence renders as an ordinary code block rather than mounting anything. - Sessions are click-to-connect. Accepted v1 limitation: reordering two identically-configured connected terminals can misattribute which live session lands where.
- Failure states render in place: “Loading Merlin Terminal…”, “Merlin Terminal failed to start: …”, “Merlin Terminal unavailable: …”.
AI operations (ĀYŌDÈ Intelligence)
Twenty-five text operations across Content, Clarity, Pedagogy, Style, Generate and Custom, plus the three assessment-panel families (see the menu tables). Mechanics:
- The component dispatches an
aitunerequestevent to the host with the selection, operation name, and RAG/generate flags; the host routes it to the platform’s lesson-text-tune endpoints (synchronous or RAG/async). Assessment requests reuse the same event with a distinguishingdetail.kind. - A progress modal shows stage, elapsed time, and completion; results arrive in a compare view with Copy / Replace / Insert Before / Insert After.
- Every request carries the full lesson context (marker-delimited panel content plus the glossary) so operations match the lesson’s language level and terminology.
Host routing, endpoint selection and the review flows are documented in ĀYŌDÈ Intelligence Operations.
Preview and rendering
The pipeline is myst-parser (with strikethrough and math extensions), then
three AST transforms, then myst-to-html, then post-processing:
- Glossary transform —
{term}roles become glossary links. - Highlight transform —
{highlight-*}roles become classed spans. - Terminal transform —
{merlin-terminal}directives become renderable placeholders.
Live terminal mounts are detached immediately after the transforms, before the HTML swap.
Post-processing, in order:
- External-link targeting — every external link gets
target="_blank" rel="noopener noreferrer"; in-page#fragments (glossary terms,{numref}and{eq}anchors) are deliberately excluded, because they scroll within the same preview. - KaTeX for inline and display math.
- Equation numbering for labeled display math, with
{eq}reference synchronization (the fragment decode is guarded, so a malformed percent-escape cannot abort the pass). - Dropdown conversion (
:class: dropdownadmonitions →<details>). - Image
:scale:application. - Figure numbering (“Figure N.” injected into captions) with
{numref}synchronization.
Terminal placeholders are then replaced with live component mounts, and the
component emits componentrendered.
Callout styling and glossary-link styling are not in this pipeline — both are static CSS keyed off classes the transforms and the class token already put in place.
The preview re-renders on a 160 ms debounce after each change.
Student mode
Activated by the host (setStudentMode() or the component view-mode
dataset). The layout collapses to preview-only; the source pane, gutter,
marker mirror, find bar, and context menu are hidden or inert; editing,
undo, and redo are unavailable. Published-lesson viewers without the
Studio SDK degrade gracefully to plain preview — the Merlin Terminal ▸
Challenge entry is among the things that simply present as disabled.