@openpresentation/opf-editor 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,9 +76,9 @@ control.destroy();
76
76
 
77
77
  The host owns the source draft, OPF validation, slide preview, persistence and session history. `onChange` includes invalid JSON while typing; keep the last valid preview until the draft validates. `update` applies external source without emitting `onChange` or creating a local undo step. It preserves unrelated edits through CodeMirror's history mapping; it is not a collaboration/conflict-resolution protocol. Importing either entrypoint does not require a DOM; mounting requires an existing browser container.
78
78
 
79
- The control provides indentation, paired quotes/brackets, folding shortcuts, search, undo/redo, JSON syntax diagnostics and explicit formatting (`Cmd/Ctrl+Shift+F`). Enter at the end of a nonempty string array item supplies indentation, quotes and commas. Other Enter positions use ordinary JSON indentation. Source offsets are UTF-16 in the original authored string. Existing CRLF, CR and LF spellings survive ordinary edits and undo; new typed lines use the first existing separator. Paste retains the text delivered by the browser clipboard event. Explicit formatting deliberately standardizes whitespace, preserving JSON values and numeric lexemes.
79
+ The control provides indentation, paired quotes/brackets, folding shortcuts, search, undo/redo, JSON syntax diagnostics, multiple cursors and explicit formatting (`Shift+Alt+F` or `Cmd/Ctrl+Shift+F`, also while the JSON does not parse). Its keys follow VS Code (see Keyboard below). Enter at the end of a nonempty string array item supplies indentation, quotes and commas. Other Enter positions use ordinary JSON indentation. Source offsets are UTF-16 in the original authored string. Existing CRLF, CR and LF spellings survive ordinary edits and undo; new typed lines use the first existing separator. Paste retains the text delivered by the browser clipboard event. Explicit formatting deliberately standardizes whitespace, preserving JSON values and numeric lexemes.
80
80
 
81
- Click a categorical property/value or press `Ctrl+Space`, `Cmd+Space` or `Alt+Down` to open its choices. Records the document embeds come first, then those of the catalogs the host passes as `catalogs` (`Catalog[]`, the session's registered list); there are no built-in records. The actual schema supplies enums and booleans. Prose and extension fields do not gain menus just because their keys resemble schema fields. External catalog URLs are not fetched. `setCatalogs(list)` replaces the host's catalogs and closes stale choices. Supply `onOptions(left?, top?)` to retain an existing menu UI.
81
+ Press `Alt+Shift+O` (and, with `completions` off, `Ctrl+Space`) to open the choices of the categorical property or value at the caret; clicking a value only places the caret. Records the document embeds come first, then those of the catalogs the host passes as `catalogs` (`Catalog[]`, the session's registered list); there are no built-in records. The actual schema supplies enums and booleans. Prose and extension fields do not gain menus just because their keys resemble schema fields. External catalog URLs are not fetched. `setCatalogs(list)` replaces the host's catalogs and closes stale choices. Supply `onOptions(left?, top?)` to retain an existing menu UI.
82
82
 
83
83
  In OPF 0.19, layout choices change only the template reference and preserve authored content. Empty regions remain available for explicit insertion; choosing a template does not invent blank payloads. The JSON field menu groups templates by intent and describes their supported regions. Hosts apply `replaceFieldOption` or `fieldOptionEdit(context, value)` as one undoable edit and check that `context.source` is still current.
84
84
 
@@ -116,13 +116,16 @@ Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below)
116
116
  - **Keyboard Undo and Redo in the pane (one model).** Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) work in two layers, in this order. First CodeMirror's history, which holds only the text typed in the pane since its last whole-document change (a `replaceDocument`, or a session Undo, Redo or restore, each of which clears it because the typed edits no longer fit the text). When that has nothing left, the key steps the session over a whole-document replace, exactly as its Undo and Redo do, so the text comes back byte for byte: Undo steps back over the nearest `replaceDocument` together with the pane's own edits typed on it (the typing was already undone, step by step, by CodeMirror's history); Redo steps forward over the replace. The key never reaches over an edit made elsewhere (the canvas, a panel): Undo stops at it, and the host's Undo button (or `editor.undo()`) undoes that edit, after which the key reaches the load again. A read-only pane takes neither key. `mountJsonCodeEditor` exposes the same seam as `historyFallback: { undo(), redo() }` (answer `true` when you did something) plus `resetHistory()` and `undoDepth()` on the control.
117
117
  - **JSON is canonical** and keeps the author's bytes: a YAML or Markdown edit rewrites only the changed JSON tokens (`exact-source`), and Undo restores the exact spelling.
118
118
  - **YAML and Markdown** are core's browser-safe converters, `@openpresentation/opf/yaml` and `/markdown`. Switching format writes the session's document in the new format. An edit parses back to the document and reaches the session as one `replace` patch, after a short delay (`applyDelay`, default 250 ms) so one burst of typing is one undo step. A syntax error, or an OPF rule the session refuses, is listed with its line and never reaches the document; Undo, Redo and outside edits replace a draft that did not parse.
119
- - **Nothing is lost silently.** Markdown writes what its dialect has no syntax for (a `design`, region keys, nested groups) as YAML in `opf-slide` and `opf-block` fences; each such part is a warning finding with its JSON Pointer and reason. Comments in YAML cannot survive a rewrite from the document: Format and outside edits drop them and say how many. A document that is not valid OPF cannot be written as YAML or Markdown; the pane stays in JSON and says so.
120
- - **Completions.** In JSON the pane offers property names and values as you type (`completions: false` keeps the field menu only): the keys the schema allows at the cursor's object, and the enum, boolean and catalog values of the field, from the same data as the field menu (`getJsonCompletions` in `/json-options`, which reads drafts that do not parse yet). Enter accepts, Escape closes, and Ctrl+Space or Alt+ArrowDown still open the field menu. YAML and Markdown keep the menu only. `mountJsonCodeEditor` takes the same `completions` option, off by default there.
121
- - **Accessibility.** The editor's `aria-invalid` is `true` while the findings include an error and `false` otherwise. The format button (`aria-label="Source format"`) opens a `role="menu"` of `menuitemradio` items: Arrow keys, Home, End, Enter, Escape (focus returns to the button) and Tab. The file name is a button that becomes a text input; Enter or Escape commits. Controls show a 2px `--opf-ui-ring` focus outline.
119
+ - **Nothing is lost silently.** Markdown writes what its dialect has no syntax for (a `design`, region keys, nested groups) as YAML in `opf-slide` and `opf-block` fences; each such part is a warning finding with its JSON Pointer and reason. Comments in YAML cannot survive a rewrite from the document: outside edits drop them and say how many. Format (below) never rewrites from the document, so it keeps them. A document that is not valid OPF cannot be written as YAML or Markdown; the pane stays in JSON and says so.
120
+ - **Completions.** In JSON the pane offers what the schema and the catalogs say belongs at the caret (`completions: false` keeps the field menu only). *Keys*: the properties of the object at the caret, the required ones first, then the common ones in a curated order (a slide: `title`, `subtitle`, `layout`, `items`, `text`, `notes`, ...), then the rest; the promoted region keys of a slide (`top+middle:left+center`) show only once you type three characters of one. Keys already in the object are not offered. Accepting one writes the whole pair: a string key gets `"key": "|"`, a number `"key": |`, an array `"key": [|]`, an object `"key": {|}`, and an object with required members (`organization`: `id`, `name`) writes them one per line, each a tab stop that `Tab` walks (the last `Tab` leaves the braces). A key with a closed set of values (an enum, a catalog id, a boolean) opens the list of its values at once. A comma is added when a property follows, and not before `}`. *Values*: enum values and booleans, and catalog ids (layouts, themes, color and font schemes, narratives, audiences, purposes, tones: the document's own records first, then the registered catalogs', layouts in the order of their intents, each with its description and regions in the documentation panel); after a colon, `{}` and `[]` templates. *Snippets*: `slide`, `slide-text`, `slide-image`, `slide-chart`, `slide-quote`, `slide-metric`, `slide-table`, `slide-code`, `slide-two-column` and `section` in `slides`, `reference` in `references`, `dataset` in `datasets` and `deck` in a blank document, with tab stops (the chart type stop opens the list of chart types). *Triggers*: a quote or a word at a key or value place, `:` and the space after it, and `Ctrl+Space` (`Cmd+I` on macOS) anywhere the schema has something to offer, also right after a finished string; free text (titles, notes, alt text) and numbers never open a list on their own. *Keys in the list*: `Tab` and `Enter` accept (at once, right after a keystroke too), `Shift+Tab` and `Shift+Enter` accept too (an accepted item always replaces the whole token), `Escape` closes it, `Left`, `Right` and `Backspace` inside the word keep it open and filter by the text before the caret, `Ctrl+Space` with the list open shows or hides the documentation panel. With several cursors the accepted item goes in at each of them (as plain text). The data is core's (`describeField` and `catalogKindAt` of `@openpresentation/opf/schema`; `getJsonCompletions` of `/json-options` remains for hosts that want the plain list). YAML and Markdown have no completions yet. `mountJsonCodeEditor` takes the same `completions` option, off by default there.
121
+ - **Hover and problems.** Hovering a key shows the schema's description, its type and its allowed values; hovering a catalog id shows the record (name, description, regions, where it comes from); `Cmd/Ctrl+K Cmd/Ctrl+I` shows the same at the caret and `Escape` hides it. Every finding that has a place in the text is underlined (errors, warnings and notes, in every format that gives core a range; JSON gets one for each schema and reference finding too), with a tooltip that says what is wrong, what could come next and how many quick fixes there are. JSON syntax errors come from core's recovery analyzer (`analyzeJsonSyntax` of `@openpresentation/opf/source`): one problem per cause, at the cause (a key with no value is reported on its own line, not at the next token), up to five, 150 ms after the last key; a problem is held back while a list of keys is open next to it, so typing `"des` never flashes red. The panel rows say `Ln 3, Col 5`, the message, `Next: ...`, the rule and the quick fix buttons. `F8` and `Shift+F8` select the next and the previous problem (and say it; they wrap), `Cmd/Ctrl+Shift+M` moves focus to the panel (arrows, Home and End move between rows, Enter selects the range and returns to the code, Escape returns, `Cmd/Ctrl+.` applies the first fix of the row), `Cmd/Ctrl+.` in the code lists the fixes at the caret (the safest first; arrows, Enter, Escape). A fix is one undo step. Quick fixes in this version: the syntax repairs (insert or remove a comma, add `{ }` or remove a key with no value, close a bracket, straight quotes, ...), "Change to ..." for an enum or a reference that resolves nowhere, "Rename to ..." and "Remove property" for an unknown key, "Add `slides`" and the like for a missing required property, "Wrap in a list", "Add a slide", "Remove the first/second" for a duplicate key, and the patch and focus fixes core ships (alt text, readable color, language). `pane.getProblems()`, `nextProblem()`, `previousProblem()`, `showProblem(index)` and `triggerCompletion()` expose the same to a host.
122
+ - **Keyboard.** The keys are VS Code's. `Tab` accepts a suggestion when the list is open; otherwise it inserts spaces up to the next tab stop (replacing a selection inside one line; a selection over several lines, or a whole line, is indented line by line) and `Shift+Tab` outdents the line. The indentation unit is the document's (JSON: read once when a document is opened, two spaces if nothing is indented, or a tab; YAML and Markdown: two spaces) and typing never changes it. `Alt+Up` and `Alt+Down` move the line, `Shift+Alt+Up` and `Shift+Alt+Down` copy it, `Cmd/Ctrl+Shift+K` deletes it. `Cmd/Ctrl+D` adds the next occurrence, `Cmd/Ctrl+Shift+L` all of them, `Cmd/Ctrl+Alt+Up` and `Down` a cursor above or below, `Alt+click` a cursor, `Shift+Alt+drag` a column; `Escape` goes back to one cursor. The field menu (a searchable dialog of the choices of one field) is on `Alt+Shift+O`; `Tab` and `Shift+Tab` stay inside it and `Escape` returns to the editor. A paste inserts the clipboard text as it is, however many lines it has.
123
+ - **Format document.** `Shift+Alt+F` (and `Cmd/Ctrl+Shift+F`) and the Format button do the same in JSON, YAML and Markdown, and change whitespace only, as one undo step with the cursor kept on its token. JSON is laid out token by token with the document's indentation, also while it does not parse; YAML keeps its comments, blank lines, quote styles, number spellings and key order (the pane says so and does nothing when the YAML has errors); Markdown gets at most one blank line between blocks (code fences untouched), its front matter formatted as YAML, and one newline at the end. A format that would change the document is refused. The pane does not rewrite the text from the document (`toYaml`, `toMarkdown`) any more, so it no longer drops comments.
124
+ - **Accessibility.** One polite live region (`.opf-source-live`) says how many suggestions there are when the list opens, the selected item and the first sentence of its documentation as you arrow (after a short pause), `Inserted design` when you accept, `Tab stop 2 of 3: ...` in a snippet, the problem when the caret enters a squiggle or `F8` reaches it, and `2 problems: 1 error, 1 warning` once the problems stop changing for 600 ms; hosts get every message through the `announce` option. Every suggestion has an accessible name with its kind and its place (`design, property, Design. 1 of 3`), the editor's `aria-description` says how to open the list and how to leave the editor (`Escape` then `Tab`, or `Ctrl+M`; `Ctrl+Shift+M` on macOS), and the problem rows start with the severity as text as well as a shape. The editor's `aria-invalid` is `true` while the findings include an error and `false` otherwise. The format button (`aria-label="Source format"`) opens a `role="menu"` of `menuitemradio` items: Arrow keys, Home, End, Enter, Escape (focus returns to the button) and Tab. The file name is a button that becomes a text input; Enter or Escape commits. Controls show a 2px `--opf-ui-ring` focus outline.
122
125
  - **Styles.** `src/styles/source-pane.css` uses only `--opf-ui-*` tokens under the `opf-source-` prefix. It ships in the editor's `styles.css` (`@openpresentation/opf-editor/styles.css`), which also defines the tokens.
123
- - **Dependency.** `@codemirror/lang-yaml` (MIT) highlights YAML; Markdown uses a small stream tokenizer built on `@codemirror/language`, so no Markdown, HTML, CSS or JavaScript grammar is added.
126
+ - **Dependency.** `yaml` (ISC; core already depends on it) formats YAML while keeping its comments. `@codemirror/lang-yaml` (MIT) highlights YAML; Markdown uses a small stream tokenizer built on `@codemirror/language`, so no Markdown, HTML, CSS or JavaScript grammar is added.
124
127
 
125
- Icons are inline paths from [Lucide](https://lucide.dev) (ISC license). The harness is `examples/source-pane.html` (built by `pnpm run build:playground`); `pnpm run test:source-pane-browser` drives it.
128
+ Icons are inline paths from [Lucide](https://lucide.dev) (ISC license). The harness is `examples/source-pane.html` (built by `pnpm run build:playground`); `pnpm run test:source-pane-browser` drives it, and `pnpm run test:source-keyboard-browser` and `pnpm run test:source-keyboard-json-browser` run the keyboard cases (real key events; `OPF_BROWSER_ENGINE=webkit` and `OPF_KEYBOARD_PLATFORM=win32|linux|darwin` pick the browser and the key map).
126
129
 
127
130
  ## Layout editor (RR-99)
128
131
 
@@ -1,16 +1,57 @@
1
1
  import type { JsonCatalogContext } from './json-options.js';
2
2
 
3
+ /** One text edit of a quick fix, in source offsets of the text as it is when the fix is applied. */
4
+ export interface SourceEdit { from: number; to: number; insert: string }
5
+ /**
6
+ * A quick fix of a problem: `edits` are applied as one undoable change (`caret`, or `select: [from, to]`, is a position in the text after them),
7
+ * or `run(api, problem)` does something else (the source pane applies a patch through the session).
8
+ */
9
+ export interface EditorFix {
10
+ id: string;
11
+ title: string;
12
+ /** Mechanical and unambiguous: offered first. */
13
+ safe?: boolean;
14
+ edits?: SourceEdit[];
15
+ caret?: number;
16
+ select?: [number, number];
17
+ run?(api: JsonEditorApi, problem: EditorProblem): void;
18
+ }
19
+ /** A problem drawn in the text: a squiggle over `from..to` (source offsets), its message, what could come next and its quick fixes. */
20
+ export interface EditorProblem {
21
+ from: number;
22
+ to: number;
23
+ severity: "error" | "warning" | "info";
24
+ message: string;
25
+ /** What to do, without a leading "Next:". */
26
+ next?: string;
27
+ /** The rule id, shown as the source of the problem. */
28
+ rule?: string;
29
+ fixes?: EditorFix[];
30
+ }
31
+
3
32
  /** All offsets address the original source in UTF-16, including CRLF pairs. */
4
33
  export interface JsonEditorApi {
34
+ /** Say `message` in the editor's live region (screen readers). */
35
+ say(message: string): void;
36
+ /** Apply several edits as one undoable change; `caret` or `select` is a position in the text after them. */
37
+ applyEdits(edits: SourceEdit[], options?: { caret?: number; select?: [number, number]; userEvent?: string }): void;
5
38
  getValue(): string;
6
39
  getSelection(): [number, number];
7
40
  hasFocus(): boolean;
8
41
  focus(): void;
9
42
  setSelection(from: number, to?: number): void;
10
- /** Replace one source range atomically; boundaries inside CRLF are rejected. */
11
- replace(from: number, to: number, text: string): void;
43
+ /**
44
+ * Replace one source range atomically; boundaries inside CRLF are rejected. The cursor moves to the end of the inserted text unless
45
+ * `keepSelection` is set. `userEvent` names the edit (default `"input.replace"`; a paste uses `"input.paste"`): it must not be
46
+ * `"input.type"` or `"input.complete"`, which re-indent the inserted lines.
47
+ */
48
+ replace(from: number, to: number, text: string, options?: { keepSelection?: boolean; userEvent?: string }): void;
49
+ /** Replace the whole text with `text` as one undoable edit; only what differs changes, and a whitespace-only difference (a format) is one change per run of whitespace, so the cursor stays on its token. */
50
+ replaceDiff(text: string): void;
51
+ /** The indentation unit now in effect: a tab character or some spaces (JSON: read once when a document is opened; YAML and Markdown: two spaces). */
52
+ indentUnit(): string;
12
53
  coordinates(): { left: number; top: number };
13
- /** Explicit, undoable whitespace formatting; returns false for invalid JSON. */
54
+ /** Explicit, undoable whitespace formatting of JSON, also while it does not parse; returns false in YAML and Markdown (the source pane formats those). */
14
55
  format(): boolean;
15
56
  }
16
57
  export interface JsonCodeEditorOptions {
@@ -23,7 +64,13 @@ export interface JsonCodeEditorOptions {
23
64
  /** Highlighting and behavior: "json" (default) adds the parse linter, field highlights and the option menu. */
24
65
  language?: "json" | "yaml" | "markdown";
25
66
  readOnly?: boolean;
26
- /** Live completion popup in JSON: property names and enum, boolean and catalog values at the cursor's JSON path, from the same data as the field menu (default false here; the source pane turns it on). Ctrl+Space still opens the field menu. */
67
+ /**
68
+ * Live completion list in JSON (default false here; the source pane turns it on): property names from the schema, ranked with the required and the
69
+ * common ones first, and values (enums, booleans, catalog ids, `{}` and `[]` templates with their required members, snippets for slides and the like)
70
+ * at the cursor's JSON path. Typing a quote, a word, `:` or the space after it opens the list; Ctrl+Space (Cmd+I on macOS) opens it anywhere (with
71
+ * the list open it shows or hides the documentation). Tab and Enter (and Shift+Tab, Shift+Enter) accept the selected item, Escape closes the list,
72
+ * Left, Right and Backspace inside the word keep it open. The field menu is on Alt+Shift+O; with `completions` off it is also on Ctrl+Space.
73
+ */
27
74
  completions?: boolean;
28
75
  catalogs?: JsonCatalogContext;
29
76
  /** Overrides the built-in menu, allowing an existing host UI to keep its appearance. */
@@ -34,6 +81,22 @@ export interface JsonCodeEditorOptions {
34
81
  * whole-document replace.
35
82
  */
36
83
  historyFallback?: { undo?(): boolean; redo?(): boolean };
84
+ /**
85
+ * Format document (Shift+Alt+F and Cmd/Ctrl+Shift+F). The source pane passes one that formats JSON, YAML and Markdown; without it the keys
86
+ * format JSON only.
87
+ */
88
+ onFormat?(): void;
89
+ /** Hover documentation over keys and catalog ids in JSON, and Cmd/Ctrl+K Cmd/Ctrl+I (default true). */
90
+ hover?: boolean;
91
+ /** Called with every message the live region says (hosts mirror them into a region of their own). */
92
+ announce?(message: string): void;
93
+ /**
94
+ * JSON syntax errors are underlined by the editor itself, one per cause, 300 ms after the last key (default true). A host that supplies every
95
+ * problem through `setProblems` (the source pane) turns it off.
96
+ */
97
+ syntaxProblems?: boolean;
98
+ /** Cmd/Ctrl+Shift+M: move focus to the host's problems list. Answer true when it did. */
99
+ onFocusProblems?(): boolean;
37
100
  onFocus?(): void;
38
101
  onHoverField?(path: string | null): void;
39
102
  onError?(error: unknown): void;
@@ -43,6 +106,22 @@ export interface JsonCodeEditor {
43
106
  /** Apply external source without emitting onChange or adding a local undo step. */
44
107
  update(code: string, invalid?: boolean, field?: string | null, describedBy?: string): void;
45
108
  setCatalogs(catalogs: JsonCatalogContext): void;
109
+ /** Draw the problems in the text (squiggles with their messages and quick fixes). Replaces the ones drawn before. */
110
+ setProblems(problems: readonly EditorProblem[]): void;
111
+ /** The problems drawn now, in position order, at their current source offsets. */
112
+ getProblems(): EditorProblem[];
113
+ /** F8: select the next problem and say what it is; wraps. */
114
+ nextProblem(): boolean;
115
+ /** Shift+F8. */
116
+ previousProblem(): boolean;
117
+ /** Cmd/Ctrl+.: the quick fixes of the problems at the caret. */
118
+ triggerQuickFix(): boolean;
119
+ /** Ctrl+Space: open the suggestion list at the caret. */
120
+ triggerCompletion(): boolean;
121
+ /** Whether a suggestion list is open or being asked for. */
122
+ isCompleting(): boolean;
123
+ /** Perform a quick fix. */
124
+ applyFix(fix: EditorFix, problem?: EditorProblem): void;
46
125
  setLanguage(language: "json" | "yaml" | "markdown"): void;
47
126
  setReadOnly(readOnly: boolean): void;
48
127
  /** Forget the typed edits CodeMirror can undo, for when the text they were typed against was replaced. The text is unchanged. */