@fusion.dev/helix-editor 0.220.0 → 0.221.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.
@@ -0,0 +1,143 @@
1
+ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ const NOTHING_FOUND = { current: -1, matches: [], query: undefined };
4
+ export const editorFindKey = new PluginKey('helix-editor-find');
5
+ const patternOf = (query, global) => {
6
+ if (query.search === '') {
7
+ return undefined;
8
+ }
9
+ const source = query.regex ? query.search : query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ const flags = `${global ? 'g' : ''}${query.caseSensitive ? '' : 'i'}u`;
11
+ try {
12
+ return new RegExp(query.wholeWord ? `\\b(?:${source})\\b` : source, flags);
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ };
18
+ const BETWEEN = '\u0000';
19
+ export const matchesIn = (doc, query) => {
20
+ const pattern = patternOf(query, true);
21
+ if (!pattern) {
22
+ return [];
23
+ }
24
+ const matches = [];
25
+ doc.descendants((node, pos) => {
26
+ if (!node.isTextblock) {
27
+ return true;
28
+ }
29
+ let text = '';
30
+ const at = [];
31
+ node.forEach((child, offset) => {
32
+ const start = pos + 1 + offset;
33
+ if (child.isText) {
34
+ const words = child.text ?? '';
35
+ for (let index = 0; index < words.length; index += 1) {
36
+ at.push(start + index);
37
+ }
38
+ text += words;
39
+ }
40
+ else {
41
+ at.push(start);
42
+ text += BETWEEN;
43
+ }
44
+ });
45
+ for (const match of text.matchAll(pattern)) {
46
+ const found = match[0];
47
+ if (found !== '' && !found.includes(BETWEEN)) {
48
+ const first = at[match.index] ?? 0;
49
+ const last = at[match.index + found.length - 1] ?? first;
50
+ matches.push({ from: first, to: last + 1 });
51
+ }
52
+ }
53
+ return false;
54
+ });
55
+ return matches;
56
+ };
57
+ export const editorFind = () => new Plugin({
58
+ key: editorFindKey,
59
+ props: {
60
+ decorations: (state) => {
61
+ const found = editorFindKey.getState(state);
62
+ if (!found || found.matches.length === 0) {
63
+ return DecorationSet.empty;
64
+ }
65
+ return DecorationSet.create(state.doc, found.matches.map((match, index) => Decoration.inline(match.from, match.to, {
66
+ class: index === found.current ? 'helix-find-current' : 'helix-find-match',
67
+ })));
68
+ },
69
+ },
70
+ state: {
71
+ apply: (tr, found, _before, state) => {
72
+ const told = tr.getMeta(editorFindKey);
73
+ if (told) {
74
+ return told;
75
+ }
76
+ if (!found.query || !tr.docChanged) {
77
+ return found;
78
+ }
79
+ const matches = matchesIn(state.doc, found.query);
80
+ const was = found.matches[found.current];
81
+ const anchor = was ? tr.mapping.map(was.from) : 0;
82
+ const next = matches.findIndex((match) => match.from >= anchor);
83
+ return {
84
+ current: next === -1 ? matches.length - 1 : next,
85
+ matches,
86
+ query: found.query,
87
+ };
88
+ },
89
+ init: () => NOTHING_FOUND,
90
+ },
91
+ });
92
+ export const findResultOf = (state) => {
93
+ const found = editorFindKey.getState(state);
94
+ return found?.query ? { count: found.matches.length, index: found.current } : undefined;
95
+ };
96
+ export const findIn = (view, query, direction) => {
97
+ const { state } = view;
98
+ const matches = matchesIn(state.doc, query);
99
+ const { from, to } = state.selection;
100
+ let current = -1;
101
+ if (matches.length > 0) {
102
+ current =
103
+ direction === 'previous'
104
+ ? matches.findLastIndex((match) => match.from < from)
105
+ : matches.findIndex((match) => match.from >= (direction === 'next' ? to : from));
106
+ if (current === -1) {
107
+ current = direction === 'previous' ? matches.length - 1 : 0;
108
+ }
109
+ }
110
+ const tr = state.tr
111
+ .setMeta(editorFindKey, { current, matches, query })
112
+ .setMeta('addToHistory', false);
113
+ const selected = matches[current];
114
+ if (selected) {
115
+ tr.setSelection(TextSelection.create(tr.doc, selected.from, selected.to)).scrollIntoView();
116
+ }
117
+ view.dispatch(tr);
118
+ return { count: matches.length, index: current };
119
+ };
120
+ export const endFind = (view) => {
121
+ if (editorFindKey.getState(view.state)?.query) {
122
+ view.dispatch(view.state.tr.setMeta(editorFindKey, NOTHING_FOUND).setMeta('addToHistory', false));
123
+ }
124
+ };
125
+ export const replaceIn = (view, query, all) => {
126
+ const one = patternOf(query, false);
127
+ if (!one || !view.editable) {
128
+ return findIn(view, query, 'first');
129
+ }
130
+ const { state } = view;
131
+ const matches = matchesIn(state.doc, query);
132
+ const { from, to } = state.selection;
133
+ const chosen = all ? matches : matches.filter((match) => match.from === from && match.to === to);
134
+ if (chosen.length === 0) {
135
+ return findIn(view, query, 'next');
136
+ }
137
+ const tr = state.tr;
138
+ for (const match of [...chosen].reverse()) {
139
+ tr.insertText(state.doc.textBetween(match.from, match.to).replace(one, query.replace), match.from, match.to);
140
+ }
141
+ view.dispatch(tr);
142
+ return findIn(view, query, all ? 'first' : 'next');
143
+ };
package/dist/index.d.ts CHANGED
@@ -1 +1,13 @@
1
- export { type CodeMirrorOptions, codeMirrorEngine, connectEditor, languageName, languageNames, } from './codemirror/index.js';
1
+ export type { BlockIdsOptions } from './block-ids/block-ids.js';
2
+ export { blockIds, blockIdsKey, findBlock, newBlockId } from './block-ids/block-ids.js';
3
+ export type { ConnectEditorOptions, EditorConnection } from './connect-editor/connect-editor.js';
4
+ export { connectEditor, editorFrame, editorFrameKey } from './connect-editor/connect-editor.js';
5
+ export type { EditorFindDirection, EditorFindQuery, EditorFindResult } from './find/find.js';
6
+ export { editorFind, editorFindKey, endFind, findIn, findResultOf, matchesIn, replaceIn, } from './find/find.js';
7
+ export type { EditorPluginsOptions } from './plugins/plugins.js';
8
+ export { editorPlugins } from './plugins/plugins.js';
9
+ export type { EditorSchemaOptions } from './schema/schema.js';
10
+ export { BLOCK_ID_ATTRIBUTE, editorMarks, editorNodes, editorSchema, safeHref, withBlockId, } from './schema/schema.js';
11
+ export { adoptEditorSheet, EDITOR_CSS } from './sheet/editor-sheet.js';
12
+ export type { EditorFormats, EditorTool } from './tools/tools.js';
13
+ export { EDITOR_TOOLS, formatsOf, markRange, setLink, toolCommands } from './tools/tools.js';
package/dist/index.js CHANGED
@@ -1 +1,7 @@
1
- export { codeMirrorEngine, connectEditor, languageName, languageNames, } from './codemirror/index.js';
1
+ export { blockIds, blockIdsKey, findBlock, newBlockId } from './block-ids/block-ids.js';
2
+ export { connectEditor, editorFrame, editorFrameKey } from './connect-editor/connect-editor.js';
3
+ export { editorFind, editorFindKey, endFind, findIn, findResultOf, matchesIn, replaceIn, } from './find/find.js';
4
+ export { editorPlugins } from './plugins/plugins.js';
5
+ export { BLOCK_ID_ATTRIBUTE, editorMarks, editorNodes, editorSchema, safeHref, withBlockId, } from './schema/schema.js';
6
+ export { adoptEditorSheet, EDITOR_CSS } from './sheet/editor-sheet.js';
7
+ export { EDITOR_TOOLS, formatsOf, markRange, setLink, toolCommands } from './tools/tools.js';
@@ -0,0 +1,33 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import type { Command, Plugin } from 'prosemirror-state';
3
+ import { type EditorTool } from '../tools/tools.js';
4
+ /** What `editorPlugins` takes. */
5
+ export interface EditorPluginsOptions {
6
+ /**
7
+ * The commands to run in place of Helix's, by tool, such as undo and redo
8
+ * where y-prosemirror keeps the history: its `undoCommand` and
9
+ * `redoCommand`. The keys run them too.
10
+ */
11
+ commands?: Partial<Record<EditorTool, Command>> | undefined;
12
+ /**
13
+ * Whether `prosemirror-history` keeps the undo history. `false` where
14
+ * another plugin keeps it, such as y-prosemirror's `yUndoPlugin`.
15
+ */
16
+ history?: boolean | undefined;
17
+ /**
18
+ * Whether Markdown typed at the start of a line makes a block, such as
19
+ * `# ` a heading and `- ` a list. On by default.
20
+ */
21
+ markdown?: boolean | undefined;
22
+ }
23
+ /**
24
+ * The plugins a document Helix's toolbar edits takes: the keys for every
25
+ * tool, Enter, Tab, and Shift+Tab in a list, Shift+Enter for a line break,
26
+ * Markdown typed at the start of a line, undo history, the cursors drawn while
27
+ * something is dragged and where no text can stand, `blockIds`, the find
28
+ * bar's search, and the plugin `connectEditor` speaks to the frame through.
29
+ *
30
+ * **Tab moves in a list only.** Anywhere else it is the browser's, and leaves
31
+ * the document, so the document is never a place the keyboard cannot leave.
32
+ */
33
+ export declare const editorPlugins: (schema: Schema, options?: EditorPluginsOptions) => Plugin[];
@@ -0,0 +1,102 @@
1
+ import { baseKeymap, chainCommands, exitCode } from 'prosemirror-commands';
2
+ import { dropCursor } from 'prosemirror-dropcursor';
3
+ import { gapCursor } from 'prosemirror-gapcursor';
4
+ import { history } from 'prosemirror-history';
5
+ import { inputRules, textblockTypeInputRule, undoInputRule, wrappingInputRule, } from 'prosemirror-inputrules';
6
+ import { keymap } from 'prosemirror-keymap';
7
+ import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
8
+ import { blockIds } from '../block-ids/block-ids.js';
9
+ import { editorFrame } from '../connect-editor/connect-editor.js';
10
+ import { editorFind } from '../find/find.js';
11
+ import { toolCommands } from '../tools/tools.js';
12
+ const KEYS = [
13
+ ['Mod-z', 'undo'],
14
+ ['Shift-Mod-z', 'redo'],
15
+ ['Mod-y', 'redo'],
16
+ ['Mod-b', 'bold'],
17
+ ['Mod-i', 'italic'],
18
+ ['Shift-Mod-x', 'strike'],
19
+ ['Mod-e', 'code'],
20
+ ['Mod-Alt-0', 'paragraph'],
21
+ ['Mod-Alt-1', 'heading-1'],
22
+ ['Mod-Alt-2', 'heading-2'],
23
+ ['Mod-Alt-3', 'heading-3'],
24
+ ['Mod-Alt-8', 'code-block'],
25
+ ['Shift-Mod-8', 'bullet-list'],
26
+ ['Shift-Mod-7', 'ordered-list'],
27
+ ['Shift-Mod-9', 'quote'],
28
+ ];
29
+ const inList = (state, item) => {
30
+ const { $from } = state.selection;
31
+ for (let depth = $from.depth; depth > 0; depth -= 1) {
32
+ if ($from.node(depth).type === item) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ };
38
+ const markdownRules = (schema) => {
39
+ const { nodes } = schema;
40
+ const rules = [];
41
+ if (nodes.blockquote) {
42
+ rules.push(wrappingInputRule(/^\s*>\s$/, nodes.blockquote));
43
+ }
44
+ if (nodes.ordered_list) {
45
+ rules.push(wrappingInputRule(/^(\d+)\.\s$/, nodes.ordered_list, (match) => ({ order: Number(match[1]) }), (match, node) => node.childCount + node.attrs.order === Number(match[1])));
46
+ }
47
+ if (nodes.bullet_list) {
48
+ rules.push(wrappingInputRule(/^\s*([-+*])\s$/, nodes.bullet_list));
49
+ }
50
+ if (nodes.code_block) {
51
+ rules.push(textblockTypeInputRule(/^```$/, nodes.code_block));
52
+ }
53
+ if (nodes.heading) {
54
+ rules.push(textblockTypeInputRule(/^(#{1,6})\s$/, nodes.heading, (match) => ({
55
+ level: match[1]?.length ?? 1,
56
+ })));
57
+ }
58
+ return rules;
59
+ };
60
+ export const editorPlugins = (schema, options = {}) => {
61
+ const commands = toolCommands(schema, options.commands);
62
+ const keys = {};
63
+ for (const [key, tool] of KEYS) {
64
+ const command = commands[tool];
65
+ if (command) {
66
+ keys[key] = command;
67
+ }
68
+ }
69
+ const item = schema.nodes.list_item;
70
+ if (item) {
71
+ keys.Enter = splitListItem(item);
72
+ keys.Tab = (state, dispatch) => {
73
+ if (!inList(state, item)) {
74
+ return false;
75
+ }
76
+ sinkListItem(item)(state, dispatch);
77
+ return true;
78
+ };
79
+ keys['Shift-Tab'] = liftListItem(item);
80
+ }
81
+ const lineBreak = schema.nodes.hard_break;
82
+ if (lineBreak) {
83
+ keys['Shift-Enter'] = chainCommands(exitCode, (state, dispatch) => {
84
+ if (dispatch) {
85
+ dispatch(state.tr.replaceSelectionWith(lineBreak.create()).scrollIntoView());
86
+ }
87
+ return true;
88
+ });
89
+ }
90
+ keys.Backspace = undoInputRule;
91
+ return [
92
+ ...(options.markdown === false ? [] : [inputRules({ rules: markdownRules(schema) })]),
93
+ keymap(keys),
94
+ keymap(baseKeymap),
95
+ dropCursor({ class: 'helix-editor-drop-cursor', color: false }),
96
+ gapCursor(),
97
+ ...(options.history === false ? [] : [history()]),
98
+ blockIds(),
99
+ editorFind(),
100
+ editorFrame(),
101
+ ];
102
+ };
@@ -0,0 +1,53 @@
1
+ import { type MarkSpec, type NodeSpec, Schema } from 'prosemirror-model';
2
+ /** The attribute a block's id is written to in HTML. */
3
+ export declare const BLOCK_ID_ATTRIBUTE = "data-block-id";
4
+ /**
5
+ * Whether an address may be a link's, and the address to write: a web page,
6
+ * mail, a phone number, or a path on the page's own site. An address with no
7
+ * scheme, such as `fusion.dev/helix`, is taken as a web page. Any other
8
+ * scheme, `javascript:` among them, is refused with an empty string, so a
9
+ * link pasted from elsewhere cannot run anything when the document is served.
10
+ */
11
+ export declare const safeHref: (href: string) => string;
12
+ /**
13
+ * Helix's blocks, under ProseMirror's own names, so a document reads and
14
+ * writes Markdown through `prosemirror-markdown` as it is.
15
+ *
16
+ * **Each block carries an id**, written to `data-block-id`, which comments,
17
+ * provenance, and an agent's edits address a block by. `blockIds` keeps them
18
+ * unique.
19
+ *
20
+ * **A paragraph or a heading runs in the direction its own words take**,
21
+ * `dir="auto"`, so an Arabic paragraph in an English document runs right to
22
+ * left. A code block runs left to right, as code does in every script.
23
+ */
24
+ export declare const editorNodes: Record<string, NodeSpec>;
25
+ /**
26
+ * Helix's marks, under ProseMirror's names: a link, emphasis, strong text,
27
+ * struck-through text, and inline code.
28
+ */
29
+ export declare const editorMarks: Record<string, MarkSpec>;
30
+ /**
31
+ * A block spec of your own that keeps an id as Helix's blocks do: the `id`
32
+ * attribute, read from `data-block-id` and written back to it. A spec that
33
+ * already declares `id` is returned as it is.
34
+ */
35
+ export declare const withBlockId: (spec: NodeSpec) => NodeSpec;
36
+ /** What `editorSchema` takes beyond Helix's own blocks and marks. */
37
+ export interface EditorSchemaOptions {
38
+ /**
39
+ * Marks of your own, added after Helix's, or one of Helix's replaced by
40
+ * giving its name.
41
+ */
42
+ marks?: Record<string, MarkSpec> | undefined;
43
+ /**
44
+ * Nodes of your own, added after Helix's, or one of Helix's replaced by
45
+ * giving its name. A block among them takes an id as Helix's do.
46
+ */
47
+ nodes?: Record<string, NodeSpec> | undefined;
48
+ }
49
+ /**
50
+ * The schema of a document Helix's toolbar edits: its blocks, each with an
51
+ * id, and its marks, with any of your own added.
52
+ */
53
+ export declare const editorSchema: (options?: EditorSchemaOptions) => Schema;
@@ -0,0 +1,292 @@
1
+ import { Schema, } from 'prosemirror-model';
2
+ export const BLOCK_ID_ATTRIBUTE = 'data-block-id';
3
+ export const safeHref = (href) => {
4
+ const address = href.trim();
5
+ if (address === '') {
6
+ return '';
7
+ }
8
+ if (/^(?:https?:|mailto:|tel:)/i.test(address) || /^(?:[/#?]|\.\.?\/)/.test(address)) {
9
+ return address;
10
+ }
11
+ if (/^[a-z][a-z0-9+.-]*:/i.test(address)) {
12
+ return '';
13
+ }
14
+ return `https://${address}`;
15
+ };
16
+ const blockAttributes = (node, more = {}) => typeof node.attrs.id === 'string' && node.attrs.id !== ''
17
+ ? { [BLOCK_ID_ATTRIBUTE]: node.attrs.id, ...more }
18
+ : more;
19
+ const idOf = (dom) => ({
20
+ id: dom.getAttribute(BLOCK_ID_ATTRIBUTE),
21
+ });
22
+ const ID = { id: { default: null, validate: 'string|null' } };
23
+ const NODES = [
24
+ ['doc', { content: 'block+' }],
25
+ [
26
+ 'paragraph',
27
+ {
28
+ attrs: ID,
29
+ content: 'inline*',
30
+ group: 'block',
31
+ parseDOM: [{ getAttrs: idOf, tag: 'p' }],
32
+ toDOM: (node) => ['p', blockAttributes(node, { dir: 'auto' }), 0],
33
+ },
34
+ ],
35
+ [
36
+ 'heading',
37
+ {
38
+ attrs: { ...ID, level: { default: 1, validate: 'number' } },
39
+ content: 'inline*',
40
+ defining: true,
41
+ group: 'block',
42
+ parseDOM: [1, 2, 3, 4, 5, 6].map((level) => ({
43
+ getAttrs: (dom) => ({ ...idOf(dom), level }),
44
+ tag: `h${level}`,
45
+ })),
46
+ toDOM: (node) => [
47
+ `h${node.attrs.level}`,
48
+ blockAttributes(node, { dir: 'auto' }),
49
+ 0,
50
+ ],
51
+ },
52
+ ],
53
+ [
54
+ 'blockquote',
55
+ {
56
+ attrs: ID,
57
+ content: 'block+',
58
+ defining: true,
59
+ group: 'block',
60
+ parseDOM: [{ getAttrs: idOf, tag: 'blockquote' }],
61
+ toDOM: (node) => ['blockquote', blockAttributes(node), 0],
62
+ },
63
+ ],
64
+ [
65
+ 'code_block',
66
+ {
67
+ attrs: { ...ID, language: { default: '', validate: 'string' } },
68
+ code: true,
69
+ content: 'text*',
70
+ defining: true,
71
+ group: 'block',
72
+ marks: '',
73
+ parseDOM: [
74
+ {
75
+ getAttrs: (dom) => ({
76
+ ...idOf(dom),
77
+ language: dom.getAttribute('data-language') ??
78
+ /\blanguage-(\S+)/.exec(dom.querySelector('code')?.className ?? '')?.[1] ??
79
+ '',
80
+ }),
81
+ preserveWhitespace: 'full',
82
+ tag: 'pre',
83
+ },
84
+ ],
85
+ toDOM: (node) => [
86
+ 'pre',
87
+ blockAttributes(node, {
88
+ dir: 'ltr',
89
+ ...(node.attrs.language ? { 'data-language': node.attrs.language } : {}),
90
+ }),
91
+ ['code', 0],
92
+ ],
93
+ },
94
+ ],
95
+ [
96
+ 'horizontal_rule',
97
+ {
98
+ attrs: ID,
99
+ group: 'block',
100
+ parseDOM: [{ getAttrs: idOf, tag: 'hr' }],
101
+ toDOM: (node) => ['hr', blockAttributes(node)],
102
+ },
103
+ ],
104
+ [
105
+ 'bullet_list',
106
+ {
107
+ attrs: ID,
108
+ content: 'list_item+',
109
+ group: 'block',
110
+ parseDOM: [{ getAttrs: idOf, tag: 'ul' }],
111
+ toDOM: (node) => ['ul', blockAttributes(node), 0],
112
+ },
113
+ ],
114
+ [
115
+ 'ordered_list',
116
+ {
117
+ attrs: { ...ID, order: { default: 1, validate: 'number' } },
118
+ content: 'list_item+',
119
+ group: 'block',
120
+ parseDOM: [
121
+ {
122
+ getAttrs: (dom) => ({
123
+ ...idOf(dom),
124
+ order: dom.hasAttribute('start') ? Number(dom.getAttribute('start')) || 1 : 1,
125
+ }),
126
+ tag: 'ol',
127
+ },
128
+ ],
129
+ toDOM: (node) => [
130
+ 'ol',
131
+ blockAttributes(node, node.attrs.order === 1 ? {} : { start: String(node.attrs.order) }),
132
+ 0,
133
+ ],
134
+ },
135
+ ],
136
+ [
137
+ 'list_item',
138
+ {
139
+ attrs: ID,
140
+ content: 'paragraph block*',
141
+ defining: true,
142
+ parseDOM: [{ getAttrs: idOf, tag: 'li' }],
143
+ toDOM: (node) => ['li', blockAttributes(node), 0],
144
+ },
145
+ ],
146
+ ['text', { group: 'inline' }],
147
+ [
148
+ 'hard_break',
149
+ {
150
+ group: 'inline',
151
+ inline: true,
152
+ parseDOM: [{ tag: 'br' }],
153
+ selectable: false,
154
+ toDOM: () => ['br'],
155
+ },
156
+ ],
157
+ ];
158
+ export const editorNodes = Object.fromEntries(NODES);
159
+ const MARKS = [
160
+ [
161
+ 'link',
162
+ {
163
+ attrs: {
164
+ href: { validate: 'string' },
165
+ title: { default: null, validate: 'string|null' },
166
+ },
167
+ inclusive: false,
168
+ parseDOM: [
169
+ {
170
+ getAttrs: (dom) => {
171
+ const href = safeHref(dom.getAttribute('href') ?? '');
172
+ return href === '' ? false : { href, title: dom.getAttribute('title') };
173
+ },
174
+ tag: 'a[href]',
175
+ },
176
+ ],
177
+ toDOM: (mark) => [
178
+ 'a',
179
+ mark.attrs.title
180
+ ? { href: mark.attrs.href, title: mark.attrs.title }
181
+ : { href: mark.attrs.href },
182
+ 0,
183
+ ],
184
+ },
185
+ ],
186
+ [
187
+ 'em',
188
+ {
189
+ parseDOM: [
190
+ { tag: 'i' },
191
+ { tag: 'em' },
192
+ { getAttrs: (value) => value === 'italic' && null, style: 'font-style' },
193
+ ],
194
+ toDOM: () => ['em', 0],
195
+ },
196
+ ],
197
+ [
198
+ 'strong',
199
+ {
200
+ parseDOM: [
201
+ { tag: 'strong' },
202
+ { getAttrs: (dom) => dom.style.fontWeight !== 'normal' && null, tag: 'b' },
203
+ {
204
+ getAttrs: (value) => /^(?:bold(?:er)?|[5-9]\d{2,})$/.test(value) && null,
205
+ style: 'font-weight',
206
+ },
207
+ ],
208
+ toDOM: () => ['strong', 0],
209
+ },
210
+ ],
211
+ [
212
+ 'strike',
213
+ {
214
+ parseDOM: [
215
+ { tag: 's' },
216
+ { tag: 'del' },
217
+ { tag: 'strike' },
218
+ {
219
+ getAttrs: (value) => value.includes('line-through') && null,
220
+ style: 'text-decoration',
221
+ },
222
+ ],
223
+ toDOM: () => ['s', 0],
224
+ },
225
+ ],
226
+ [
227
+ 'code',
228
+ {
229
+ code: true,
230
+ parseDOM: [{ tag: 'code' }],
231
+ toDOM: () => ['code', 0],
232
+ },
233
+ ],
234
+ ];
235
+ export const editorMarks = Object.fromEntries(MARKS);
236
+ const isBlock = (name, spec, top) => name !== top && name !== 'text' && spec.inline !== true;
237
+ const withId = (output, id) => {
238
+ if (typeof id !== 'string' || id === '') {
239
+ return output;
240
+ }
241
+ if (Array.isArray(output)) {
242
+ const [tag, second, ...rest] = output;
243
+ if (second && typeof second === 'object' && !Array.isArray(second) && !('nodeType' in second)) {
244
+ return [
245
+ tag,
246
+ { ...second, [BLOCK_ID_ATTRIBUTE]: id },
247
+ ...rest,
248
+ ];
249
+ }
250
+ return (second === undefined
251
+ ? [tag, { [BLOCK_ID_ATTRIBUTE]: id }]
252
+ : [tag, { [BLOCK_ID_ATTRIBUTE]: id }, second, ...rest]);
253
+ }
254
+ const dom = 'dom' in output
255
+ ? output.dom
256
+ : output;
257
+ if (dom instanceof Element) {
258
+ dom.setAttribute(BLOCK_ID_ATTRIBUTE, id);
259
+ }
260
+ return output;
261
+ };
262
+ export const withBlockId = (spec) => {
263
+ if (spec.attrs && 'id' in spec.attrs) {
264
+ return spec;
265
+ }
266
+ const toDOM = spec.toDOM;
267
+ return {
268
+ ...spec,
269
+ attrs: { ...spec.attrs, ...ID },
270
+ ...(spec.parseDOM
271
+ ? {
272
+ parseDOM: spec.parseDOM.map((rule) => 'tag' in rule && rule.tag
273
+ ? {
274
+ ...rule,
275
+ getAttrs: (dom) => {
276
+ const attrs = rule.getAttrs ? rule.getAttrs(dom) : null;
277
+ return attrs === false ? false : { ...(attrs ?? {}), ...idOf(dom) };
278
+ },
279
+ }
280
+ : rule),
281
+ }
282
+ : {}),
283
+ ...(toDOM ? { toDOM: (node) => withId(toDOM(node), node.attrs.id) } : {}),
284
+ };
285
+ };
286
+ export const editorSchema = (options = {}) => {
287
+ const nodes = {};
288
+ for (const [name, spec] of Object.entries({ ...editorNodes, ...options.nodes })) {
289
+ nodes[name] = isBlock(name, spec, 'doc') ? withBlockId(spec) : spec;
290
+ }
291
+ return new Schema({ marks: { ...editorMarks, ...options.marks }, nodes });
292
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What ProseMirror needs from a stylesheet to edit correctly, and what it
3
+ * draws beside the text, in your tokens: the node a reader selected, the
4
+ * cursor between two blocks where no text can stand, the line where a drop
5
+ * lands, the words in an empty document, and the find bar's matches.
6
+ *
7
+ * **In the page and not in the frame's shadow root**, because the document
8
+ * is the page's own element, which the frame's sheet reaches only at its
9
+ * outer edge. Each rule is `:where()`, so any rule of the page's own wins over
10
+ * it, and the page needs no stylesheet from ProseMirror.
11
+ */
12
+ export declare const EDITOR_CSS = "\n:where(.ProseMirror) {\n font-feature-settings: \"liga\" 0;\n font-variant-ligatures: none;\n overflow-wrap: break-word;\n position: relative;\n white-space: pre-wrap;\n white-space: break-spaces;\n}\n\n:where(.ProseMirror pre) {\n white-space: pre-wrap;\n}\n\n:where(.ProseMirror li) {\n position: relative;\n}\n\n:where(.ProseMirror-hideselection *)::selection {\n background: transparent;\n}\n\n:where(.ProseMirror-hideselection) {\n caret-color: transparent;\n}\n\n:where(.ProseMirror [draggable][contenteditable=\"false\"]) {\n user-select: text;\n}\n\n:where(.ProseMirror-selectednode) {\n outline: var(--helix-size-focus-ring, 2px) solid var(--editor-selected-color, var(--helix-color-focus-ring, Highlight));\n outline-offset: var(--helix-size-focus-offset, 2px);\n}\n\n:where(li.ProseMirror-selectednode) {\n outline: none;\n}\n\n:where(li.ProseMirror-selectednode)::after {\n border: var(--helix-size-focus-ring, 2px) solid var(--editor-selected-color, var(--helix-color-focus-ring, Highlight));\n content: \"\";\n inset-block: calc(var(--helix-size-focus-ring, 2px) * -1);\n inset-inline: calc(var(--helix-space-lg, 1.5rem) * -1) calc(var(--helix-size-focus-ring, 2px) * -1);\n pointer-events: none;\n position: absolute;\n}\n\nimg.ProseMirror-separator {\n border: none !important;\n display: inline !important;\n margin: 0 !important;\n}\n\n:where(.ProseMirror-gapcursor) {\n display: none;\n pointer-events: none;\n position: absolute;\n}\n\n:where(.ProseMirror-gapcursor)::after {\n animation: helix-editor-blink 1.1s steps(2, start) infinite;\n border-block-start: var(--helix-border-thin, 1px) solid currentColor;\n content: \"\";\n display: block;\n inline-size: var(--helix-space-lg, 1.5rem);\n inset-block-start: -2px;\n position: absolute;\n}\n\n:where(.ProseMirror-focused .ProseMirror-gapcursor) {\n display: block;\n}\n\n@keyframes helix-editor-blink {\n to {\n visibility: hidden;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :where(.ProseMirror-gapcursor)::after {\n animation: none;\n }\n}\n\n:where(.helix-editor-drop-cursor) {\n background-color: var(--helix-color-primary, Highlight);\n}\n\n:where(.helix-find-match) {\n background-color: light-dark(\n oklch(from var(--helix-color-warning, oklch(0.52 0.11 75)) 0.93 0.07 h),\n color-mix(in oklab, var(--helix-color-warning, oklch(0.82 0.185 75)) 32%, transparent)\n );\n}\n\n:where(.helix-find-current) {\n background-color: oklch(from var(--helix-color-warning, oklch(0.52 0.11 75)) 0.8 0.16 h);\n color: oklch(from var(--helix-color-warning, oklch(0.52 0.11 75)) 0.25 0.04 h);\n}\n\n:where(.helix-editor-placeholder)::before {\n block-size: 0;\n color: var(--editor-placeholder-color, var(--helix-color-foreground-subtle, GrayText));\n content: attr(data-placeholder);\n float: inline-start;\n pointer-events: none;\n}\n";
13
+ /**
14
+ * Adds the editor's rules to the document or shadow root a view is drawn in,
15
+ * once. They stay there after the view goes, matching nothing.
16
+ */
17
+ export declare const adoptEditorSheet: (root: Document | ShadowRoot) => void;