# 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](index.md#the-three-rendering-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](formats-and-contracts.md). For the application around
it, see [Studio Shell](studio-shell.md).

## Contents

- [Editing surface and history](#editing-surface-and-history)
- [Context menu](#context-menu)
- [Keyboard shortcuts](#keyboard-shortcuts)
- [Find and Replace](#find-and-replace)
- [Source markers](#source-markers)
- [Glossary](#glossary)
- [Call outs and boxes](#call-outs-and-boxes)
- [Insert and format operations](#insert-and-format-operations)
- [Merlin Terminal embeds](#merlin-terminal-embeds)
- [AI operations (ĀYŌDÈ Intelligence)](#ai-operations-āyōdè-intelligence)
- [Preview and rendering](#preview-and-rendering)
- [Student mode](#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](#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](#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 `assessment` panel, "Generate"
  otherwise (and when no SDK is present).
- The three question families map 1:1 onto the host's challenge
  `evaluationMode` values. 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](ai-operations.md).

### 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](studio-shell.md).

| 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 m` match 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
  `contentchange` hook, 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:

1. **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.
2. **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.
3. **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](formats-and-contracts.md).

## Call outs and boxes

See the [Call outs submenu](#call-outs-submenu) for the five branded
types and the [Insert submenu](#text--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 since `myst-to-html` ignores 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](programs-and-workbench.md) 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
  `innerHTML` swap 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-embed ```` code 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](#āyōdè-intelligence-submenu)). Mechanics:

- The component dispatches an `aitunerequest` event 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 distinguishing
  `detail.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](ai-operations.md).

## Preview and rendering

The pipeline is `myst-parser` (with strikethrough and math extensions), then
**three AST transforms**, then `myst-to-html`, then post-processing:

1. **Glossary transform** — `{term}` roles become glossary links.
2. **Highlight transform** — `{highlight-*}` roles become classed spans.
3. **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: dropdown` admonitions → `<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.
