stilo-editor 0.0.0-stage → 0.2.0

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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -2
  3. package/dist/characters--8h5uTeF.js +854 -0
  4. package/dist/content.css +398 -0
  5. package/dist/core/drop.d.ts +6 -0
  6. package/dist/core/editor.d.ts +120 -0
  7. package/dist/core/errors.d.ts +6 -0
  8. package/dist/core/events.d.ts +19 -0
  9. package/dist/core/extension-manager.d.ts +57 -0
  10. package/dist/core/extension.d.ts +48 -0
  11. package/dist/core/helpers/active.d.ts +33 -0
  12. package/dist/core/helpers/block-style.d.ts +25 -0
  13. package/dist/core/helpers/input-rules.d.ts +28 -0
  14. package/dist/core/helpers/lists.d.ts +20 -0
  15. package/dist/core/helpers/marks.d.ts +52 -0
  16. package/dist/core/helpers/nodes.d.ts +26 -0
  17. package/dist/core/helpers/parse-rules.d.ts +8 -0
  18. package/dist/core/helpers/word-count.d.ts +14 -0
  19. package/dist/core/html/css.d.ts +106 -0
  20. package/dist/core/html/parse.d.ts +7 -0
  21. package/dist/core/html/paste/base-styles.d.ts +7 -0
  22. package/dist/core/html/paste/google-docs.d.ts +7 -0
  23. package/dist/core/html/paste/index.d.ts +9 -0
  24. package/dist/core/html/paste/table-widths.d.ts +7 -0
  25. package/dist/core/html/paste/word.d.ts +8 -0
  26. package/dist/core/html/sanitize.d.ts +49 -0
  27. package/dist/core/html/serialize.d.ts +31 -0
  28. package/dist/core/types.d.ts +107 -0
  29. package/dist/extensions/blockquote/index.d.ts +15 -0
  30. package/dist/extensions/bold/index.d.ts +17 -0
  31. package/dist/extensions/bullet-list/index.d.ts +9 -0
  32. package/dist/extensions/clear-formatting/index.d.ts +12 -0
  33. package/dist/extensions/code/index.d.ts +15 -0
  34. package/dist/extensions/code-block/index.d.ts +24 -0
  35. package/dist/extensions/default-extensions/index.d.ts +57 -0
  36. package/dist/extensions/document/index.d.ts +17 -0
  37. package/dist/extensions/drop-cursor/index.d.ts +15 -0
  38. package/dist/extensions/gap-cursor/index.d.ts +6 -0
  39. package/dist/extensions/hard-break/index.d.ts +11 -0
  40. package/dist/extensions/heading/index.d.ts +25 -0
  41. package/dist/extensions/highlight/index.d.ts +24 -0
  42. package/dist/extensions/history/index.d.ts +21 -0
  43. package/dist/extensions/horizontal-rule/index.d.ts +11 -0
  44. package/dist/extensions/image/image-view.d.ts +27 -0
  45. package/dist/extensions/image/index.d.ts +29 -0
  46. package/dist/extensions/index.d.ts +36 -0
  47. package/dist/extensions/italic/index.d.ts +16 -0
  48. package/dist/extensions/line-height/index.d.ts +18 -0
  49. package/dist/extensions/link/index.d.ts +23 -0
  50. package/dist/extensions/list-item/index.d.ts +16 -0
  51. package/dist/extensions/move-block/index.d.ts +17 -0
  52. package/dist/extensions/ordered-list/index.d.ts +12 -0
  53. package/dist/extensions/paragraph/index.d.ts +14 -0
  54. package/dist/extensions/placeholder/index.d.ts +13 -0
  55. package/dist/extensions/search/index.d.ts +55 -0
  56. package/dist/extensions/strike/index.d.ts +16 -0
  57. package/dist/extensions/subscript/index.d.ts +16 -0
  58. package/dist/extensions/superscript/index.d.ts +17 -0
  59. package/dist/extensions/table/index.d.ts +70 -0
  60. package/dist/extensions/task-item/index.d.ts +24 -0
  61. package/dist/extensions/task-item/task-item-view.d.ts +24 -0
  62. package/dist/extensions/task-list/index.d.ts +11 -0
  63. package/dist/extensions/text/index.d.ts +12 -0
  64. package/dist/extensions/text-align/index.d.ts +18 -0
  65. package/dist/extensions/text-case/index.d.ts +14 -0
  66. package/dist/extensions/text-style/font-size.d.ts +21 -0
  67. package/dist/extensions/text-style/index.d.ts +30 -0
  68. package/dist/extensions/underline/index.d.ts +15 -0
  69. package/dist/index.d.ts +14 -0
  70. package/dist/index.js +1 -0
  71. package/dist/src-CV6pgfTS.js +2662 -0
  72. package/dist/style.css +1395 -0
  73. package/dist/ui/components/character-picker.d.ts +22 -0
  74. package/dist/ui/components/characters.d.ts +10 -0
  75. package/dist/ui/components/color-button.d.ts +23 -0
  76. package/dist/ui/components/dropdown.d.ts +34 -0
  77. package/dist/ui/components/find-bar.d.ts +24 -0
  78. package/dist/ui/components/font-size-input.d.ts +21 -0
  79. package/dist/ui/components/icon-menu.d.ts +20 -0
  80. package/dist/ui/components/icon.d.ts +3 -0
  81. package/dist/ui/components/link-bubble.d.ts +19 -0
  82. package/dist/ui/components/link-popover.d.ts +20 -0
  83. package/dist/ui/components/popover.d.ts +27 -0
  84. package/dist/ui/components/shortcut.d.ts +11 -0
  85. package/dist/ui/components/status-bar.d.ts +19 -0
  86. package/dist/ui/components/table-actions.d.ts +25 -0
  87. package/dist/ui/components/table-handles.d.ts +16 -0
  88. package/dist/ui/components/table-menu.d.ts +21 -0
  89. package/dist/ui/i18n/en.d.ts +3 -0
  90. package/dist/ui/i18n/it.d.ts +3 -0
  91. package/dist/ui/i18n/types.d.ts +4 -0
  92. package/dist/ui/index.d.ts +23 -0
  93. package/dist/ui/toolbar/fonts.d.ts +8 -0
  94. package/dist/ui/toolbar/items.d.ts +33 -0
  95. package/dist/ui/toolbar/palette.d.ts +6 -0
  96. package/dist/ui/toolbar/toolbar.d.ts +28 -0
  97. package/dist/ui/toolbar/types.d.ts +150 -0
  98. package/dist/ui.css +996 -0
  99. package/dist/ui.js +2350 -0
  100. package/package.json +96 -4
@@ -0,0 +1,398 @@
1
+ /* Stilo content styles: ProseMirror base CSS and defaults for the editable content. */
2
+
3
+ /*
4
+ * prosemirror-view/style/prosemirror.css and prosemirror-gapcursor/style/gapcursor.css.
5
+ * Copyright (C) 2015-2017 by Marijn Haverbeke and others, MIT license. Adapted to the lint
6
+ * rules: no `white-space: pre-wrap` fallback before `break-spaces`, and the separator image
7
+ * rule gains specificity instead of `!important`.
8
+ */
9
+ .ProseMirror {
10
+ position: relative;
11
+ }
12
+
13
+ .ProseMirror {
14
+ word-wrap: break-word;
15
+ white-space: break-spaces;
16
+ -webkit-font-variant-ligatures: none;
17
+ font-variant-ligatures: none;
18
+ font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
19
+ }
20
+
21
+ .ProseMirror pre {
22
+ white-space: pre-wrap;
23
+ }
24
+
25
+ .ProseMirror li {
26
+ position: relative;
27
+ }
28
+
29
+ .ProseMirror-hideselection *::selection {
30
+ background: transparent;
31
+ }
32
+ .ProseMirror-hideselection *::-moz-selection {
33
+ background: transparent;
34
+ }
35
+ .ProseMirror-hideselection {
36
+ caret-color: transparent;
37
+ }
38
+
39
+ /* See https://code.haverbeke.berlin/prosemirror/prosemirror/issues/1421#issuecomment-1759320191 */
40
+ .ProseMirror [draggable][contenteditable="false"] {
41
+ user-select: text;
42
+ }
43
+
44
+ .ProseMirror-selectednode {
45
+ outline: 2px solid #8cf;
46
+ }
47
+
48
+ /* Make sure li selections wrap around markers */
49
+
50
+ li.ProseMirror-selectednode {
51
+ outline: none;
52
+ }
53
+
54
+ li.ProseMirror-selectednode:after {
55
+ content: "";
56
+ position: absolute;
57
+ left: -32px;
58
+ right: -2px;
59
+ top: -2px;
60
+ bottom: -2px;
61
+ border: 2px solid #8cf;
62
+ pointer-events: none;
63
+ }
64
+
65
+ /* Protect against generic img rules */
66
+
67
+ .ProseMirror img.ProseMirror-separator {
68
+ display: inline;
69
+ border: none;
70
+ margin: 0;
71
+ }
72
+
73
+ .ProseMirror-gapcursor {
74
+ display: none;
75
+ pointer-events: none;
76
+ position: absolute;
77
+ }
78
+
79
+ .ProseMirror-gapcursor:after {
80
+ content: "";
81
+ display: block;
82
+ position: absolute;
83
+ top: -2px;
84
+ width: 20px;
85
+ border-top: 1px solid black;
86
+ animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
87
+ }
88
+
89
+ @keyframes ProseMirror-cursor-blink {
90
+ to {
91
+ visibility: hidden;
92
+ }
93
+ }
94
+
95
+ .ProseMirror-focused .ProseMirror-gapcursor {
96
+ display: block;
97
+ }
98
+
99
+ /*
100
+ * prosemirror-tables/style/tables.css. Copyright (C) 2015-2016 by Marijn Haverbeke and others,
101
+ * MIT license. Adapted: one `cursor` value, the colors from the Stilo custom properties.
102
+ */
103
+ .ProseMirror .tableWrapper {
104
+ overflow-x: auto;
105
+ }
106
+
107
+ .ProseMirror table {
108
+ border-collapse: collapse;
109
+ table-layout: fixed;
110
+ width: 100%;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .ProseMirror td,
115
+ .ProseMirror th {
116
+ vertical-align: top;
117
+ box-sizing: border-box;
118
+ position: relative;
119
+ }
120
+
121
+ .ProseMirror td:not([data-colwidth]):not(.column-resize-dragging),
122
+ .ProseMirror th:not([data-colwidth]):not(.column-resize-dragging) {
123
+ min-width: var(--default-cell-min-width);
124
+ }
125
+
126
+ .ProseMirror .column-resize-handle {
127
+ position: absolute;
128
+ right: -2px;
129
+ top: 0;
130
+ bottom: 0;
131
+ width: 4px;
132
+ z-index: 20;
133
+ background-color: var(--stilo-color-focus, #0969da);
134
+ pointer-events: none;
135
+ }
136
+
137
+ .ProseMirror.resize-cursor {
138
+ cursor: col-resize;
139
+ }
140
+
141
+ .ProseMirror .selectedCell:after {
142
+ z-index: 2;
143
+ position: absolute;
144
+ content: "";
145
+ left: 0;
146
+ right: 0;
147
+ top: 0;
148
+ bottom: 0;
149
+ background: var(--stilo-color-selected-cell, rgb(9 105 218 / 0.12));
150
+ pointer-events: none;
151
+ }
152
+
153
+ /* Stilo content defaults */
154
+
155
+ /* Lists: list items hold paragraphs, which must not add their own margins. */
156
+ .stilo-content ul,
157
+ .stilo-content ol {
158
+ margin: 0.5em 0;
159
+ padding-left: 1.75em;
160
+ }
161
+
162
+ .stilo-content li > p,
163
+ .stilo-content li > ul,
164
+ .stilo-content li > ol,
165
+ .stilo-content .stilo-task-item__content > p,
166
+ .stilo-content .stilo-task-item__content > ul,
167
+ .stilo-content .stilo-task-item__content > ol {
168
+ margin: 0;
169
+ }
170
+
171
+ /* Task lists: a checkbox in place of the bullet, done items struck through. In the editor the
172
+ item is a flex row: its <input> checkbox, then the content (ProseMirror needs a container that
173
+ holds only the content). Content shown elsewhere under the class stilo-content gets a box drawn
174
+ from data-checked instead. Either way the box is centered on the first line, not on the whole
175
+ item, which may hold more paragraphs and sub-lists. */
176
+ .stilo-content ul[data-type="taskList"] {
177
+ list-style: none;
178
+ }
179
+
180
+ .stilo-content.ProseMirror ul[data-type="taskList"] {
181
+ padding-left: 0.25em;
182
+ }
183
+
184
+ .stilo-content.ProseMirror ul[data-type="taskList"] > li {
185
+ display: flex;
186
+ gap: 0.5em;
187
+ }
188
+
189
+ .stilo-task-item__content {
190
+ flex: 1;
191
+ min-width: 0;
192
+ }
193
+
194
+ /* Form controls have their own font: inherit it, so that em and lh are those of the text. */
195
+ .stilo-task-item__checkbox {
196
+ flex: none;
197
+ width: 1em;
198
+ height: 1em;
199
+ margin: calc(0.5lh - 0.5em) 0 0;
200
+ font-size: inherit;
201
+ line-height: inherit;
202
+ accent-color: var(--stilo-color-focus, #0969da);
203
+ cursor: pointer;
204
+ }
205
+
206
+ .stilo-content:not(.ProseMirror) ul[data-type="taskList"] > li {
207
+ position: relative;
208
+ }
209
+
210
+ .stilo-content:not(.ProseMirror) ul[data-type="taskList"] > li::before {
211
+ content: "";
212
+ display: flex;
213
+ position: absolute;
214
+ top: calc(0.5lh - 0.5em);
215
+ left: -1.5em;
216
+ box-sizing: border-box;
217
+ width: 1em;
218
+ height: 1em;
219
+ align-items: center;
220
+ justify-content: center;
221
+ border: 1px solid var(--stilo-color-muted, #59636e);
222
+ border-radius: 0.2em;
223
+ color: #ffffff;
224
+ }
225
+
226
+ .stilo-content:not(.ProseMirror) ul[data-type="taskList"] > li[data-checked="true"]::before {
227
+ content: "\2713";
228
+ border-color: var(--stilo-color-focus, #0969da);
229
+ background: var(--stilo-color-focus, #0969da);
230
+ }
231
+
232
+ .stilo-content [data-checked="true"] > .stilo-task-item__content > p:first-child,
233
+ .stilo-content:not(.ProseMirror) [data-checked="true"] > p:first-child,
234
+ .stilo-content:not(.ProseMirror) [data-checked="true"]:not(:has(> p)) {
235
+ color: var(--stilo-color-muted, #59636e);
236
+ text-decoration: line-through;
237
+ }
238
+
239
+ .stilo-content blockquote {
240
+ margin: 0.5em 0;
241
+ padding-left: 1em;
242
+ border-left: 3px solid var(--stilo-color-border, #d1d9e0);
243
+ color: var(--stilo-color-muted, #59636e);
244
+ }
245
+
246
+ .stilo-content hr {
247
+ margin: 1em 0;
248
+ border: none;
249
+ border-top: 1px solid var(--stilo-color-border, #d1d9e0);
250
+ }
251
+
252
+ .stilo-content code {
253
+ padding: 0.1em 0.3em;
254
+ border-radius: 0.25em;
255
+ background: var(--stilo-color-hover, #eef1f4);
256
+ font-family: ui-monospace, monospace;
257
+ font-size: 0.9em;
258
+ }
259
+
260
+ /* Code block: the <code> inside it drops the inline code box. */
261
+ .stilo-content pre {
262
+ margin: 0.5em 0;
263
+ padding: 0.75em 1em;
264
+ border-radius: 0.375em;
265
+ background: var(--stilo-color-hover, #eef1f4);
266
+ font-family: ui-monospace, monospace;
267
+ font-size: 0.9em;
268
+ line-height: 1.5;
269
+ }
270
+
271
+ .stilo-content pre code {
272
+ padding: 0;
273
+ border-radius: 0;
274
+ background: none;
275
+ font-size: inherit;
276
+ }
277
+
278
+ /* Tables: a cell holds blocks, whose outer margins would pad the cell twice. */
279
+ .stilo-content .tableWrapper {
280
+ margin: 1em 0;
281
+ }
282
+
283
+ .stilo-content td,
284
+ .stilo-content th {
285
+ padding: 6px 8px;
286
+ border: 1px solid var(--stilo-color-border, #d1d9e0);
287
+ }
288
+
289
+ .stilo-content th {
290
+ background: var(--stilo-color-table-header, #f6f8fa);
291
+ font-weight: 600;
292
+ text-align: start;
293
+ }
294
+
295
+ .stilo-content td > :first-child,
296
+ .stilo-content th > :first-child {
297
+ margin-top: 0;
298
+ }
299
+
300
+ /* The resize handle shown on hover is the last child of its cell: the block before it is the last
301
+ block, otherwise its margin would come back and the table grow under the mouse. */
302
+ .stilo-content td > :last-child,
303
+ .stilo-content th > :last-child,
304
+ .stilo-content td > :has(+ .column-resize-handle:last-child),
305
+ .stilo-content th > :has(+ .column-resize-handle:last-child) {
306
+ margin-bottom: 0;
307
+ }
308
+
309
+ /* Images: never wider than the content; with width and height they keep their ratio. */
310
+ .stilo-content img {
311
+ max-width: 100%;
312
+ height: auto;
313
+ }
314
+
315
+ /* Image in the editor: two side handles, inside the image (a top-level block clips what
316
+ overflows it, see "Large documents" below), shown while the image is selected. Only a selected
317
+ image is positioned: a positioned element paints over the caret placed right after it. */
318
+ .stilo-image {
319
+ display: inline-block;
320
+ max-width: 100%;
321
+ }
322
+
323
+ .stilo-image.ProseMirror-selectednode {
324
+ position: relative;
325
+ }
326
+
327
+ .stilo-image img {
328
+ display: block;
329
+ }
330
+
331
+ .stilo-image__handle {
332
+ display: none;
333
+ position: absolute;
334
+ top: 50%;
335
+ width: 6px;
336
+ height: min(48px, 50%);
337
+ min-height: 16px;
338
+ transform: translateY(-50%);
339
+ border: 1px solid #ffffff;
340
+ border-radius: 3px;
341
+ background: var(--stilo-color-text, #1f2328);
342
+ opacity: 0.7;
343
+ cursor: ew-resize;
344
+ touch-action: none;
345
+ }
346
+
347
+ .stilo-image__handle--left {
348
+ left: 6px;
349
+ }
350
+
351
+ .stilo-image__handle--right {
352
+ right: 6px;
353
+ }
354
+
355
+ .stilo-content[contenteditable="true"] .stilo-image.ProseMirror-selectednode .stilo-image__handle {
356
+ display: block;
357
+ }
358
+
359
+ .stilo-image__handle:hover {
360
+ opacity: 1;
361
+ }
362
+
363
+ /* Placeholder: shown only while the content is editable. */
364
+ .stilo-content[contenteditable="true"] .stilo-placeholder::before {
365
+ content: attr(data-placeholder);
366
+ float: left;
367
+ height: 0;
368
+ color: var(--stilo-color-muted, #59636e);
369
+ pointer-events: none;
370
+ }
371
+
372
+ /*
373
+ * Large documents (ADR 0003): the browser skips style, layout and paint of the top-level blocks
374
+ * outside the viewport. Real typing in 10k paragraphs went from 144 ms to 24 ms per key (median,
375
+ * Chromium). A skipped block is 3em tall until it has been rendered once.
376
+ */
377
+ .stilo-content > * {
378
+ content-visibility: auto;
379
+ contain-intrinsic-size: auto 3em;
380
+ }
381
+
382
+ /* Containment stops margins from collapsing through a block: trim them inside a blockquote. */
383
+ .stilo-content blockquote > :first-child {
384
+ margin-top: 0;
385
+ }
386
+
387
+ .stilo-content blockquote > :last-child {
388
+ margin-bottom: 0;
389
+ }
390
+
391
+ /* Find and replace (T-043): the matches and the selected one */
392
+ .stilo-search-match {
393
+ background: var(--stilo-color-search-match, #fff1a8);
394
+ }
395
+
396
+ .stilo-search-match--current {
397
+ background: var(--stilo-color-search-current, #ffb347);
398
+ }
@@ -0,0 +1,6 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ /**
3
+ * Makes a drag-and-drop move of whole textblocks behave as a move of blocks (ARCHITECTURE §8.2):
4
+ * dropped inside text they stay blocks, and the blocks they came from do not stay behind empty.
5
+ */
6
+ export declare function createDropMovePlugin(): Plugin;
@@ -0,0 +1,120 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import { EditorState } from 'prosemirror-state';
3
+ import { EditorView } from 'prosemirror-view';
4
+ import { type EventHandler } from './events';
5
+ import type { CommandRunners, EditorEvents, EditorOptions, JSONContent, SetContentOptions } from './types';
6
+ /**
7
+ * Transaction meta key of the empty transaction applied to every new `EditorState`: plugins can
8
+ * normalize the loaded document in `appendTransaction` (e.g. the `Table` extension fixes
9
+ * irregular tables), before the view shows it and without an undo step.
10
+ */
11
+ export declare const LOAD_META = "stiloLoad";
12
+ /**
13
+ * The Stilo editor. See docs/ARCHITECTURE.md §5.
14
+ *
15
+ * ```ts
16
+ * const editor = new Editor({
17
+ * element: document.querySelector('#editor')!,
18
+ * extensions: [defaultExtensions()],
19
+ * content: '<p>Hello <strong>world</strong></p>',
20
+ * });
21
+ * ```
22
+ */
23
+ export declare class Editor {
24
+ /** The ProseMirror view (escape hatch). */
25
+ readonly view: EditorView;
26
+ /** The schema built from the extensions. */
27
+ readonly schema: Schema;
28
+ /**
29
+ * Runs a command: `editor.commands.toggleBold()` returns whether it applied;
30
+ * `editor.commands.toggleBold.can()` tells whether it would apply, without applying it.
31
+ */
32
+ readonly commands: CommandRunners;
33
+ private readonly manager;
34
+ private readonly plugins;
35
+ private readonly emitter;
36
+ private readonly compactParagraphs;
37
+ private destroyed;
38
+ /**
39
+ * Creates the editor:
40
+ *
41
+ * 1. `ExtensionManager` from `options.extensions` → `schema`;
42
+ * 2. `createPlugins(this, emit)`, then the core `createDropMovePlugin()`; command runners are built
43
+ * from the manager's commands;
44
+ * 3. initial document from `parseHTML(schema, options.content ?? '')`;
45
+ * 4. `EditorView` mounted inside `options.element`, with attributes `class="stilo-content"`,
46
+ * `role="textbox"`, `aria-multiline="true"`, `aria-label` (if given); `editable` from
47
+ * options (default `true`); `transformPastedHTML` → `cleanPastedHTML`, then
48
+ * `sanitizeToString`; `clipboardSerializer` → `createClipboardSerializer`; transactions go
49
+ * through the editor's dispatch (see events in `EditorEvents`);
50
+ * 5. `autofocus`: `true`/`'start'` → focus at start, `'end'` → focus at end.
51
+ */
52
+ constructor(options: EditorOptions);
53
+ /** Current editor state. */
54
+ get state(): EditorState;
55
+ /** `true` when the document is a single empty paragraph. */
56
+ get isEmpty(): boolean;
57
+ /** Whether the content is editable. */
58
+ get isEditable(): boolean;
59
+ /** Whether {@link destroy} has been called. */
60
+ get isDestroyed(): boolean;
61
+ /**
62
+ * Canonical HTML of the document (`serializeHTML`, honoring `compactParagraphs`). Empty
63
+ * document → `<p></p>`.
64
+ */
65
+ getHTML(): string;
66
+ /**
67
+ * Replaces the content with `html` (sanitized and parsed with `parseHTML`).
68
+ *
69
+ * - `resetHistory: false` (default): one transaction replacing the whole content
70
+ * (`tr.replaceWith(0, doc.content.size, newDoc.content)`), selection at document start;
71
+ * undoable as its own step (`closeHistory`: never grouped with the changes before it).
72
+ * - `resetHistory: true`: a new `EditorState` with the same plugins; `input` is emitted with
73
+ * `transaction: null`.
74
+ * - `emitInput: false` suppresses the `input` event.
75
+ */
76
+ setHTML(html: string, options?: SetContentOptions): void;
77
+ /** The document as ProseMirror JSON (`doc.toJSON()`). */
78
+ getJSON(): JSONContent;
79
+ /** Same as {@link setHTML}, from ProseMirror JSON (`Node.fromJSON`). */
80
+ setJSON(json: JSONContent, options?: SetContentOptions): void;
81
+ /**
82
+ * Plain text: `doc.textBetween(0, doc.content.size, '\n\n')`. Leaf nodes provide their own
83
+ * text through `leafText` in their node spec (hard break → `\n`).
84
+ */
85
+ getText(): string;
86
+ /**
87
+ * Whether a mark or node named `name` is active (`isMarkActive` / `isNodeActive`).
88
+ * Unknown names return `false`.
89
+ */
90
+ isActive(name: string, attrs?: Record<string, unknown>): boolean;
91
+ /**
92
+ * Attributes of the mark or node named `name` at the selection (`getMarkAttributes` /
93
+ * `getNodeAttributes`), e.g. the `href` of the link at the cursor. `{}` when there is none or
94
+ * the name is unknown.
95
+ */
96
+ getAttributes(name: string): Record<string, unknown>;
97
+ /** Switches read-only mode on/off. */
98
+ setEditable(editable: boolean): void;
99
+ /** Focuses the editor; optionally moves the cursor to the start or end of the document. */
100
+ focus(position?: 'start' | 'end'): void;
101
+ /** Removes focus from the editor. */
102
+ blur(): void;
103
+ /** Registers an event handler. */
104
+ on<E extends keyof EditorEvents>(event: E, handler: EventHandler<EditorEvents[E]>): void;
105
+ /** Removes an event handler. */
106
+ off<E extends keyof EditorEvents>(event: E, handler: EventHandler<EditorEvents[E]>): void;
107
+ /** Emits `destroy`, destroys the view, removes all handlers. Calling it twice is a no-op. */
108
+ destroy(): void;
109
+ private createState;
110
+ /** Builds `commands` from the extension commands; `can` runs without `dispatch`. */
111
+ private createRunners;
112
+ private setContent;
113
+ /**
114
+ * Removes the DOM selection when it lies inside the editor. Replacing many nodes while the
115
+ * browser tracks a selection in them is much slower (Chromium, 50,000 paragraphs: 42 s →
116
+ * 1.6 s); ProseMirror puts the selection back after the update when the editor has focus.
117
+ */
118
+ private clearDOMSelection;
119
+ private dispatch;
120
+ }
@@ -0,0 +1,6 @@
1
+ /** Error thrown by Stilo for invalid configuration or API misuse. */
2
+ export declare class StiloError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** Placeholder for functions whose implementation is assigned to a task. */
6
+ export declare function notImplemented(name: string): never;
@@ -0,0 +1,19 @@
1
+ /** A handler for an event payload. */
2
+ export type EventHandler<T> = (payload: T) => void;
3
+ /**
4
+ * Minimal typed event emitter.
5
+ *
6
+ * - `on` registers a handler; registering the same handler twice for the same event has no
7
+ * additional effect.
8
+ * - `off` removes a handler; removing an unknown handler is a no-op.
9
+ * - `emit` calls the handlers of the event in registration order. Handlers added or removed
10
+ * during an `emit` do not affect the current emit.
11
+ * - `clear` removes all handlers of all events.
12
+ */
13
+ export declare class EventEmitter<Events extends object> {
14
+ private handlers;
15
+ on<E extends keyof Events>(event: E, handler: EventHandler<Events[E]>): void;
16
+ off<E extends keyof Events>(event: E, handler: EventHandler<Events[E]>): void;
17
+ emit<E extends keyof Events>(event: E, payload: Events[E]): void;
18
+ clear(): void;
19
+ }
@@ -0,0 +1,57 @@
1
+ import { Schema } from 'prosemirror-model';
2
+ import type { Plugin } from 'prosemirror-state';
3
+ import type { Editor } from './editor';
4
+ import type { Extension, ExtensionContext } from './extension';
5
+ import type { CommandFactory } from './types';
6
+ /**
7
+ * Schema order of the built-in marks, outermost first. It defines how marks nest in the HTML
8
+ * output (docs/ARCHITECTURE.md §6.2). Marks not listed here come after these, in extension
9
+ * order.
10
+ */
11
+ export declare const MARK_ORDER: readonly string[];
12
+ /** Nodes every schema must contain. */
13
+ export declare const REQUIRED_NODES: readonly string[];
14
+ /**
15
+ * Flattens nested arrays one level and sorts extensions by `priority` (descending), keeping the
16
+ * order of appearance for equal priorities (stable sort).
17
+ */
18
+ export declare function sortExtensions(extensions: Array<Extension | Extension[]>): Extension[];
19
+ /**
20
+ * Builds the schema, plugins and commands from a list of extensions.
21
+ * See docs/ARCHITECTURE.md §6.2 for the rules.
22
+ */
23
+ export declare class ExtensionManager {
24
+ /** The extensions, flattened and sorted with {@link sortExtensions}. */
25
+ readonly extensions: readonly Extension[];
26
+ readonly schema: Schema;
27
+ private readonly commandFactories;
28
+ /**
29
+ * Sorts the extensions and creates the schema:
30
+ *
31
+ * - duplicate extension names → `StiloError`;
32
+ * - nodes from `spec.nodes(options)` in extension order; duplicate node name → `StiloError`;
33
+ * - marks from `spec.marks(options)`, ordered by {@link MARK_ORDER} then extension order;
34
+ * duplicate mark name → `StiloError`;
35
+ * - any of {@link REQUIRED_NODES} missing → `StiloError`;
36
+ * - `topNode` is `doc`.
37
+ */
38
+ constructor(extensions: Array<Extension | Extension[]>);
39
+ /**
40
+ * Command factories of all extensions, by command name. Empty until {@link createPlugins}
41
+ * has been called.
42
+ */
43
+ get commands(): ReadonlyMap<string, CommandFactory>;
44
+ /**
45
+ * Called once by the editor. For each extension in order, with context
46
+ * `{ options, schema, editor, emit }`: collects `commands` (duplicate name → `StiloError`),
47
+ * `inputRules`, `keymap`, `plugins`. Returns, in this order:
48
+ *
49
+ * 1. one `inputRules({ rules })` plugin with all input rules;
50
+ * 2. `keymap({ Backspace: undoInputRule })`: it applies only right after an input rule, so it
51
+ * takes precedence over the extensions (Backspace after `- ` gives back `- `);
52
+ * 3. one `keymap()` plugin per extension that defines a keymap, in extension order;
53
+ * 4. all extension plugins, in extension order;
54
+ * 5. `keymap(baseKeymap)` from prosemirror-commands.
55
+ */
56
+ createPlugins(editor: Editor, emit: ExtensionContext<object>['emit']): Plugin[];
57
+ }
@@ -0,0 +1,48 @@
1
+ import type { InputRule } from 'prosemirror-inputrules';
2
+ import type { MarkSpec, NodeSpec, Schema } from 'prosemirror-model';
3
+ import type { Command, Plugin } from 'prosemirror-state';
4
+ import type { Editor } from './editor';
5
+ import type { CommandFactories, EditorEvents } from './types';
6
+ /** Context passed to the runtime parts of an extension. */
7
+ export interface ExtensionContext<Options> {
8
+ options: Readonly<Options>;
9
+ schema: Schema;
10
+ editor: Editor;
11
+ /**
12
+ * Emits an editor event. An extension adds its own events to `EditorEvents` by module
13
+ * augmentation, as it does for `Commands`.
14
+ */
15
+ emit<E extends keyof EditorEvents>(event: E, payload: EditorEvents[E]): void;
16
+ }
17
+ /** Definition of an extension. See docs/ARCHITECTURE.md §6. */
18
+ export interface ExtensionSpec<Options extends object = Record<string, never>> {
19
+ /** Unique camelCase name, equal to the main node/mark of the extension. */
20
+ name: string;
21
+ /** Default 100. Higher = earlier in schema order and higher keymap precedence. */
22
+ priority?: number;
23
+ defaultOptions?: Options;
24
+ nodes?(options: Readonly<Options>): Record<string, NodeSpec>;
25
+ marks?(options: Readonly<Options>): Record<string, MarkSpec>;
26
+ commands?(ctx: ExtensionContext<Options>): Partial<CommandFactories>;
27
+ keymap?(ctx: ExtensionContext<Options>): Record<string, Command>;
28
+ inputRules?(ctx: ExtensionContext<Options>): InputRule[];
29
+ plugins?(ctx: ExtensionContext<Options>): Plugin[];
30
+ }
31
+ /** A configured extension instance, created by the factory returned by {@link defineExtension}. */
32
+ export interface Extension<Options extends object = object> {
33
+ readonly name: string;
34
+ readonly priority: number;
35
+ readonly options: Readonly<Options>;
36
+ readonly spec: ExtensionSpec<Options>;
37
+ }
38
+ /** Default extension priority. */
39
+ export declare const DEFAULT_PRIORITY = 100;
40
+ /**
41
+ * Defines an extension. Returns a factory that creates configured instances:
42
+ *
43
+ * ```ts
44
+ * export const Heading = defineExtension<{ levels: number[] }>({ name: 'heading', ... });
45
+ * const heading = Heading({ levels: [1, 2, 3] });
46
+ * ```
47
+ */
48
+ export declare function defineExtension<Options extends object = Record<string, never>>(spec: ExtensionSpec<Options>): (options?: Partial<Options>) => Extension<Options>;
@@ -0,0 +1,33 @@
1
+ import type { Attrs, MarkType, NodeType } from 'prosemirror-model';
2
+ import { type EditorState } from 'prosemirror-state';
3
+ /**
4
+ * Whether `actual` contains every key of `expected` with a strictly equal (`===`) value.
5
+ * `undefined` `expected` always matches.
6
+ */
7
+ export declare function attrsMatch(actual: Attrs, expected?: Record<string, unknown>): boolean;
8
+ /**
9
+ * Whether a mark of `type` (with matching `attrs`, see {@link attrsMatch}) is active:
10
+ *
11
+ * - empty selection: present in `state.storedMarks`, or if those are `null`, in
12
+ * `selection.$from.marks()`;
13
+ * - non-empty selection: every text node between `from` and `to` has the mark, and there is at
14
+ * least one text node. Traverse only the selected range (`doc.nodesBetween(from, to)`).
15
+ */
16
+ export declare function isMarkActive(state: EditorState, type: MarkType, attrs?: Record<string, unknown>): boolean;
17
+ /**
18
+ * Whether a node of `type` (with matching `attrs`) is active: the selection is a
19
+ * `NodeSelection` of such a node, or `selection.$from` has such an ancestor (any depth,
20
+ * including the document itself).
21
+ */
22
+ export declare function isNodeActive(state: EditorState, type: NodeType, attrs?: Record<string, unknown>): boolean;
23
+ /**
24
+ * Attributes of the mark of `type` at the selection, `{}` when there is none: for an empty
25
+ * selection the mark in `state.storedMarks` (or `$from.marks()`), otherwise the first one found
26
+ * in the selected range.
27
+ */
28
+ export declare function getMarkAttributes(state: EditorState, type: MarkType): Attrs;
29
+ /**
30
+ * Attributes of the node of `type` at the selection, `{}` when there is none: the selected node
31
+ * (`NodeSelection`), or the deepest ancestor of `selection.$from`.
32
+ */
33
+ export declare function getNodeAttributes(state: EditorState, type: NodeType): Attrs;