stilo-editor 0.1.0 → 0.2.1

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 (57) hide show
  1. package/README.md +76 -129
  2. package/dist/content.css +238 -1
  3. package/dist/core/editor.d.ts +15 -3
  4. package/dist/core/extension-manager.d.ts +3 -3
  5. package/dist/core/extension.d.ts +6 -1
  6. package/dist/core/helpers/block-style.d.ts +25 -0
  7. package/dist/core/helpers/input-rules.d.ts +8 -1
  8. package/dist/core/helpers/lists.d.ts +10 -2
  9. package/dist/core/helpers/marks.d.ts +14 -0
  10. package/dist/core/helpers/nodes.d.ts +12 -0
  11. package/dist/core/helpers/word-count.d.ts +14 -0
  12. package/dist/core/html/css.d.ts +16 -1
  13. package/dist/core/html/paste/base-styles.d.ts +7 -0
  14. package/dist/core/html/paste/google-docs.d.ts +7 -0
  15. package/dist/core/html/paste/index.d.ts +9 -0
  16. package/dist/core/html/paste/table-widths.d.ts +7 -0
  17. package/dist/core/html/paste/word.d.ts +8 -0
  18. package/dist/core/html/serialize.d.ts +2 -2
  19. package/dist/extensions/code-block/index.d.ts +24 -0
  20. package/dist/extensions/default-extensions/index.d.ts +11 -0
  21. package/dist/extensions/heading/index.d.ts +2 -1
  22. package/dist/extensions/image/image-view.d.ts +27 -0
  23. package/dist/extensions/image/index.d.ts +29 -0
  24. package/dist/extensions/index.d.ts +10 -1
  25. package/dist/extensions/line-height/index.d.ts +18 -0
  26. package/dist/extensions/list-item/index.d.ts +1 -1
  27. package/dist/extensions/paragraph/index.d.ts +4 -4
  28. package/dist/extensions/search/index.d.ts +55 -0
  29. package/dist/extensions/table/index.d.ts +70 -0
  30. package/dist/extensions/task-item/index.d.ts +24 -0
  31. package/dist/extensions/task-item/task-item-view.d.ts +24 -0
  32. package/dist/extensions/task-list/index.d.ts +11 -0
  33. package/dist/extensions/text-align/index.d.ts +18 -0
  34. package/dist/extensions/text-case/index.d.ts +14 -0
  35. package/dist/extensions/text-style/font-size.d.ts +21 -0
  36. package/dist/extensions/text-style/index.d.ts +18 -2
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -1
  39. package/dist/src-CV6pgfTS.js +2662 -0
  40. package/dist/style.css +599 -1
  41. package/dist/ui/components/dropdown.d.ts +9 -0
  42. package/dist/ui/components/find-bar.d.ts +24 -0
  43. package/dist/ui/components/font-size-input.d.ts +21 -0
  44. package/dist/ui/components/icon-menu.d.ts +20 -0
  45. package/dist/ui/components/status-bar.d.ts +19 -0
  46. package/dist/ui/components/table-actions.d.ts +25 -0
  47. package/dist/ui/components/table-handles.d.ts +16 -0
  48. package/dist/ui/components/table-menu.d.ts +21 -0
  49. package/dist/ui/i18n/types.d.ts +1 -1
  50. package/dist/ui/index.d.ts +6 -2
  51. package/dist/ui/toolbar/fonts.d.ts +8 -0
  52. package/dist/ui/toolbar/items.d.ts +23 -2
  53. package/dist/ui/toolbar/types.d.ts +67 -4
  54. package/dist/ui.css +361 -0
  55. package/dist/ui.js +1361 -303
  56. package/package.json +19 -9
  57. package/dist/src-3tAxKI0c.js +0 -1316
package/README.md CHANGED
@@ -1,95 +1,44 @@
1
- <h1 align="center">
1
+ <p align="center">
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/logo-dark.svg">
4
4
  <img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/logo.svg" alt="Stilo" width="300">
5
5
  </picture>
6
- </h1>
6
+ </p>
7
7
 
8
- Open-source HTML5 WYSIWYG editor built on [ProseMirror](https://prosemirror.net).
9
- Clean HTML in and out, headless core, no paid tiers.
8
+ <h3 align="center">A complete rich text editor. Everything included, ready in minutes.</h3>
10
9
 
11
- > [!WARNING]
12
- > **Preview release.** Versions `0.x` are early previews: the editor works, but APIs may change
13
- > in any minor version until `1.0.0`. Read the release notes before updating.
10
+ <p align="center">
11
+ Free and open source (MIT) · clean HTML in and out · built on ProseMirror
12
+ </p>
14
13
 
15
- ## Why Stilo
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/stilo-editor"><img src="https://img.shields.io/npm/v/stilo-editor" alt="npm version"></a>
16
+ <a href="https://github.com/kojak8791/stilo-editor/actions/workflows/ci.yml"><img src="https://github.com/kojak8791/stilo-editor/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
17
+ <img src="https://img.shields.io/npm/types/stilo-editor" alt="TypeScript types included">
18
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/stilo-editor" alt="MIT license"></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://kojak8791.github.io/stilo-editor/"><strong>Live demo</strong></a> ·
23
+ <a href="docs/API.md"><strong>API</strong></a> ·
24
+ <a href="docs/FEATURES.md"><strong>All features</strong></a>
25
+ </p>
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot.png" alt="Stilo with the default toolbar: formatted text, a checklist, a table and the link popover open" width="800">
29
+ </p>
16
30
 
17
- - **Built for very large documents.** Typing stays immediate in a document longer than most
18
- novels (see [Large documents](#large-documents)). Measured in a real browser, checked against
19
- budgets, not assumed.
20
- - **Entirely open source (MIT).** Aiming at the feature level of Tiptap or CKEditor 5, with no paid
21
- tiers and no closed "pro" extensions.
22
- - **HTML is the public format.** Content goes in as HTML and comes out as clean, semantic,
23
- self-contained HTML, with a strict allowlist for tags, attributes and inline styles.
24
- - **Headless core, optional UI.** The core works without any UI; the default toolbar and menus are
25
- a separate, replaceable module.
26
- - **Extensible by design.** Every feature is an extension with the same shape.
27
- - **Framework-agnostic.** Vanilla TypeScript first; React/Vue wrappers later as thin layers.
28
-
29
- ## Large documents
30
-
31
- Stilo is made for long documents: contracts, manuals, books, reports. Typing feels immediate in
32
- a document of 10,000 paragraphs — about 400,000 words, longer than most novels — and the editor
33
- stays usable at five times that size, without pagination tricks and without splitting the
34
- content. Formatting, search and selection keep up with the size of the text.
35
-
36
- This is measured, not promised. A benchmark drives the editor in a real browser on generated
37
- documents and times everything from loading and exporting the HTML down to the milliseconds
38
- between a key press and the character on screen; every change is checked against performance
39
- budgets. What it means in practice, the numbers and the limits:
40
- [Performance](docs/PERFORMANCE.md).
41
-
42
- ## Features
43
-
44
- What the editor does today, with `defaultExtensions` and the default toolbar. On macOS, use
45
- <kbd>⌘</kbd> for <kbd>Ctrl</kbd> and <kbd>Option</kbd> for <kbd>Alt</kbd>. The *Markdown*
46
- column is what you type to get the format as you write.
47
-
48
- | Feature | Keyboard | Markdown | Notes |
49
- |---|---|---|---|
50
- | **Text** | | | |
51
- | Bold | <kbd>Ctrl</kbd>+<kbd>B</kbd> | `**text**` | |
52
- | Italic | <kbd>Ctrl</kbd>+<kbd>I</kbd> | `*text*` `_text_` | |
53
- | Underline | <kbd>Ctrl</kbd>+<kbd>U</kbd> | | |
54
- | Strikethrough | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> | `~~text~~` | |
55
- | Inline code | <kbd>Ctrl</kbd>+<kbd>E</kbd> | `` `text` `` | |
56
- | Subscript | <kbd>Ctrl</kbd>+<kbd>,</kbd> | | Subscript and superscript exclude each other |
57
- | Superscript | <kbd>Ctrl</kbd>+<kbd>.</kbd> | | |
58
- | Text color | | | The button applies the last color; its arrow opens a palette, "No color" and a custom color |
59
- | Highlight | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd> | `==text==` | Same button as text color; the shortcut applies the default highlight |
60
- | Clear formatting | <kbd>Ctrl</kbd>+<kbd>\\</kbd> | | Links stay |
61
- | **Links** | | | |
62
- | Link | <kbd>Ctrl</kbd>+<kbd>K</kbd> | | A popover sets the URL and "open in new tab"; on a link, a small bubble offers open, edit and remove |
63
- | **Blocks** | | | |
64
- | Paragraph | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>0</kbd> | | |
65
- | Headings 1–6 | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>1</kbd>…<kbd>6</kbd> | `# ` … `###### ` | Also in the toolbar dropdown |
66
- | Quote | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd> | `> ` | |
67
- | Horizontal line | | `---` | |
68
- | Line break | <kbd>Shift</kbd>+<kbd>Enter</kbd> | | New line in the same paragraph |
69
- | Move block up / down | <kbd>Alt</kbd>+<kbd>↑</kbd> / <kbd>Alt</kbd>+<kbd>↓</kbd> | | The block or list item at the cursor, or all the selected ones; sub-items move along |
70
- | **Lists** | | | |
71
- | Bulleted list | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>8</kbd> | `- ` `* ` `+ ` | |
72
- | Numbered list | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>7</kbd> | `1. ` | Starts from the number you type |
73
- | Indent / outdent item | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | | |
74
- | **Insert** | | | |
75
- | Special characters and emoji | | | A popover with search, symbols and emoji; it stays open to insert several in a row |
76
- | **Editing** | | | |
77
- | Undo | <kbd>Ctrl</kbd>+<kbd>Z</kbd> | | <kbd>Backspace</kbd> right after a Markdown shortcut undoes it |
78
- | Redo | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or <kbd>Ctrl</kbd>+<kbd>Y</kbd> | | |
79
- | Paste | <kbd>Ctrl</kbd>+<kbd>V</kbd> | | HTML is cleaned up; plain text lines become paragraphs |
80
-
81
- **Keyboard and accessibility.** Most shortcuts are the ones of Google Docs; the others follow
82
- the usual conventions of web editors, and every toolbar tooltip shows its shortcut. The toolbar
83
- follows the WAI-ARIA *Toolbar* pattern: <kbd>Alt</kbd>+<kbd>F10</kbd> moves the focus from the
84
- text to the toolbar, the arrow keys move between buttons, <kbd>Esc</kbd> goes back to the text.
85
- Every popover works with the keyboard alone and <kbd>Esc</kbd> closes it.
86
-
87
- ## Usage
31
+ ## Quick start
88
32
 
89
33
  ```sh
90
34
  npm install stilo-editor
91
35
  ```
92
36
 
37
+ ```html
38
+ <div id="toolbar"></div>
39
+ <div id="editor"></div>
40
+ ```
41
+
93
42
  ```ts
94
43
  import 'stilo-editor/style.css';
95
44
  import { Editor, defaultExtensions } from 'stilo-editor';
@@ -106,68 +55,66 @@ createToolbar(editor, { container: document.querySelector<HTMLElement>('#toolbar
106
55
  editor.getHTML(); // '<p>Hello <strong>world</strong></p>'
107
56
  ```
108
57
 
109
- Options, commands, events, extensions and the toolbar: **[API reference](docs/API.md)**.
110
- Without the default toolbar, import `stilo-editor/content.css` instead of `style.css` and build
111
- your own controls on the same API.
58
+ ✅ That is the whole setup.
59
+
60
+ ### Want your own UI?
61
+
62
+ Skip the toolbar: the core is headless.
63
+
64
+ ```ts
65
+ import 'stilo-editor/content.css';
66
+ import { Editor, defaultExtensions } from 'stilo-editor';
67
+
68
+ const editor = new Editor({
69
+ element: document.querySelector<HTMLElement>('#editor')!,
70
+ extensions: [defaultExtensions()],
71
+ });
112
72
 
113
- ## Support
73
+ document.querySelector('#bold')!.addEventListener('click', () => editor.commands.toggleBold());
74
+ document.querySelector('#undo')!.addEventListener('click', () => editor.commands.undo());
75
+ ```
114
76
 
115
- - **Bugs and feature requests:** open an
116
- [issue](https://github.com/kojak8791/stilo-editor/issues/new/choose); the forms ask for what
117
- is needed to reproduce a bug (steps, Stilo version, browser).
118
- - **Questions:** open an issue too, with a short title that says what you are trying to do.
119
- - **Security vulnerabilities:** never in a public issue; report them privately as described in
120
- [SECURITY.md](SECURITY.md).
77
+ Commands, button states and events: [API reference](docs/API.md).
121
78
 
122
- Contributions are welcome: start from [CONTRIBUTING.md](CONTRIBUTING.md).
79
+ ## What's included
123
80
 
124
- ## Roadmap
81
+ - **Text:** bold, italic, underline, strikethrough, code, sub/superscript, fonts, sizes,
82
+ colors, highlight, text case
83
+ - **Blocks:** headings, quotes, code blocks, alignment, line spacing, move up/down
84
+ - **Lists:** bulleted, numbered, checklists
85
+ - **Tables:** insert, merge and split, row and column handles, resizable columns
86
+ - **Images:** paste and resize
87
+ - **Tools:** links, find and replace, special characters and emoji, word count
125
88
 
126
- | Milestone | Content | Status |
127
- |---|---|---|
128
- | M0 — Foundations | Docs, scaffolding, core contracts, HTML pipeline, test helpers, `Bold` reference extension | Done |
129
- | M1 — MVP | Core editing, history, basic marks and blocks, lists, paste cleanup, default toolbar, demo, benchmarks | Done |
130
- | M2 — v1.0 | Preview releases `0.x` on npm, then tables, images, fonts and colors, productivity tools up to `1.0.0` | In progress |
131
- | M3 — Later | Real-time collaboration (Yjs), framework wrappers, embeds | Planned |
89
+ Markdown shortcuts as you type, a keyboard shortcut for everything, a toolbar you can drive
90
+ from the keyboard alone. Every feature, shortcut and option: [All features](docs/FEATURES.md).
132
91
 
133
- Full feature list and priorities in [docs/SPEC.md](docs/SPEC.md).
92
+ ## Why Stilo
134
93
 
135
- ## How it is built
94
+ - **Clean, safe HTML.** HTML in, semantic HTML out, through a strict allowlist of tags,
95
+ attributes and styles.
96
+ - **Paste from Word and Google Docs.** Real lists, kept tables, no stray fonts and colors.
97
+ - **Big documents welcome.** Typing stays instant at 10,000 paragraphs
98
+ ([numbers](docs/PERFORMANCE.md)).
99
+ - **Tested in Chromium, Firefox and WebKit**, with TypeScript types included.
100
+ - **Nothing behind a paywall.** Tables, paste from Word, find and replace: all in the free,
101
+ MIT-licensed package.
136
102
 
137
- Stilo is designed and owned by Alessio Coggiola and developed with AI assistance
138
- ([Claude](https://claude.com/claude-code)). Every decision is written down before the code
139
- ([docs/](docs/), [ADRs](docs/decisions/)), every change goes through the quality gate
140
- (typecheck, lint, about 950 unit tests including XSS vectors) and is tried in the demo before
141
- it is merged, and performance is measured against explicit budgets (`npm run bench`).
103
+ ## Status
142
104
 
143
- | Document | Content |
144
- |---|---|
145
- | [API.md](docs/API.md) | API reference for applications |
146
- | [VISION.md](docs/VISION.md) | Why and what |
147
- | [SPEC.md](docs/SPEC.md) | Features, non-functional requirements, roadmap |
148
- | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Stack, file layout, extension API, HTML contract |
149
- | [UI.md](docs/UI.md) | Design of the default UI |
150
- | [PERFORMANCE.md](docs/PERFORMANCE.md) | Performance in practice, how it is measured, limits |
151
- | [BACKLOG.md](docs/BACKLOG.md) | Tasks done and planned |
152
- | [AGENTS.md](AGENTS.md) | Rules for contributors and coding agents |
153
- | [CONTRIBUTING.md](CONTRIBUTING.md) | How to report bugs and propose changes |
154
- | [SECURITY.md](SECURITY.md) | How to report a vulnerability |
105
+ `0.x` preview: it works, but APIs may change in minor versions until `1.0.0`.
106
+ Read the [changelog](CHANGELOG.md) before updating.
107
+ Next: framework wrappers, real-time collaboration ([roadmap](docs/SPEC.md)).
155
108
 
156
- ## Development
109
+ ## Contributing
157
110
 
158
- Requires Node.js 22 or later.
111
+ Bugs, ideas and questions: [issues](https://github.com/kojak8791/stilo-editor/issues/new/choose).
112
+ Code: [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilities: [SECURITY.md](SECURITY.md).
159
113
 
160
- ```sh
161
- npm install
162
- npm run check # typecheck + lint + unit tests
163
- npm run dev # demo page
164
- npm run bench # performance benchmarks (needs: npx playwright install chromium)
165
- npm run test:e2e # E2E tests in Chromium, Firefox and WebKit (needs: npx playwright install)
166
- ```
114
+ Designed by Alessio Coggiola and built with [Claude](https://claude.com/claude-code), from a
115
+ written [architecture](docs/ARCHITECTURE.md) and [decision records](docs/decisions/).
167
116
 
168
117
  ## License
169
118
 
170
- [MIT](LICENSE) © 2026 Alessio Coggiola
171
-
172
- The logo wordmark is drawn from [IBM Plex](https://github.com/IBM/plex), licensed under the
173
- [SIL Open Font License 1.1](https://openfontlicense.org).
119
+ [MIT](LICENSE) © 2026 Alessio Coggiola. Logo wordmark from
120
+ [IBM Plex](https://github.com/IBM/plex) ([OFL 1.1](https://openfontlicense.org)).
package/dist/content.css CHANGED
@@ -96,6 +96,60 @@ li.ProseMirror-selectednode:after {
96
96
  display: block;
97
97
  }
98
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
+
99
153
  /* Stilo content defaults */
100
154
 
101
155
  /* Lists: list items hold paragraphs, which must not add their own margins. */
@@ -107,10 +161,81 @@ li.ProseMirror-selectednode:after {
107
161
 
108
162
  .stilo-content li > p,
109
163
  .stilo-content li > ul,
110
- .stilo-content li > ol {
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 {
111
168
  margin: 0;
112
169
  }
113
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
+
114
239
  .stilo-content blockquote {
115
240
  margin: 0.5em 0;
116
241
  padding-left: 1em;
@@ -132,6 +257,109 @@ li.ProseMirror-selectednode:after {
132
257
  font-size: 0.9em;
133
258
  }
134
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
+
135
363
  /* Placeholder: shown only while the content is editable. */
136
364
  .stilo-content[contenteditable="true"] .stilo-placeholder::before {
137
365
  content: attr(data-placeholder);
@@ -159,3 +387,12 @@ li.ProseMirror-selectednode:after {
159
387
  .stilo-content blockquote > :last-child {
160
388
  margin-bottom: 0;
161
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
+ }
@@ -3,6 +3,12 @@ import { EditorState } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
  import { type EventHandler } from './events';
5
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";
6
12
  /**
7
13
  * The Stilo editor. See docs/ARCHITECTURE.md §5.
8
14
  *
@@ -33,13 +39,13 @@ export declare class Editor {
33
39
  * Creates the editor:
34
40
  *
35
41
  * 1. `ExtensionManager` from `options.extensions` → `schema`;
36
- * 2. `createPlugins(this)`, then the core `createDropMovePlugin()`; command runners are built
42
+ * 2. `createPlugins(this, emit)`, then the core `createDropMovePlugin()`; command runners are built
37
43
  * from the manager's commands;
38
44
  * 3. initial document from `parseHTML(schema, options.content ?? '')`;
39
45
  * 4. `EditorView` mounted inside `options.element`, with attributes `class="stilo-content"`,
40
46
  * `role="textbox"`, `aria-multiline="true"`, `aria-label` (if given); `editable` from
41
- * options (default `true`); `transformPastedHTML` → `sanitizeToString`;
42
- * `clipboardSerializer` → `createClipboardSerializer`; transactions go
47
+ * options (default `true`); `transformPastedHTML` → `cleanPastedHTML`, then
48
+ * `sanitizeToString`; `clipboardSerializer` → `createClipboardSerializer`; transactions go
43
49
  * through the editor's dispatch (see events in `EditorEvents`);
44
50
  * 5. `autofocus`: `true`/`'start'` → focus at start, `'end'` → focus at end.
45
51
  */
@@ -104,5 +110,11 @@ export declare class Editor {
104
110
  /** Builds `commands` from the extension commands; `can` runs without `dispatch`. */
105
111
  private createRunners;
106
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;
107
119
  private dispatch;
108
120
  }
@@ -1,7 +1,7 @@
1
1
  import { Schema } from 'prosemirror-model';
2
2
  import type { Plugin } from 'prosemirror-state';
3
3
  import type { Editor } from './editor';
4
- import type { Extension } from './extension';
4
+ import type { Extension, ExtensionContext } from './extension';
5
5
  import type { CommandFactory } from './types';
6
6
  /**
7
7
  * Schema order of the built-in marks, outermost first. It defines how marks nest in the HTML
@@ -43,7 +43,7 @@ export declare class ExtensionManager {
43
43
  get commands(): ReadonlyMap<string, CommandFactory>;
44
44
  /**
45
45
  * Called once by the editor. For each extension in order, with context
46
- * `{ options, schema, editor }`: collects `commands` (duplicate name → `StiloError`),
46
+ * `{ options, schema, editor, emit }`: collects `commands` (duplicate name → `StiloError`),
47
47
  * `inputRules`, `keymap`, `plugins`. Returns, in this order:
48
48
  *
49
49
  * 1. one `inputRules({ rules })` plugin with all input rules;
@@ -53,5 +53,5 @@ export declare class ExtensionManager {
53
53
  * 4. all extension plugins, in extension order;
54
54
  * 5. `keymap(baseKeymap)` from prosemirror-commands.
55
55
  */
56
- createPlugins(editor: Editor): Plugin[];
56
+ createPlugins(editor: Editor, emit: ExtensionContext<object>['emit']): Plugin[];
57
57
  }
@@ -2,12 +2,17 @@ import type { InputRule } from 'prosemirror-inputrules';
2
2
  import type { MarkSpec, NodeSpec, Schema } from 'prosemirror-model';
3
3
  import type { Command, Plugin } from 'prosemirror-state';
4
4
  import type { Editor } from './editor';
5
- import type { CommandFactories } from './types';
5
+ import type { CommandFactories, EditorEvents } from './types';
6
6
  /** Context passed to the runtime parts of an extension. */
7
7
  export interface ExtensionContext<Options> {
8
8
  options: Readonly<Options>;
9
9
  schema: Schema;
10
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;
11
16
  }
12
17
  /** Definition of an extension. See docs/ARCHITECTURE.md §6. */
13
18
  export interface ExtensionSpec<Options extends object = Record<string, never>> {
@@ -0,0 +1,25 @@
1
+ import type { Attrs, DOMOutputSpec, Node, NodeType } from 'prosemirror-model';
2
+ /** A text alignment (docs/SPEC.md F-308). `left` is the default and is stored as `null`. */
3
+ export type TextAlignment = 'left' | 'center' | 'right' | 'justify';
4
+ /** The `textAlign` and `lineHeight` attributes of a textblock spec, both `null` by default. */
5
+ export declare function blockStyleAttrs(): {
6
+ textAlign: {
7
+ default: null;
8
+ };
9
+ lineHeight: {
10
+ default: null;
11
+ };
12
+ };
13
+ /**
14
+ * The style attributes of `node` that `type` also has, to keep them when a block becomes `type`
15
+ * (a centered paragraph stays centered as a heading).
16
+ */
17
+ export declare function keptBlockStyles(type: NodeType, node: Node): Attrs;
18
+ /**
19
+ * The style attributes of a parsed block: its `text-align` (`null` for left) and `line-height`.
20
+ * `null` when it has neither, so the block keeps the shared default attributes: one object per
21
+ * paragraph is a lot of memory in a large document.
22
+ */
23
+ export declare function parseBlockStyle(element: HTMLElement): Attrs | null;
24
+ /** The `toDOM` output of a textblock `tag` with its style attributes, if any. */
25
+ export declare function blockStyleDOM(tag: string, node: Node): DOMOutputSpec;
@@ -1,5 +1,5 @@
1
1
  import { InputRule } from 'prosemirror-inputrules';
2
- import type { Attrs, MarkType } from 'prosemirror-model';
2
+ import type { Attrs, MarkType, NodeType } from 'prosemirror-model';
3
3
  /**
4
4
  * Creates an input rule that turns markdown-like syntax into a mark.
5
5
  *
@@ -19,3 +19,10 @@ import type { Attrs, MarkType } from 'prosemirror-model';
19
19
  * ```
20
20
  */
21
21
  export declare function markInputRule(regexp: RegExp, type: MarkType, attrs?: Attrs | null): InputRule;
22
+ /**
23
+ * Like `textblockTypeInputRule` from prosemirror-inputrules (turns the textblock where `regexp`
24
+ * matches at its start into `type` with `attrs`, or with the attributes `attrs` returns for the
25
+ * match), but the block keeps the style attributes (`textAlign`, `lineHeight`) that `type` also
26
+ * has, as `setTextblockType` does.
27
+ */
28
+ export declare function textblockTypeInputRule(regexp: RegExp, type: NodeType, attrs?: Attrs | null | ((match: RegExpMatchArray) => Attrs | null)): InputRule;
@@ -1,12 +1,20 @@
1
1
  import type { Attrs, NodeType } from 'prosemirror-model';
2
- import type { Command } from 'prosemirror-state';
2
+ import { type Command } from 'prosemirror-state';
3
3
  /**
4
4
  * Command: toggles a list of `listType` (whose items are `itemType`) at the selection. List
5
5
  * nodes are recognized by the `list` group in their spec. Looking at the nearest list around
6
6
  * the start of the selection:
7
7
  *
8
8
  * - a list of `listType`: the selected items are lifted out one level (`liftListItem`);
9
- * - a list of another type: that list becomes a `listType` list with `attrs`, items unchanged;
9
+ * - a list of another type: that list becomes a `listType` list with `attrs`, its items
10
+ * `itemType` items (a task item loses its `checked` state);
10
11
  * - no list: the selected blocks are wrapped in a new list (`wrapInList`).
11
12
  */
12
13
  export declare function toggleList(listType: NodeType, itemType: NodeType, attrs?: Attrs | null): Command;
14
+ /**
15
+ * Command for `Backspace` in lists whose items are `itemType`. At the start of the first
16
+ * textblock of an item, lifts the item (`liftListItem`). At the start of a textblock right after
17
+ * such a list, moves its content to the end of the list's last textblock and deletes it (an
18
+ * empty one just disappears); without this, ProseMirror wraps the textblock in a new item.
19
+ */
20
+ export declare function backspaceInList(itemType: NodeType): Command;