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.
- package/README.md +76 -129
- package/dist/content.css +238 -1
- package/dist/core/editor.d.ts +15 -3
- package/dist/core/extension-manager.d.ts +3 -3
- package/dist/core/extension.d.ts +6 -1
- package/dist/core/helpers/block-style.d.ts +25 -0
- package/dist/core/helpers/input-rules.d.ts +8 -1
- package/dist/core/helpers/lists.d.ts +10 -2
- package/dist/core/helpers/marks.d.ts +14 -0
- package/dist/core/helpers/nodes.d.ts +12 -0
- package/dist/core/helpers/word-count.d.ts +14 -0
- package/dist/core/html/css.d.ts +16 -1
- package/dist/core/html/paste/base-styles.d.ts +7 -0
- package/dist/core/html/paste/google-docs.d.ts +7 -0
- package/dist/core/html/paste/index.d.ts +9 -0
- package/dist/core/html/paste/table-widths.d.ts +7 -0
- package/dist/core/html/paste/word.d.ts +8 -0
- package/dist/core/html/serialize.d.ts +2 -2
- package/dist/extensions/code-block/index.d.ts +24 -0
- package/dist/extensions/default-extensions/index.d.ts +11 -0
- package/dist/extensions/heading/index.d.ts +2 -1
- package/dist/extensions/image/image-view.d.ts +27 -0
- package/dist/extensions/image/index.d.ts +29 -0
- package/dist/extensions/index.d.ts +10 -1
- package/dist/extensions/line-height/index.d.ts +18 -0
- package/dist/extensions/list-item/index.d.ts +1 -1
- package/dist/extensions/paragraph/index.d.ts +4 -4
- package/dist/extensions/search/index.d.ts +55 -0
- package/dist/extensions/table/index.d.ts +70 -0
- package/dist/extensions/task-item/index.d.ts +24 -0
- package/dist/extensions/task-item/task-item-view.d.ts +24 -0
- package/dist/extensions/task-list/index.d.ts +11 -0
- package/dist/extensions/text-align/index.d.ts +18 -0
- package/dist/extensions/text-case/index.d.ts +14 -0
- package/dist/extensions/text-style/font-size.d.ts +21 -0
- package/dist/extensions/text-style/index.d.ts +18 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/src-CV6pgfTS.js +2662 -0
- package/dist/style.css +599 -1
- package/dist/ui/components/dropdown.d.ts +9 -0
- package/dist/ui/components/find-bar.d.ts +24 -0
- package/dist/ui/components/font-size-input.d.ts +21 -0
- package/dist/ui/components/icon-menu.d.ts +20 -0
- package/dist/ui/components/status-bar.d.ts +19 -0
- package/dist/ui/components/table-actions.d.ts +25 -0
- package/dist/ui/components/table-handles.d.ts +16 -0
- package/dist/ui/components/table-menu.d.ts +21 -0
- package/dist/ui/i18n/types.d.ts +1 -1
- package/dist/ui/index.d.ts +6 -2
- package/dist/ui/toolbar/fonts.d.ts +8 -0
- package/dist/ui/toolbar/items.d.ts +23 -2
- package/dist/ui/toolbar/types.d.ts +67 -4
- package/dist/ui.css +361 -0
- package/dist/ui.js +1361 -303
- package/package.json +19 -9
- package/dist/src-3tAxKI0c.js +0 -1316
package/README.md
CHANGED
|
@@ -1,95 +1,44 @@
|
|
|
1
|
-
<
|
|
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
|
-
</
|
|
6
|
+
</p>
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
>
|
|
12
|
-
|
|
13
|
-
>
|
|
10
|
+
<p align="center">
|
|
11
|
+
Free and open source (MIT) · clean HTML in and out · built on ProseMirror
|
|
12
|
+
</p>
|
|
14
13
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
your own
|
|
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
|
-
|
|
73
|
+
document.querySelector('#bold')!.addEventListener('click', () => editor.commands.toggleBold());
|
|
74
|
+
document.querySelector('#undo')!.addEventListener('click', () => editor.commands.undo());
|
|
75
|
+
```
|
|
114
76
|
|
|
115
|
-
|
|
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
|
-
|
|
79
|
+
## What's included
|
|
123
80
|
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
## Why Stilo
|
|
134
93
|
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
##
|
|
109
|
+
## Contributing
|
|
157
110
|
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
+
}
|
package/dist/core/editor.d.ts
CHANGED
|
@@ -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` → `
|
|
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
|
}
|
package/dist/core/extension.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|