@prosekit/extensions 0.12.1 → 0.13.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.
- package/dist/list/style.css +5 -5
- package/dist/list/style.css.map +1 -1
- package/dist/prosekit-extensions-autocomplete.d.ts +11 -3
- package/dist/prosekit-extensions-autocomplete.d.ts.map +1 -1
- package/dist/prosekit-extensions-autocomplete.js +171 -60
- package/dist/prosekit-extensions-autocomplete.js.map +1 -1
- package/dist/prosekit-extensions-blockquote.js +1 -1
- package/dist/prosekit-extensions-blockquote.js.map +1 -1
- package/dist/prosekit-extensions-code-block.js +1 -1
- package/dist/prosekit-extensions-code-block.js.map +1 -1
- package/dist/prosekit-extensions-commit.js +1 -1
- package/dist/prosekit-extensions-commit.js.map +1 -1
- package/dist/prosekit-extensions-heading.d.ts.map +1 -1
- package/dist/prosekit-extensions-heading.js +6 -6
- package/dist/prosekit-extensions-heading.js.map +1 -1
- package/dist/prosekit-extensions-loro.d.ts +16 -17
- package/dist/prosekit-extensions-loro.d.ts.map +1 -1
- package/dist/prosekit-extensions-loro.js +13 -6
- package/dist/prosekit-extensions-loro.js.map +1 -1
- package/dist/prosekit-extensions-paragraph.js +1 -1
- package/dist/prosekit-extensions-paragraph.js.map +1 -1
- package/dist/prosekit-extensions-placeholder.d.ts.map +1 -1
- package/dist/prosekit-extensions-placeholder.js +2 -3
- package/dist/prosekit-extensions-placeholder.js.map +1 -1
- package/dist/prosekit-extensions-strike.js +2 -2
- package/dist/prosekit-extensions-strike.js.map +1 -1
- package/dist/prosekit-extensions-table.js +0 -1
- package/dist/prosekit-extensions-text-align.js +4 -4
- package/dist/prosekit-extensions-text-align.js.map +1 -1
- package/dist/prosekit-extensions-yjs.d.ts.map +1 -1
- package/dist/prosekit-extensions-yjs.js +3 -3
- package/dist/prosekit-extensions-yjs.js.map +1 -1
- package/package.json +15 -14
- package/src/autocomplete/autocomplete-helpers.ts +18 -9
- package/src/autocomplete/autocomplete-plugin.ts +261 -117
- package/src/autocomplete/autocomplete-rule.ts +3 -3
- package/src/autocomplete/autocomplete.spec.ts +239 -20
- package/src/autocomplete/autocomplete.ts +8 -0
- package/src/blockquote/blockquote-keymap.spec.ts +4 -4
- package/src/blockquote/blockquote-keymap.ts +1 -1
- package/src/commit/index.ts +1 -1
- package/src/hard-break/hard-break-keymap.spec.ts +5 -7
- package/src/heading/heading-keymap.spec.ts +7 -7
- package/src/heading/heading-keymap.ts +6 -6
- package/src/link/index.spec.ts +9 -8
- package/src/list/list-keymap.spec.ts +5 -5
- package/src/list/style.css +5 -5
- package/src/loro/loro-cursor-plugin.ts +23 -13
- package/src/loro/loro-keymap.ts +1 -1
- package/src/loro/loro.ts +14 -10
- package/src/paragraph/paragraph-keymap.ts +1 -1
- package/src/placeholder/index.ts +2 -1
- package/src/strike/index.ts +2 -2
- package/src/testing/index.ts +2 -2
- package/src/testing/keyboard.ts +1 -32
- package/src/text-align/index.ts +4 -4
- package/src/yjs/yjs-keymap.ts +1 -1
package/dist/list/style.css
CHANGED
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
& > .list-marker {
|
|
16
16
|
position: absolute;
|
|
17
|
-
left: 0;
|
|
18
17
|
width: 1.5em;
|
|
19
18
|
width: 1lh;
|
|
20
19
|
height: 1.5em;
|
|
21
20
|
height: 1lh;
|
|
21
|
+
inset-inline-start: 0;
|
|
22
22
|
text-align: center;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
& > .list-content {
|
|
26
|
-
margin-
|
|
27
|
-
margin-
|
|
26
|
+
margin-inline-start: 1.5em;
|
|
27
|
+
margin-inline-start: 1lh;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
&[data-list-kind="bullet"] > .list-marker,
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
&::before {
|
|
66
66
|
position: absolute;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
inset-inline-end: calc(100% - 1.5em);
|
|
68
|
+
inset-inline-end: calc(100% - 1lh);
|
|
69
69
|
content: counter(prosemirror-flat-list-counter, decimal) ". ";
|
|
70
70
|
font-variant-numeric: tabular-nums;
|
|
71
71
|
}
|
package/dist/list/style.css.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.css","names":[],"sources":["../../src/list/style.css"],"sourcesContent":[":root {\n --prosekit-list-bullet-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='2.5' fill='currentColor'/%3E%3C/svg%3E\");\n --prosekit-list-toggle-open-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpolygon points='8,10 12,14 16,10' fill='currentColor'/%3E%3C/svg%3E\");\n --prosekit-list-toggle-closed-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpolygon points='10,8 14,12 10,16' fill='currentColor'/%3E%3C/svg%3E\");\n}\n\n.prosemirror-flat-list {\n & {\n position: relative;\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n & > .list-marker {\n position: absolute;\n
|
|
1
|
+
{"version":3,"file":"style.css","names":[],"sources":["../../src/list/style.css"],"sourcesContent":[":root {\n --prosekit-list-bullet-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='2.5' fill='currentColor'/%3E%3C/svg%3E\");\n --prosekit-list-toggle-open-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpolygon points='8,10 12,14 16,10' fill='currentColor'/%3E%3C/svg%3E\");\n --prosekit-list-toggle-closed-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpolygon points='10,8 14,12 10,16' fill='currentColor'/%3E%3C/svg%3E\");\n}\n\n.prosemirror-flat-list {\n & {\n position: relative;\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n & > .list-marker {\n position: absolute;\n width: 1.5em;\n width: 1lh;\n height: 1.5em;\n height: 1lh;\n inset-inline-start: 0;\n text-align: center;\n }\n\n & > .list-content {\n margin-inline-start: 1.5em;\n margin-inline-start: 1lh;\n }\n\n &[data-list-kind=\"bullet\"] > .list-marker,\n &[data-list-kind=\"toggle\"] > .list-marker {\n background-color: currentColor;\n mask-position: center;\n mask-repeat: no-repeat;\n mask-size: contain;\n }\n\n &[data-list-kind=\"bullet\"] {\n & > .list-marker {\n mask-image: var(--prosekit-list-bullet-icon);\n }\n }\n\n &[data-list-kind=\"toggle\"] {\n & > .list-marker {\n mask-image: var(--prosekit-list-toggle-open-icon);\n }\n\n &[data-list-collapsable][data-list-collapsed] > .list-marker {\n mask-image: var(--prosekit-list-toggle-closed-icon);\n }\n }\n\n &[data-list-kind=\"ordered\"] {\n /*\n Ensure that the counters in children don't escape, so that the sub lists\n won't affect the counter of the parent list.\n \n See also https://github.com/ocavue/prosemirror-flat-list/issues/23\n */\n & > * {\n contain: style;\n }\n\n &::before {\n position: absolute;\n inset-inline-end: calc(100% - 1.5em);\n inset-inline-end: calc(100% - 1lh);\n content: counter(prosemirror-flat-list-counter, decimal) \". \";\n font-variant-numeric: tabular-nums;\n }\n counter-increment: prosemirror-flat-list-counter;\n\n /* \n Reset the counter for the first list node in the sequence.\n */\n &:first-child,\n :not(&) + & {\n counter-reset: prosemirror-flat-list-counter;\n\n /* \n If the first list node has a custom order number, set the counter to that value.\n */\n &[data-list-order] {\n @supports (counter-set: prosemirror-flat-list-counter 1) {\n counter-set: prosemirror-flat-list-counter var(--prosemirror-flat-list-order);\n }\n\n /* \n Safari older than version 17.2 doesn't support `counter-set` \n */\n @supports not (counter-set: prosemirror-flat-list-counter 1) {\n counter-increment: prosemirror-flat-list-counter var(--prosemirror-flat-list-order);\n }\n }\n }\n }\n\n &[data-list-kind=\"task\"] {\n & > .list-marker {\n &,\n & * {\n /* Make sure that the checkbox is at the center */\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n padding: 0;\n cursor: pointer;\n }\n }\n }\n\n &[data-list-kind=\"toggle\"] {\n &[data-list-collapsable] > .list-marker {\n cursor: pointer;\n }\n &:not([data-list-collapsable]) > .list-marker {\n opacity: 40%;\n pointer-events: none;\n }\n\n /* If collapsed, hide the second and futher children */\n &[data-list-collapsable][data-list-collapsed] > .list-content > *:nth-child(n+2) {\n display: none;\n }\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA"}
|
|
@@ -60,7 +60,7 @@ interface AutocompleteRuleOptions {
|
|
|
60
60
|
* The regular expression to match against the text before the cursor. The
|
|
61
61
|
* last match before the cursor is used.
|
|
62
62
|
*
|
|
63
|
-
* For a slash menu, you might use `/(?<!\S)\/(
|
|
63
|
+
* For a slash menu, you might use `/(?<!\S)\/(\S.*)?$/u`.
|
|
64
64
|
* For a mention, you might use `/@\w*$/`
|
|
65
65
|
*/
|
|
66
66
|
regex: RegExp;
|
|
@@ -75,8 +75,8 @@ interface AutocompleteRuleOptions {
|
|
|
75
75
|
onLeave?: VoidFunction;
|
|
76
76
|
/**
|
|
77
77
|
* A predicate to determine if the rule can be applied in the current editor
|
|
78
|
-
* state. If not provided, it defaults to only allowing matches
|
|
79
|
-
*
|
|
78
|
+
* state. If not provided, it defaults to only allowing matches that are not
|
|
79
|
+
* inside a code block or code mark.
|
|
80
80
|
*/
|
|
81
81
|
canMatch?: CanMatchPredicate;
|
|
82
82
|
}
|
|
@@ -100,6 +100,14 @@ declare class AutocompleteRule {
|
|
|
100
100
|
}
|
|
101
101
|
//#endregion
|
|
102
102
|
//#region src/autocomplete/autocomplete.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* Defines an autocomplete extension that executes logic when the text before
|
|
105
|
+
* the cursor matches the given regular expression.
|
|
106
|
+
*
|
|
107
|
+
* When a match is found, an inline decoration is applied to the matched text
|
|
108
|
+
* with the class `prosekit-autocomplete-match` and a `data-autocomplete-match-text`
|
|
109
|
+
* attribute containing the full matched string.
|
|
110
|
+
*/
|
|
103
111
|
declare function defineAutocomplete(rule: AutocompleteRule): Extension;
|
|
104
112
|
//#endregion
|
|
105
113
|
export { AutocompleteRule, type AutocompleteRuleOptions, type CanMatchOptions, type CanMatchPredicate, type MatchHandler, type MatchHandlerOptions, defineAutocomplete };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prosekit-extensions-autocomplete.d.ts","names":[],"sources":["../src/autocomplete/autocomplete-rule.ts","../src/autocomplete/autocomplete.ts"],"sourcesContent":[],"mappings":";;;;;;;AAOiB,UAAA,mBAAA,CAAmB;EAuCxB;AAKZ;AAUA;EAKiB,KAAA,EAvDR,WAuDQ;EAQR;;;EAkBI,KAAA,EA5EJ,eA4EI;EAAiB;AAQ9B;;EAIoB,IAAA,EAAA,MAAA;EAEC;;;EAIyB,EAAA,EAAA,MAAA;;;;
|
|
1
|
+
{"version":3,"file":"prosekit-extensions-autocomplete.d.ts","names":[],"sources":["../src/autocomplete/autocomplete-rule.ts","../src/autocomplete/autocomplete.ts"],"sourcesContent":[],"mappings":";;;;;;;AAOiB,UAAA,mBAAA,CAAmB;EAuCxB;AAKZ;AAUA;EAKiB,KAAA,EAvDR,WAuDQ;EAQR;;;EAkBI,KAAA,EA5EJ,eA4EI;EAAiB;AAQ9B;;EAIoB,IAAA,EAAA,MAAA;EAEC;;;EAIyB,EAAA,EAAA,MAAA;;;;AC3F9C;;;;;;;;;;;;;KD2BY,YAAA,aAAyB;;;;UAKpB,eAAA;;;;SAIR;;;;;KAMG,iBAAA,aAA8B;;;;UAKzB,uBAAA;;;;;;;;SAQR;;;;;WAME;;;;YAKC;;;;;;aAOC;;;;;;;cAQA,gBAAA;;kBAEK;;oBAEE;;qBAEC;;;WAEmB;;uBAEjB;;;;;;AAvGvB;AAuCA;AAKA;AAUA;AAKA;;AAcW,iBC7DK,kBAAA,CD6DL,IAAA,EC7D8B,gBD6D9B,CAAA,EC7DiD,SD6DjD"}
|
|
@@ -4,11 +4,16 @@ import { Decoration, DecorationSet } from "@prosekit/pm/view";
|
|
|
4
4
|
|
|
5
5
|
//#region src/autocomplete/autocomplete-helpers.ts
|
|
6
6
|
function defaultCanMatch({ state }) {
|
|
7
|
-
|
|
7
|
+
const $pos = state.selection.$from;
|
|
8
|
+
return !isInsideCodeBlock($pos) && !isInsideCodeMark($pos);
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
+
function isInsideCodeBlock($pos) {
|
|
10
11
|
for (let d = $pos.depth; d > 0; d--) if ($pos.node(d).type.spec.code) return true;
|
|
11
|
-
return
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
function isInsideCodeMark($pos) {
|
|
15
|
+
for (const mark of $pos.marks()) if (mark.type.spec.code) return true;
|
|
16
|
+
return false;
|
|
12
17
|
}
|
|
13
18
|
function getPluginState(state) {
|
|
14
19
|
return pluginKey.getState(state);
|
|
@@ -23,6 +28,23 @@ const pluginKey = new PluginKey("prosekit-autocomplete");
|
|
|
23
28
|
|
|
24
29
|
//#endregion
|
|
25
30
|
//#region src/autocomplete/autocomplete-plugin.ts
|
|
31
|
+
/**
|
|
32
|
+
* Creates a plugin that handles autocomplete functionality.
|
|
33
|
+
*
|
|
34
|
+
* Workflow:
|
|
35
|
+
*
|
|
36
|
+
* 1. {@link handleTextInput}: called when text is going to be input, but the
|
|
37
|
+
* transaction is not yet created. Injects a new matching as a transaction
|
|
38
|
+
* meta if applicable. This is the only place to create a new matching if
|
|
39
|
+
* there is no existing matching.
|
|
40
|
+
* 2. {@link handleTransaction}: called when a transaction is going to be
|
|
41
|
+
* applied. Updates the plugin state based on the transaction. This step
|
|
42
|
+
* determines if a matching should be created, updated or removed.
|
|
43
|
+
* 3. {@link handleUpdate}: called when the editor state is updated. This is the
|
|
44
|
+
* place to call `onMatch` and register `deleteMatch` and `ignoreMatch`
|
|
45
|
+
* callbacks.
|
|
46
|
+
* 4. {@link getDecorations}: creates the decorations for the current matching.
|
|
47
|
+
*/
|
|
26
48
|
function createAutocompletePlugin({ getRules }) {
|
|
27
49
|
return new Plugin({
|
|
28
50
|
key: pluginKey,
|
|
@@ -34,82 +56,171 @@ function createAutocompletePlugin({ getRules }) {
|
|
|
34
56
|
};
|
|
35
57
|
},
|
|
36
58
|
apply: (tr, prevValue, oldState, newState) => {
|
|
37
|
-
|
|
38
|
-
if (!tr.docChanged && oldState.selection.eq(newState.selection) && !meta) return prevValue;
|
|
39
|
-
if (meta) {
|
|
40
|
-
let ignores = prevValue.ignores;
|
|
41
|
-
if (!ignores.includes(meta.ignore)) ignores = [...ignores, meta.ignore];
|
|
42
|
-
return {
|
|
43
|
-
matching: null,
|
|
44
|
-
ignores
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const ignoreSet = new Set(prevValue.ignores.map((pos) => tr.mapping.map(pos)));
|
|
48
|
-
let matching = calcPluginStateMatching(newState, getRules());
|
|
49
|
-
if (matching && ignoreSet.has(matching.from)) matching = null;
|
|
50
|
-
return {
|
|
51
|
-
matching,
|
|
52
|
-
ignores: Array.from(ignoreSet)
|
|
53
|
-
};
|
|
59
|
+
return handleTransaction(tr, prevValue, oldState, newState, getRules);
|
|
54
60
|
}
|
|
55
61
|
},
|
|
56
|
-
view: () => ({ update:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
state: view.state,
|
|
71
|
-
match,
|
|
72
|
-
from,
|
|
73
|
-
to,
|
|
74
|
-
deleteMatch,
|
|
75
|
-
ignoreMatch
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
} }),
|
|
79
|
-
props: { decorations: (state) => {
|
|
80
|
-
const pluginState = getPluginState(state);
|
|
81
|
-
if (pluginState?.matching) {
|
|
82
|
-
const { from, to } = pluginState.matching;
|
|
83
|
-
const deco = Decoration.inline(from, to, { class: "prosemirror-prediction-match" });
|
|
84
|
-
return DecorationSet.create(state.doc, [deco]);
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
} }
|
|
62
|
+
view: () => ({ update: handleUpdate }),
|
|
63
|
+
props: {
|
|
64
|
+
handleTextInput: (view, from, to, textAdded, getTr) => {
|
|
65
|
+
const meta = handleTextInput(view, from, to, textAdded, getRules);
|
|
66
|
+
if (meta) {
|
|
67
|
+
const tr = getTr();
|
|
68
|
+
setTrMeta(tr, meta);
|
|
69
|
+
view.dispatch(tr);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
decorations: getDecorations
|
|
75
|
+
}
|
|
88
76
|
});
|
|
89
77
|
}
|
|
78
|
+
function handleTextInput(view, from, to, textAdded, getRules) {
|
|
79
|
+
if (from !== to) return;
|
|
80
|
+
const textFull = getTextBackward(view.state.doc.resolve(from)) + textAdded;
|
|
81
|
+
const textTo = to + textAdded.length;
|
|
82
|
+
const textFrom = textTo - textFull.length;
|
|
83
|
+
const ignores = getPluginState(view.state)?.ignores ?? [];
|
|
84
|
+
const currMatching = matchRule(view.state, getRules(), textFull, textFrom, textTo, ignores);
|
|
85
|
+
if (currMatching) return {
|
|
86
|
+
type: "enter",
|
|
87
|
+
matching: currMatching
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function handleTransaction(tr, prevValue, oldState, newState, getRules) {
|
|
91
|
+
const meta = getTrMeta(tr);
|
|
92
|
+
if (!meta && !tr.docChanged && oldState.selection.eq(newState.selection)) return prevValue;
|
|
93
|
+
const ignoreSet = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const ignore of prevValue.ignores) {
|
|
95
|
+
const result = tr.mapping.mapResult(ignore);
|
|
96
|
+
if (!result.deletedBefore && !result.deletedAfter) ignoreSet.add(result.pos);
|
|
97
|
+
}
|
|
98
|
+
const ignores = Array.from(ignoreSet);
|
|
99
|
+
const prevMatching = prevValue.matching && mapMatching(prevValue.matching, tr.mapping);
|
|
100
|
+
if (!meta) {
|
|
101
|
+
if (!prevMatching) return {
|
|
102
|
+
matching: null,
|
|
103
|
+
ignores
|
|
104
|
+
};
|
|
105
|
+
const { selection } = newState;
|
|
106
|
+
if (selection.to < prevMatching.from || selection.from > prevMatching.to) {
|
|
107
|
+
ignores.push(prevMatching.from);
|
|
108
|
+
return {
|
|
109
|
+
matching: null,
|
|
110
|
+
ignores
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const text = getTextBetween(newState.doc, prevMatching.from, prevMatching.to);
|
|
114
|
+
return {
|
|
115
|
+
matching: matchRule(newState, getRules(), text, prevMatching.from, prevMatching.to, ignores) ?? null,
|
|
116
|
+
ignores
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (meta.type === "enter") {
|
|
120
|
+
if (prevMatching && prevMatching.from !== meta.matching.from) ignores.push(prevMatching.from);
|
|
121
|
+
return {
|
|
122
|
+
matching: meta.matching,
|
|
123
|
+
ignores
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (meta.type === "leave") {
|
|
127
|
+
if (prevMatching) ignores.push(prevMatching.from);
|
|
128
|
+
return {
|
|
129
|
+
matching: null,
|
|
130
|
+
ignores
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Invalid transaction meta: ${meta}`);
|
|
134
|
+
}
|
|
135
|
+
function handleUpdate(view, prevState) {
|
|
136
|
+
const prevValue = getPluginState(prevState);
|
|
137
|
+
const currValue = getPluginState(view.state);
|
|
138
|
+
if (!prevValue || !currValue) return;
|
|
139
|
+
const prevMatching = prevValue.matching;
|
|
140
|
+
const currMatching = currValue.matching;
|
|
141
|
+
if (prevMatching && prevMatching.rule !== currMatching?.rule) prevMatching.rule.onLeave?.();
|
|
142
|
+
if (currMatching) {
|
|
143
|
+
const { from, to, match, rule } = currMatching;
|
|
144
|
+
const textSnapshot = getTextBetween(view.state.doc, from, to);
|
|
145
|
+
const deleteMatch = () => {
|
|
146
|
+
if (getTextBetween(view.state.doc, from, to) === textSnapshot) view.dispatch(view.state.tr.delete(from, to));
|
|
147
|
+
};
|
|
148
|
+
const ignoreMatch = () => {
|
|
149
|
+
view.dispatch(setTrMeta(view.state.tr, { type: "leave" }));
|
|
150
|
+
};
|
|
151
|
+
rule.onMatch({
|
|
152
|
+
state: view.state,
|
|
153
|
+
match,
|
|
154
|
+
from,
|
|
155
|
+
to,
|
|
156
|
+
deleteMatch,
|
|
157
|
+
ignoreMatch
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function getDecorations(state) {
|
|
162
|
+
const pluginState = getPluginState(state);
|
|
163
|
+
if (pluginState?.matching) {
|
|
164
|
+
const { from, to, match } = pluginState.matching;
|
|
165
|
+
const deco = Decoration.inline(from, to, {
|
|
166
|
+
"class": "prosekit-autocomplete-match",
|
|
167
|
+
"data-autocomplete-match-text": match[0]
|
|
168
|
+
});
|
|
169
|
+
return DecorationSet.create(state.doc, [deco]);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
90
173
|
const MAX_MATCH = 200;
|
|
91
|
-
|
|
92
|
-
|
|
174
|
+
/** Get the text before the given position at the current block. */
|
|
175
|
+
function getTextBackward($pos) {
|
|
93
176
|
const parentOffset = $pos.parentOffset;
|
|
94
|
-
|
|
177
|
+
return getTextBetween($pos.parent, Math.max(0, parentOffset - MAX_MATCH), parentOffset);
|
|
178
|
+
}
|
|
179
|
+
function getTextBetween(node, from, to) {
|
|
180
|
+
return node.textBetween(from, to, null, OBJECT_REPLACEMENT_CHARACTER);
|
|
181
|
+
}
|
|
182
|
+
function matchRule(state, rules, text, textFrom, textTo, ignores) {
|
|
183
|
+
let maxIgnore = -1;
|
|
184
|
+
for (const ignore of ignores) if (ignore >= textFrom && ignore < textTo && ignore > maxIgnore) maxIgnore = ignore;
|
|
185
|
+
if (maxIgnore >= 0) {
|
|
186
|
+
const cut = maxIgnore + 1 - textFrom;
|
|
187
|
+
text = text.slice(cut);
|
|
188
|
+
textFrom += cut;
|
|
189
|
+
}
|
|
190
|
+
if (textFrom >= textTo || !text) return;
|
|
95
191
|
for (const rule of rules) {
|
|
96
192
|
if (!rule.canMatch({ state })) continue;
|
|
97
193
|
rule.regex.lastIndex = 0;
|
|
98
|
-
const match = rule.regex.exec(
|
|
194
|
+
const match = rule.regex.exec(text);
|
|
99
195
|
if (!match) continue;
|
|
100
|
-
const
|
|
196
|
+
const matchTo = textTo;
|
|
101
197
|
return {
|
|
102
198
|
rule,
|
|
103
199
|
match,
|
|
104
|
-
from:
|
|
105
|
-
to
|
|
200
|
+
from: textFrom + match.index,
|
|
201
|
+
to: matchTo
|
|
106
202
|
};
|
|
107
203
|
}
|
|
108
|
-
|
|
204
|
+
}
|
|
205
|
+
function mapMatching(matching, mapping) {
|
|
206
|
+
return {
|
|
207
|
+
rule: matching.rule,
|
|
208
|
+
match: matching.match,
|
|
209
|
+
from: mapping.map(matching.from),
|
|
210
|
+
to: mapping.map(matching.to, -1)
|
|
211
|
+
};
|
|
109
212
|
}
|
|
110
213
|
|
|
111
214
|
//#endregion
|
|
112
215
|
//#region src/autocomplete/autocomplete.ts
|
|
216
|
+
/**
|
|
217
|
+
* Defines an autocomplete extension that executes logic when the text before
|
|
218
|
+
* the cursor matches the given regular expression.
|
|
219
|
+
*
|
|
220
|
+
* When a match is found, an inline decoration is applied to the matched text
|
|
221
|
+
* with the class `prosekit-autocomplete-match` and a `data-autocomplete-match-text`
|
|
222
|
+
* attribute containing the full matched string.
|
|
223
|
+
*/
|
|
113
224
|
function defineAutocomplete(rule) {
|
|
114
225
|
return defineFacetPayload(autocompleteFacet, [rule]);
|
|
115
226
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prosekit-extensions-autocomplete.js","names":["pluginKey: PluginKey<PredictionPluginState>","parentOffset: number","textBefore: string","rules: AutocompleteRule[]"],"sources":["../src/autocomplete/autocomplete-helpers.ts","../src/autocomplete/autocomplete-plugin.ts","../src/autocomplete/autocomplete.ts","../src/autocomplete/autocomplete-rule.ts"],"sourcesContent":["import type { ResolvedPos } from '@prosekit/pm/model'\nimport {\n PluginKey,\n type EditorState,\n type Transaction,\n} from '@prosekit/pm/state'\n\nimport type { AutocompleteRule } from './autocomplete-rule'\n\nexport function defaultCanMatch({ state }: { state: EditorState }): boolean {\n return state.selection.empty && !isInsideCode(state.selection.$from)\n}\n\nfunction isInsideCode($pos: ResolvedPos): boolean {\n for (let d = $pos.depth; d > 0; d--) {\n if ($pos.node(d).type.spec.code) {\n return true\n }\n }\n\n return $pos.marks().some((mark) => mark.type.name === 'code')\n}\n\n/**\n * @internal\n */\nexport interface PredictionPluginMatching {\n rule: AutocompleteRule\n from: number\n to: number\n match: RegExpExecArray\n}\n\n/**\n * @internal\n */\nexport interface PredictionPluginState {\n /**\n * The matching positions that should be ignored.\n */\n ignores: number[]\n\n /**\n * The current active matching.\n */\n matching: PredictionPluginMatching | null\n}\n\n/**\n * @internal\n */\ninterface PredictionTransactionMeta {\n /**\n * The from position that should be ignored.\n */\n ignore: number\n}\n\nexport function getPluginState(state: EditorState): PredictionPluginState | undefined {\n return pluginKey.getState(state)\n}\n\nexport function getTrMeta(tr: Transaction): PredictionTransactionMeta | undefined {\n return tr.getMeta(pluginKey) as PredictionTransactionMeta | undefined\n}\n\nexport function setTrMeta(\n tr: Transaction,\n meta: PredictionTransactionMeta,\n): Transaction {\n return tr.setMeta(pluginKey, meta)\n}\n\nexport const pluginKey: PluginKey<PredictionPluginState> = new PluginKey<PredictionPluginState>('prosekit-autocomplete')\n","import { OBJECT_REPLACEMENT_CHARACTER } from '@prosekit/core'\nimport {\n Plugin,\n type EditorState,\n type Transaction,\n} from '@prosekit/pm/state'\nimport {\n Decoration,\n DecorationSet,\n} from '@prosekit/pm/view'\n\nimport {\n getPluginState,\n getTrMeta,\n pluginKey,\n setTrMeta,\n type PredictionPluginMatching,\n type PredictionPluginState,\n} from './autocomplete-helpers'\nimport type { AutocompleteRule } from './autocomplete-rule'\n\nexport function createAutocompletePlugin({\n getRules,\n}: {\n getRules: () => AutocompleteRule[]\n}): Plugin {\n return new Plugin<PredictionPluginState>({\n key: pluginKey,\n\n state: {\n init: (): PredictionPluginState => {\n return { ignores: [], matching: null }\n },\n apply: (\n tr: Transaction,\n prevValue: PredictionPluginState,\n oldState: EditorState,\n newState: EditorState,\n ): PredictionPluginState => {\n const meta = getTrMeta(tr)\n\n // No changes\n if (\n !tr.docChanged\n && oldState.selection.eq(newState.selection)\n && !meta\n ) {\n return prevValue\n }\n\n // Receiving a meta means that we are ignoring a match\n if (meta) {\n let ignores = prevValue.ignores\n if (!ignores.includes(meta.ignore)) {\n ignores = [...ignores, meta.ignore]\n }\n return { matching: null, ignores }\n }\n\n // Calculate the new ignores\n const ignoreSet = new Set(prevValue.ignores.map(pos => tr.mapping.map(pos)))\n\n // Calculate the new matching\n let matching = calcPluginStateMatching(newState, getRules())\n\n // Check if the matching should be ignored\n if (matching && ignoreSet.has(matching.from)) {\n matching = null\n }\n\n // Return the new matching and ignores\n return { matching, ignores: Array.from(ignoreSet) }\n },\n },\n\n view: () => ({\n update: (view, prevState) => {\n const prevValue = getPluginState(prevState)\n const currValue = getPluginState(view.state)\n\n if (\n prevValue?.matching\n && prevValue.matching.rule !== currValue?.matching?.rule\n ) {\n // Deactivate the previous rule\n prevValue.matching.rule.onLeave?.()\n }\n\n if (\n currValue?.matching\n && !currValue.ignores.includes(currValue.matching.from)\n ) {\n // Activate the current rule\n\n const { from, to, match, rule } = currValue.matching\n\n const textContent = view.state.doc.textBetween(\n from,\n to,\n null,\n OBJECT_REPLACEMENT_CHARACTER,\n )\n\n const deleteMatch = () => {\n if (\n view.state.doc.textBetween(\n from,\n to,\n null,\n OBJECT_REPLACEMENT_CHARACTER,\n ) === textContent\n ) {\n view.dispatch(view.state.tr.delete(from, to))\n }\n }\n\n const ignoreMatch = () => {\n view.dispatch(\n setTrMeta(view.state.tr, { ignore: from }),\n )\n }\n\n rule.onMatch({\n state: view.state,\n match,\n from,\n to,\n deleteMatch,\n ignoreMatch,\n })\n }\n },\n }),\n\n props: {\n decorations: (state: EditorState) => {\n const pluginState = getPluginState(state)\n if (pluginState?.matching) {\n const { from, to } = pluginState.matching\n const deco = Decoration.inline(from, to, {\n class: 'prosemirror-prediction-match',\n })\n return DecorationSet.create(state.doc, [deco])\n }\n return null\n },\n },\n })\n}\n\nconst MAX_MATCH = 200\n\nfunction calcPluginStateMatching(\n state: EditorState,\n rules: AutocompleteRule[],\n): PredictionPluginMatching | null {\n const $pos = state.selection.$from\n\n const parentOffset: number = $pos.parentOffset\n\n const textBefore: string = $pos.parent.textBetween(\n Math.max(0, parentOffset - MAX_MATCH),\n parentOffset,\n null,\n OBJECT_REPLACEMENT_CHARACTER,\n )\n\n for (const rule of rules) {\n if (!rule.canMatch({ state })) {\n continue\n }\n\n rule.regex.lastIndex = 0\n const match = rule.regex.exec(textBefore)\n if (!match) {\n continue\n }\n\n const to = $pos.pos\n const from = to - textBefore.length + match.index\n\n return { rule, match, from, to }\n }\n\n return null\n}\n","import {\n defineFacet,\n defineFacetPayload,\n pluginFacet,\n type Extension,\n type PluginPayload,\n} from '@prosekit/core'\n\nimport { createAutocompletePlugin } from './autocomplete-plugin'\nimport type { AutocompleteRule } from './autocomplete-rule'\n\nexport function defineAutocomplete(rule: AutocompleteRule): Extension {\n return defineFacetPayload(autocompleteFacet, [rule])\n}\n\nconst autocompleteFacet = defineFacet<AutocompleteRule, PluginPayload>({\n reduce: () => {\n let rules: AutocompleteRule[] = []\n const getRules = () => rules\n const plugin = createAutocompletePlugin({ getRules })\n\n return function reducer(inputs) {\n rules = inputs\n return plugin\n }\n },\n parent: pluginFacet,\n singleton: true,\n})\n","import type { EditorState } from '@prosekit/pm/state'\n\nimport { defaultCanMatch } from './autocomplete-helpers'\n\n/**\n * Options for the {@link MatchHandler} callback.\n */\nexport interface MatchHandlerOptions {\n /**\n * The editor state.\n */\n state: EditorState\n\n /**\n * The result of `RegExp.exec`.\n */\n match: RegExpExecArray\n\n /**\n * The start position of the matched text.\n */\n from: number\n\n /**\n * The end position of the matched text.\n */\n to: number\n\n /**\n * Call this function to ignore the match. You probably want to call this\n * function when the user presses the `Escape` key.\n */\n ignoreMatch: () => void\n\n /**\n * Call this function to delete the matched text. For example, in a slash\n * menu, you might want to delete the matched text first then do something\n * else when the user presses the `Enter` key.\n */\n deleteMatch: () => void\n}\n\n/**\n * A callback that is called when the rule starts to match, and also on\n * subsequent updates while the rule continues to match.\n */\nexport type MatchHandler = (options: MatchHandlerOptions) => void\n\n/**\n * Options for the {@link CanMatchPredicate} callback.\n */\nexport interface CanMatchOptions {\n /**\n * The editor state.\n */\n state: EditorState\n}\n\n/**\n * A predicate to determine if the rule can be applied in the current editor state.\n */\nexport type CanMatchPredicate = (options: CanMatchOptions) => boolean\n\n/**\n * Options for creating an {@link AutocompleteRule}\n */\nexport interface AutocompleteRuleOptions {\n /**\n * The regular expression to match against the text before the cursor. The\n * last match before the cursor is used.\n *\n * For a slash menu, you might use `/(?<!\\S)\\/(|\\S.*)$/u`.\n * For a mention, you might use `/@\\w*$/`\n */\n regex: RegExp\n\n /**\n * A callback that is called when the rule starts to match, and also on\n * subsequent updates while the rule continues to match.\n */\n onEnter: MatchHandler\n\n /**\n * A callback that is called when the rule stops matching.\n */\n onLeave?: VoidFunction\n\n /**\n * A predicate to determine if the rule can be applied in the current editor\n * state. If not provided, it defaults to only allowing matches in empty\n * selections that are not inside a code block or code mark.\n */\n canMatch?: CanMatchPredicate\n}\n\n/**\n * An autocomplete rule that can be used to create an autocomplete extension.\n *\n * @public\n */\nexport class AutocompleteRule {\n /** @internal */\n readonly regex: RegExp\n /** @internal */\n readonly onMatch: MatchHandler\n /** @internal */\n readonly onLeave?: VoidFunction\n /** @internal */\n readonly canMatch: (options: { state: EditorState }) => boolean\n\n constructor(options: AutocompleteRuleOptions) {\n this.regex = options.regex\n this.onMatch = options.onEnter\n this.onLeave = options.onLeave\n this.canMatch = options.canMatch ?? defaultCanMatch\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prosekit-extensions-autocomplete.js","names":["pluginKey: PluginKey<PredictionPluginState>","parentOffset: number","rules: AutocompleteRule[]"],"sources":["../src/autocomplete/autocomplete-helpers.ts","../src/autocomplete/autocomplete-plugin.ts","../src/autocomplete/autocomplete.ts","../src/autocomplete/autocomplete-rule.ts"],"sourcesContent":["import type { ResolvedPos } from '@prosekit/pm/model'\nimport {\n PluginKey,\n type EditorState,\n type Transaction,\n} from '@prosekit/pm/state'\n\nimport type { AutocompleteRule } from './autocomplete-rule'\n\nexport function defaultCanMatch({ state }: { state: EditorState }): boolean {\n const $pos = state.selection.$from\n return !isInsideCodeBlock($pos) && !isInsideCodeMark($pos)\n}\n\nfunction isInsideCodeBlock($pos: ResolvedPos): boolean {\n for (let d = $pos.depth; d > 0; d--) {\n if ($pos.node(d).type.spec.code) {\n return true\n }\n }\n return false\n}\n\nfunction isInsideCodeMark($pos: ResolvedPos): boolean {\n for (const mark of $pos.marks()) {\n if (mark.type.spec.code) {\n return true\n }\n }\n return false\n}\n\n/**\n * @internal\n */\nexport interface PredictionPluginMatching {\n rule: AutocompleteRule\n from: number\n to: number\n match: RegExpExecArray\n}\n\n/**\n * @internal\n */\nexport interface PredictionPluginState {\n /**\n * The matching positions that should be ignored.\n */\n ignores: Array<number>\n\n /**\n * The current active matching.\n */\n matching: PredictionPluginMatching | null\n}\n\n/**\n * @internal\n */\nexport type PredictionTransactionMeta = {\n type: 'enter'\n matching: PredictionPluginMatching\n} | {\n type: 'leave'\n}\n\nexport function getPluginState(state: EditorState): PredictionPluginState | undefined {\n return pluginKey.getState(state)\n}\n\nexport function getTrMeta(tr: Transaction): PredictionTransactionMeta | undefined {\n return tr.getMeta(pluginKey) as PredictionTransactionMeta | undefined\n}\n\nexport function setTrMeta(\n tr: Transaction,\n meta: PredictionTransactionMeta,\n): Transaction {\n return tr.setMeta(pluginKey, meta)\n}\n\nexport const pluginKey: PluginKey<PredictionPluginState> = new PluginKey<PredictionPluginState>('prosekit-autocomplete')\n","import { OBJECT_REPLACEMENT_CHARACTER } from '@prosekit/core'\nimport type {\n ProseMirrorNode,\n ResolvedPos,\n} from '@prosekit/pm/model'\nimport {\n Plugin,\n type EditorState,\n type Transaction,\n} from '@prosekit/pm/state'\nimport type { Mapping } from '@prosekit/pm/transform'\nimport type { EditorView } from '@prosekit/pm/view'\nimport {\n Decoration,\n DecorationSet,\n} from '@prosekit/pm/view'\n\nimport {\n getPluginState,\n getTrMeta,\n pluginKey,\n setTrMeta,\n type PredictionPluginMatching,\n type PredictionPluginState,\n type PredictionTransactionMeta,\n} from './autocomplete-helpers'\nimport type { AutocompleteRule } from './autocomplete-rule'\n\n/**\n * Creates a plugin that handles autocomplete functionality.\n *\n * Workflow:\n *\n * 1. {@link handleTextInput}: called when text is going to be input, but the\n * transaction is not yet created. Injects a new matching as a transaction\n * meta if applicable. This is the only place to create a new matching if\n * there is no existing matching.\n * 2. {@link handleTransaction}: called when a transaction is going to be\n * applied. Updates the plugin state based on the transaction. This step\n * determines if a matching should be created, updated or removed.\n * 3. {@link handleUpdate}: called when the editor state is updated. This is the\n * place to call `onMatch` and register `deleteMatch` and `ignoreMatch`\n * callbacks.\n * 4. {@link getDecorations}: creates the decorations for the current matching.\n */\nexport function createAutocompletePlugin({\n getRules,\n}: {\n getRules: () => AutocompleteRule[]\n}): Plugin {\n return new Plugin<PredictionPluginState>({\n key: pluginKey,\n\n state: {\n init: (): PredictionPluginState => {\n return { ignores: [], matching: null }\n },\n apply: (tr, prevValue, oldState, newState): PredictionPluginState => {\n return handleTransaction(tr, prevValue, oldState, newState, getRules)\n },\n },\n\n view: () => ({\n update: handleUpdate,\n }),\n\n props: {\n handleTextInput: (view, from, to, textAdded, getTr) => {\n const meta = handleTextInput(view, from, to, textAdded, getRules)\n if (meta) {\n const tr = getTr()\n setTrMeta(tr, meta)\n view.dispatch(tr)\n return true\n }\n return false\n },\n decorations: getDecorations,\n },\n })\n}\n\nfunction handleTextInput(\n view: EditorView,\n from: number,\n to: number,\n textAdded: string,\n getRules: () => AutocompleteRule[],\n): PredictionTransactionMeta | undefined {\n // Only handle insertions\n if (from !== to) {\n return\n }\n\n const textBackward = getTextBackward(view.state.doc.resolve(from))\n const textFull = textBackward + textAdded\n const textTo = to + textAdded.length\n const textFrom = textTo - textFull.length\n\n const pluginState = getPluginState(view.state)\n const ignores = pluginState?.ignores ?? []\n\n const currMatching = matchRule(\n view.state,\n getRules(),\n textFull,\n textFrom,\n textTo,\n ignores,\n )\n\n if (currMatching) {\n return { type: 'enter', matching: currMatching }\n }\n}\n\nfunction handleTransaction(\n tr: Transaction,\n prevValue: PredictionPluginState,\n oldState: EditorState,\n newState: EditorState,\n getRules: () => AutocompleteRule[],\n): PredictionPluginState {\n const meta = getTrMeta(tr)\n\n if (\n !meta\n && !tr.docChanged\n && oldState.selection.eq(newState.selection)\n ) {\n // No changes\n return prevValue\n }\n\n // Handle position mapping changes\n const ignoreSet = new Set<number>()\n for (const ignore of prevValue.ignores) {\n const result = tr.mapping.mapResult(ignore)\n if (!result.deletedBefore && !result.deletedAfter) {\n ignoreSet.add(result.pos)\n }\n }\n const ignores = Array.from(ignoreSet)\n\n const prevMatching = prevValue.matching && mapMatching(prevValue.matching, tr.mapping)\n\n // If there is no new matching from `handleTextInput`\n if (!meta) {\n if (!prevMatching) {\n return { matching: null, ignores }\n }\n\n const { selection } = newState\n // If the text selection is before the matching or after the matching,\n // we leave the matching\n if (selection.to < prevMatching.from || selection.from > prevMatching.to) {\n ignores.push(prevMatching.from)\n return { matching: null, ignores }\n }\n\n // Get the text between the existing matching\n const text = getTextBetween(newState.doc, prevMatching.from, prevMatching.to)\n // Check the text again to see if it still matches the rule\n const currMatching = matchRule(\n newState,\n getRules(),\n text,\n prevMatching.from,\n prevMatching.to,\n ignores,\n )\n return { matching: currMatching ?? null, ignores }\n }\n\n // If a new matching is being entered from `handleTextInput`\n if (meta.type === 'enter') {\n // Ignore the previous matching if it is not the same as the new matching\n if (prevMatching && prevMatching.from !== meta.matching.from) {\n ignores.push(prevMatching.from)\n }\n\n // Return the new matching\n return { matching: meta.matching, ignores }\n }\n\n // If a matching is being exited\n if (meta.type === 'leave') {\n if (prevMatching) {\n ignores.push(prevMatching.from)\n }\n return { matching: null, ignores }\n }\n\n throw new Error(`Invalid transaction meta: ${meta satisfies never}`)\n}\n\nfunction handleUpdate(view: EditorView, prevState: EditorState): void {\n const prevValue = getPluginState(prevState)\n const currValue = getPluginState(view.state)\n\n if (!prevValue || !currValue) {\n // Should not happen\n return\n }\n\n const prevMatching = prevValue.matching\n const currMatching = currValue.matching\n\n // Deactivate the previous rule\n if (prevMatching && prevMatching.rule !== currMatching?.rule) {\n prevMatching.rule.onLeave?.()\n }\n\n // Activate the current rule\n if (currMatching) {\n const { from, to, match, rule } = currMatching\n\n const textSnapshot = getTextBetween(view.state.doc, from, to)\n\n const deleteMatch = () => {\n if (getTextBetween(view.state.doc, from, to) === textSnapshot) {\n view.dispatch(view.state.tr.delete(from, to))\n }\n }\n\n const ignoreMatch = () => {\n view.dispatch(\n setTrMeta(view.state.tr, { type: 'leave' }),\n )\n }\n\n rule.onMatch({\n state: view.state,\n match,\n from,\n to,\n deleteMatch,\n ignoreMatch,\n })\n }\n}\n\nfunction getDecorations(state: EditorState): DecorationSet | null {\n const pluginState = getPluginState(state)\n if (pluginState?.matching) {\n const { from, to, match } = pluginState.matching\n const deco = Decoration.inline(from, to, {\n 'class': 'prosekit-autocomplete-match',\n 'data-autocomplete-match-text': match[0],\n })\n return DecorationSet.create(state.doc, [deco])\n }\n return null\n}\n\nconst MAX_MATCH = 200\n\n/** Get the text before the given position at the current block. */\nfunction getTextBackward($pos: ResolvedPos): string {\n const parentOffset: number = $pos.parentOffset\n return getTextBetween(\n $pos.parent,\n Math.max(0, parentOffset - MAX_MATCH),\n parentOffset,\n )\n}\n\nfunction getTextBetween(node: ProseMirrorNode, from: number, to: number): string {\n return node.textBetween(\n from,\n to,\n null,\n OBJECT_REPLACEMENT_CHARACTER,\n )\n}\n\nfunction matchRule(\n state: EditorState,\n rules: AutocompleteRule[],\n text: string,\n textFrom: number,\n textTo: number,\n ignores: Array<number>,\n): PredictionPluginMatching | undefined {\n // Find the rightmost ignore point within the text range\n let maxIgnore = -1\n for (const ignore of ignores) {\n if (ignore >= textFrom && ignore < textTo && ignore > maxIgnore) {\n maxIgnore = ignore\n }\n }\n\n // If an ignore point is within the text range, we ignore the text to the left\n // of the ignore point (including the character right after the ignore point).\n if (maxIgnore >= 0) {\n const cut = maxIgnore + 1 - textFrom\n text = text.slice(cut)\n textFrom += cut\n }\n\n if (textFrom >= textTo || !text) {\n return\n }\n\n for (const rule of rules) {\n if (!rule.canMatch({ state })) {\n continue\n }\n\n rule.regex.lastIndex = 0\n const match = rule.regex.exec(text)\n if (!match) {\n continue\n }\n\n const matchTo = textTo\n const matchFrom = textFrom + match.index\n\n return { rule, match, from: matchFrom, to: matchTo }\n }\n}\n\nfunction mapMatching(matching: PredictionPluginMatching, mapping: Mapping): PredictionPluginMatching {\n return {\n rule: matching.rule,\n match: matching.match,\n from: mapping.map(matching.from),\n to: mapping.map(matching.to, -1),\n }\n}\n","import {\n defineFacet,\n defineFacetPayload,\n pluginFacet,\n type Extension,\n type PluginPayload,\n} from '@prosekit/core'\n\nimport { createAutocompletePlugin } from './autocomplete-plugin'\nimport type { AutocompleteRule } from './autocomplete-rule'\n\n/**\n * Defines an autocomplete extension that executes logic when the text before\n * the cursor matches the given regular expression.\n *\n * When a match is found, an inline decoration is applied to the matched text\n * with the class `prosekit-autocomplete-match` and a `data-autocomplete-match-text`\n * attribute containing the full matched string.\n */\nexport function defineAutocomplete(rule: AutocompleteRule): Extension {\n return defineFacetPayload(autocompleteFacet, [rule])\n}\n\nconst autocompleteFacet = defineFacet<AutocompleteRule, PluginPayload>({\n reduce: () => {\n let rules: AutocompleteRule[] = []\n const getRules = () => rules\n const plugin = createAutocompletePlugin({ getRules })\n\n return function reducer(inputs) {\n rules = inputs\n return plugin\n }\n },\n parent: pluginFacet,\n singleton: true,\n})\n","import type { EditorState } from '@prosekit/pm/state'\n\nimport { defaultCanMatch } from './autocomplete-helpers'\n\n/**\n * Options for the {@link MatchHandler} callback.\n */\nexport interface MatchHandlerOptions {\n /**\n * The editor state.\n */\n state: EditorState\n\n /**\n * The result of `RegExp.exec`.\n */\n match: RegExpExecArray\n\n /**\n * The start position of the matched text.\n */\n from: number\n\n /**\n * The end position of the matched text.\n */\n to: number\n\n /**\n * Call this function to ignore the match. You probably want to call this\n * function when the user presses the `Escape` key.\n */\n ignoreMatch: () => void\n\n /**\n * Call this function to delete the matched text. For example, in a slash\n * menu, you might want to delete the matched text first then do something\n * else when the user presses the `Enter` key.\n */\n deleteMatch: () => void\n}\n\n/**\n * A callback that is called when the rule starts to match, and also on\n * subsequent updates while the rule continues to match.\n */\nexport type MatchHandler = (options: MatchHandlerOptions) => void\n\n/**\n * Options for the {@link CanMatchPredicate} callback.\n */\nexport interface CanMatchOptions {\n /**\n * The editor state.\n */\n state: EditorState\n}\n\n/**\n * A predicate to determine if the rule can be applied in the current editor state.\n */\nexport type CanMatchPredicate = (options: CanMatchOptions) => boolean\n\n/**\n * Options for creating an {@link AutocompleteRule}\n */\nexport interface AutocompleteRuleOptions {\n /**\n * The regular expression to match against the text before the cursor. The\n * last match before the cursor is used.\n *\n * For a slash menu, you might use `/(?<!\\S)\\/(\\S.*)?$/u`.\n * For a mention, you might use `/@\\w*$/`\n */\n regex: RegExp\n\n /**\n * A callback that is called when the rule starts to match, and also on\n * subsequent updates while the rule continues to match.\n */\n onEnter: MatchHandler\n\n /**\n * A callback that is called when the rule stops matching.\n */\n onLeave?: VoidFunction\n\n /**\n * A predicate to determine if the rule can be applied in the current editor\n * state. If not provided, it defaults to only allowing matches that are not\n * inside a code block or code mark.\n */\n canMatch?: CanMatchPredicate\n}\n\n/**\n * An autocomplete rule that can be used to create an autocomplete extension.\n *\n * @public\n */\nexport class AutocompleteRule {\n /** @internal */\n readonly regex: RegExp\n /** @internal */\n readonly onMatch: MatchHandler\n /** @internal */\n readonly onLeave?: VoidFunction\n /** @internal */\n readonly canMatch: (options: { state: EditorState }) => boolean\n\n constructor(options: AutocompleteRuleOptions) {\n this.regex = options.regex\n this.onMatch = options.onEnter\n this.onLeave = options.onLeave\n this.canMatch = options.canMatch ?? defaultCanMatch\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prosekit-extensions-blockquote.js","names":[],"sources":["../src/blockquote/blockquote-commands.ts","../src/blockquote/blockquote-input-rule.ts","../src/blockquote/blockquote-keymap.ts","../src/blockquote/blockquote-spec.ts","../src/blockquote/blockquote.ts"],"sourcesContent":["import {\n defineCommands,\n insertNode,\n toggleWrap,\n wrap,\n type Extension,\n} from '@prosekit/core'\n\nexport type BlockquoteCommandsExtension = Extension<{\n Commands: {\n setBlockquote: []\n insertBlockquote: []\n toggleBlockquote: []\n }\n}>\n\n/**\n * @internal\n */\nexport function defineBlockquoteCommands(): BlockquoteCommandsExtension {\n return defineCommands({\n setBlockquote: () => {\n return wrap({ type: 'blockquote' })\n },\n insertBlockquote: () => {\n return insertNode({ type: 'blockquote' })\n },\n toggleBlockquote: () => {\n return toggleWrap({ type: 'blockquote' })\n },\n })\n}\n","import type { PlainExtension } from '@prosekit/core'\n\nimport { defineWrappingInputRule } from '../input-rule'\n\n/**\n * Wraps the text block in a blockquote when `>` is typed at the start of a new\n * line followed by a space.\n */\nexport function defineBlockquoteInputRule(): PlainExtension {\n return defineWrappingInputRule({\n regex: /^>\\s/,\n type: 'blockquote',\n })\n}\n","import {\n defineKeymap,\n isAtBlockStart,\n toggleWrap,\n type PlainExtension,\n} from '@prosekit/core'\nimport { joinBackward } from '@prosekit/pm/commands'\nimport type { Command } from '@prosekit/pm/state'\n\nfunction toggleBlockquoteKeybinding(): Command {\n return toggleWrap({ type: 'blockquote' })\n}\n\nfunction backspaceUnsetBlockquote(): Command {\n return (state, dispatch, view) => {\n const $pos = isAtBlockStart(state, view)\n if ($pos?.node(-1).type.name === 'blockquote') {\n return joinBackward(state, dispatch, view)\n }\n return false\n }\n}\n/**\n * @internal\n */\nexport function defineBlockquoteKeymap(): PlainExtension {\n return defineKeymap({\n '
|
|
1
|
+
{"version":3,"file":"prosekit-extensions-blockquote.js","names":[],"sources":["../src/blockquote/blockquote-commands.ts","../src/blockquote/blockquote-input-rule.ts","../src/blockquote/blockquote-keymap.ts","../src/blockquote/blockquote-spec.ts","../src/blockquote/blockquote.ts"],"sourcesContent":["import {\n defineCommands,\n insertNode,\n toggleWrap,\n wrap,\n type Extension,\n} from '@prosekit/core'\n\nexport type BlockquoteCommandsExtension = Extension<{\n Commands: {\n setBlockquote: []\n insertBlockquote: []\n toggleBlockquote: []\n }\n}>\n\n/**\n * @internal\n */\nexport function defineBlockquoteCommands(): BlockquoteCommandsExtension {\n return defineCommands({\n setBlockquote: () => {\n return wrap({ type: 'blockquote' })\n },\n insertBlockquote: () => {\n return insertNode({ type: 'blockquote' })\n },\n toggleBlockquote: () => {\n return toggleWrap({ type: 'blockquote' })\n },\n })\n}\n","import type { PlainExtension } from '@prosekit/core'\n\nimport { defineWrappingInputRule } from '../input-rule'\n\n/**\n * Wraps the text block in a blockquote when `>` is typed at the start of a new\n * line followed by a space.\n */\nexport function defineBlockquoteInputRule(): PlainExtension {\n return defineWrappingInputRule({\n regex: /^>\\s/,\n type: 'blockquote',\n })\n}\n","import {\n defineKeymap,\n isAtBlockStart,\n toggleWrap,\n type PlainExtension,\n} from '@prosekit/core'\nimport { joinBackward } from '@prosekit/pm/commands'\nimport type { Command } from '@prosekit/pm/state'\n\nfunction toggleBlockquoteKeybinding(): Command {\n return toggleWrap({ type: 'blockquote' })\n}\n\nfunction backspaceUnsetBlockquote(): Command {\n return (state, dispatch, view) => {\n const $pos = isAtBlockStart(state, view)\n if ($pos?.node(-1).type.name === 'blockquote') {\n return joinBackward(state, dispatch, view)\n }\n return false\n }\n}\n/**\n * @internal\n */\nexport function defineBlockquoteKeymap(): PlainExtension {\n return defineKeymap({\n 'Mod-B': toggleBlockquoteKeybinding(),\n 'Backspace': backspaceUnsetBlockquote(),\n })\n}\n","import {\n defineNodeSpec,\n type Extension,\n} from '@prosekit/core'\nimport type { Attrs } from '@prosekit/pm/model'\n\nexport type BlockquoteSpecExtension = Extension<{\n Nodes: {\n blockquote: Attrs\n }\n}>\n\nexport function defineBlockquoteSpec(): BlockquoteSpecExtension {\n return defineNodeSpec({\n name: 'blockquote',\n content: 'block+',\n group: 'block',\n defining: true,\n parseDOM: [{ tag: 'blockquote' }],\n toDOM() {\n return ['blockquote', 0]\n },\n })\n}\n","import {\n union,\n type Union,\n} from '@prosekit/core'\n\nimport {\n defineBlockquoteCommands,\n type BlockquoteCommandsExtension,\n} from './blockquote-commands'\nimport { defineBlockquoteInputRule } from './blockquote-input-rule'\nimport { defineBlockquoteKeymap } from './blockquote-keymap'\nimport {\n defineBlockquoteSpec,\n type BlockquoteSpecExtension,\n} from './blockquote-spec'\n\n/**\n * @internal\n */\nexport type BlockquoteExtension = Union<\n [BlockquoteSpecExtension, BlockquoteCommandsExtension]\n>\n\n/**\n * @public\n */\nexport function defineBlockquote(): BlockquoteExtension {\n return union(\n defineBlockquoteSpec(),\n defineBlockquoteInputRule(),\n defineBlockquoteCommands(),\n defineBlockquoteKeymap(),\n )\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,2BAAwD;AACtE,QAAO,eAAe;EACpB,qBAAqB;AACnB,UAAO,KAAK,EAAE,MAAM,cAAc,CAAC;;EAErC,wBAAwB;AACtB,UAAO,WAAW,EAAE,MAAM,cAAc,CAAC;;EAE3C,wBAAwB;AACtB,UAAO,WAAW,EAAE,MAAM,cAAc,CAAC;;EAE5C,CAAC;;;;;;;;;ACtBJ,SAAgB,4BAA4C;AAC1D,QAAO,wBAAwB;EAC7B,OAAO;EACP,MAAM;EACP,CAAC;;;;;ACHJ,SAAS,6BAAsC;AAC7C,QAAO,WAAW,EAAE,MAAM,cAAc,CAAC;;AAG3C,SAAS,2BAAoC;AAC3C,SAAQ,OAAO,UAAU,SAAS;AAEhC,MADa,eAAe,OAAO,KAAK,EAC9B,KAAK,GAAG,CAAC,KAAK,SAAS,aAC/B,QAAO,aAAa,OAAO,UAAU,KAAK;AAE5C,SAAO;;;;;;AAMX,SAAgB,yBAAyC;AACvD,QAAO,aAAa;EAClB,SAAS,4BAA4B;EACrC,aAAa,0BAA0B;EACxC,CAAC;;;;;ACjBJ,SAAgB,uBAAgD;AAC9D,QAAO,eAAe;EACpB,MAAM;EACN,SAAS;EACT,OAAO;EACP,UAAU;EACV,UAAU,CAAC,EAAE,KAAK,cAAc,CAAC;EACjC,QAAQ;AACN,UAAO,CAAC,cAAc,EAAE;;EAE3B,CAAC;;;;;;;;ACIJ,SAAgB,mBAAwC;AACtD,QAAO,MACL,sBAAsB,EACtB,2BAA2B,EAC3B,0BAA0B,EAC1B,wBAAwB,CACzB"}
|
|
@@ -235,7 +235,7 @@ function createLazyParser(highlighterOptions) {
|
|
|
235
235
|
*
|
|
236
236
|
* @public
|
|
237
237
|
*/
|
|
238
|
-
function defineCodeBlockShiki({ themes = ["one-dark-pro"], langs = ["text"]
|
|
238
|
+
function defineCodeBlockShiki({ themes = ["one-dark-pro"], langs = ["text"], ...rest } = {}) {
|
|
239
239
|
return defineCodeBlockHighlight({ parser: createLazyParser({
|
|
240
240
|
themes,
|
|
241
241
|
langs,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prosekit-extensions-code-block.js","names":["existCodeBlock: Command","loaded: ((options: HighlighterOptions) => HighlighterResult) | undefined","createOrGetHighlighter","parser: Parser | undefined"],"sources":["../src/code-block/code-block-commands.ts","../src/code-block/code-block-input-rule.ts","../src/code-block/code-block-keymap.ts","../src/code-block/code-block-spec.ts","../src/code-block/code-block.ts","../src/code-block/code-block-highlight.ts","../src/code-block/shiki-highlighter.ts","../src/code-block/shiki-parser.ts","../src/code-block/code-block-shiki.ts"],"sourcesContent":["import {\n defineCommands,\n insertNode,\n setBlockType,\n setNodeAttrs,\n toggleNode,\n type Extension,\n} from '@prosekit/core'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * @internal\n */\nexport type CodeBlockCommandsExtension = Extension<{\n Commands: {\n setCodeBlock: [attrs?: CodeBlockAttrs]\n insertCodeBlock: [attrs?: CodeBlockAttrs]\n toggleCodeBlock: [attrs?: CodeBlockAttrs]\n setCodeBlockAttrs: [attrs: CodeBlockAttrs]\n }\n}>\n\n/**\n * Adds commands for working with `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockCommands(): CodeBlockCommandsExtension {\n return defineCommands({\n setCodeBlock: (attrs?: CodeBlockAttrs) => {\n return setBlockType({ type: 'codeBlock', attrs })\n },\n insertCodeBlock: (attrs?: CodeBlockAttrs) => {\n return insertNode({ type: 'codeBlock', attrs })\n },\n toggleCodeBlock: (attrs?: CodeBlockAttrs) => {\n return toggleNode({ type: 'codeBlock', attrs })\n },\n setCodeBlockAttrs: (attrs: CodeBlockAttrs) => {\n return setNodeAttrs({ type: 'codeBlock', attrs })\n },\n })\n}\n","import type { PlainExtension } from '@prosekit/core'\n\nimport { defineTextBlockEnterRule } from '../enter-rule'\nimport { defineTextBlockInputRule } from '../input-rule'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * Adds input rules for `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockInputRule(): PlainExtension {\n return defineTextBlockInputRule({\n regex: /^```(\\S*)\\s$/,\n type: 'codeBlock',\n attrs: getAttrs,\n })\n}\n\n/**\n * Adds enter rules for `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockEnterRule(): PlainExtension {\n return defineTextBlockEnterRule({\n regex: /^```(\\S*)$/,\n type: 'codeBlock',\n attrs: getAttrs,\n })\n}\n\nfunction getAttrs(match: RegExpMatchArray): CodeBlockAttrs {\n return { language: match[1] || '' }\n}\n","import {\n defaultBlockAt,\n defineKeymap,\n type PlainExtension,\n} from '@prosekit/core'\nimport {\n TextSelection,\n type Command,\n} from '@prosekit/pm/state'\n\n/**\n * Defines the keymap for code blocks.\n */\nexport function defineCodeBlockKeymap(): PlainExtension {\n return defineKeymap({\n Enter: existCodeBlock,\n })\n}\n\n/**\n * Exit a code block and insert a default block below if the cursor is at the\n * end of the code block and the code block is ended with two new lines.\n */\nconst existCodeBlock: Command = (state, dispatch) => {\n if (!state.selection.empty) {\n return false\n }\n\n const { $head } = state.selection\n const parent = $head.parent\n if (\n parent.isTextblock\n && parent.type.spec.code\n && $head.parentOffset === parent.content.size\n && parent.textContent.endsWith('\\n\\n')\n ) {\n const grandParent = $head.node(-1)\n const insertIndex = $head.indexAfter(-1)\n const type = defaultBlockAt(grandParent.contentMatchAt(insertIndex))\n\n if (!type || !grandParent.canReplaceWith(insertIndex, insertIndex, type)) {\n return false\n }\n\n if (dispatch) {\n const { tr } = state\n tr.delete($head.pos - 2, $head.pos)\n const pos = tr.selection.$head.after()\n const node = type.createAndFill()\n if (node) {\n tr.replaceWith(pos, pos, node)\n tr.setSelection(TextSelection.near(tr.doc.resolve(pos), 1))\n dispatch(tr.scrollIntoView())\n }\n }\n\n return true\n }\n\n return false\n}\n","import {\n defineNodeSpec,\n type Extension,\n} from '@prosekit/core'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * @internal\n */\nexport type CodeBlockSpecExtension = Extension<{\n Nodes: {\n codeBlock: CodeBlockAttrs\n }\n}>\n\n/**\n * Defines the `codeBlock` node spec.\n *\n * @public\n */\nexport function defineCodeBlockSpec(): CodeBlockSpecExtension {\n return defineNodeSpec({\n name: 'codeBlock',\n content: 'text*',\n group: 'block',\n code: true,\n defining: true,\n marks: '',\n attrs: { language: { default: '', validate: 'string' } },\n parseDOM: [\n {\n tag: 'pre',\n preserveWhitespace: 'full',\n getAttrs: (node): CodeBlockAttrs => {\n const language = extractLanguageFromElement(node)\n || extractLanguageFromElement(node.querySelector('code'))\n return { language }\n },\n },\n ],\n toDOM(node) {\n const { language } = node.attrs as CodeBlockAttrs\n return [\n 'pre',\n { 'data-language': language || undefined },\n // `class: language-${language}` is used by remark-rehype to highlight the code block\n ['code', { class: language ? `language-${language}` : undefined }, 0],\n ]\n },\n })\n}\n\nfunction extractLanguageFromElement(element: HTMLElement | null | undefined): string {\n if (!element) {\n return ''\n }\n\n const attr = element.getAttribute('data-language')\n if (attr) {\n return attr\n }\n\n const className = element.className\n const match = className.match(/language-(\\w+)/)\n if (match) {\n return match[1]\n }\n\n return ''\n}\n","import {\n union,\n type Union,\n} from '@prosekit/core'\n\nimport {\n defineCodeBlockCommands,\n type CodeBlockCommandsExtension,\n} from './code-block-commands'\nimport {\n defineCodeBlockEnterRule,\n defineCodeBlockInputRule,\n} from './code-block-input-rule'\nimport { defineCodeBlockKeymap } from './code-block-keymap'\nimport {\n defineCodeBlockSpec,\n type CodeBlockSpecExtension,\n} from './code-block-spec'\n\n/**\n * @internal\n */\nexport type CodeBlockExtension = Union<\n [CodeBlockSpecExtension, CodeBlockCommandsExtension]\n>\n\n/**\n * Adds `codeBlock` nodes to the editor. This includes the following extensions:\n *\n * - {@link defineCodeBlockSpec}\n * - {@link defineCodeBlockInputRule}\n * - {@link defineCodeBlockEnterRule}\n * - {@link defineCodeBlockKeymap}\n * - {@link defineCodeBlockCommands}.\n *\n * @public\n */\nexport function defineCodeBlock(): CodeBlockExtension {\n return union(\n defineCodeBlockSpec(),\n defineCodeBlockInputRule(),\n defineCodeBlockEnterRule(),\n defineCodeBlockKeymap(),\n defineCodeBlockCommands(),\n )\n}\n","import {\n definePlugin,\n type Extension,\n} from '@prosekit/core'\nimport {\n createHighlightPlugin,\n type Parser,\n} from 'prosemirror-highlight'\n\n/**\n * @public\n *\n * An alias for the `Parser` type from the `prosemirror-highlight` package.\n */\nexport type HighlightParser = Parser\n\n/**\n * @public\n */\nexport type CodeBlockHighlightOptions = {\n parser: HighlightParser\n}\n\n/**\n * Adds syntax highlighting to code blocks. This function requires a `Parser`\n * instance from the `prosemirror-highlight` package. See the\n * [documentation](https://github.com/ocavue/prosemirror-highlight) for more\n * information.\n *\n * @param options\n *\n * @public\n */\nexport function defineCodeBlockHighlight({\n parser,\n}: CodeBlockHighlightOptions): Extension {\n return definePlugin(\n createHighlightPlugin({ parser }),\n )\n}\n","import type {\n HighlighterOptions,\n HighlighterResult,\n} from './shiki-highlighter-chunk'\n\nlet loaded: ((options: HighlighterOptions) => HighlighterResult) | undefined\n\nasync function load() {\n const { createOrGetHighlighter } = await import('./shiki-highlighter-chunk')\n loaded = createOrGetHighlighter\n}\n\nexport function createOrGetHighlighter(\n options: HighlighterOptions,\n): HighlighterResult {\n if (!loaded) {\n return { promise: load() }\n }\n return loaded(options)\n}\n\nexport type { HighlighterOptions }\n","import type { Parser } from 'prosemirror-highlight'\nimport { createParser } from 'prosemirror-highlight/shiki'\n\nimport type { ShikiBundledLanguage } from './shiki-bundle'\nimport {\n createOrGetHighlighter,\n type HighlighterOptions,\n} from './shiki-highlighter'\n\n/**\n * @internal\n */\nexport function createLazyParser(\n highlighterOptions: HighlighterOptions,\n): Parser {\n let parser: Parser | undefined\n\n return function lazyParser(options) {\n const language = (options.language || '') as ShikiBundledLanguage\n const { highlighter, promise } = createOrGetHighlighter({\n ...highlighterOptions,\n langs: [language],\n })\n\n if (!highlighter) {\n return promise\n }\n\n if (!parser) {\n parser = createParser(highlighter, {\n theme: highlighterOptions.themes[0],\n })\n }\n return parser(options)\n }\n}\n","import type { Extension } from '@prosekit/core'\nimport type { SpecialLanguage } from 'shiki'\n\nimport { defineCodeBlockHighlight } from './code-block-highlight'\nimport type {\n ShikiBundledLanguage,\n ShikiBundledTheme,\n} from './shiki-bundle'\nimport type { ShikiHighlighterOptions } from './shiki-highlighter-chunk'\nimport { createLazyParser } from './shiki-parser'\n\n/**\n * The options to configure the Shiki highlighter.\n *\n * @public\n */\nexport interface CodeBlockShikiOptions extends Omit<ShikiHighlighterOptions, 'themes' | 'langs' | 'engine'> {\n /**\n * A list of Shiki themes to pre-load. The first theme in the list will be\n * used to render the code block.\n *\n * @default ['one-dark-pro']\n */\n themes?: ShikiBundledTheme[]\n\n /**\n * A list of Shiki languages to pre-load.\n *\n * @default ['text']\n */\n langs?: (ShikiBundledLanguage | SpecialLanguage)[]\n\n /**\n * The RegExp engine to use. By default, the JavaScript engine is used.\n */\n engine?: ShikiHighlighterOptions['engine']\n}\n\n/**\n * Adds syntax highlighting to code blocks using the [Shiki](https://github.com/shikijs/shiki) package.\n *\n * It will set two CSS variables on the code block elements:\n *\n * - `--prosemirror-highlight`: sets text color\n * - `--prosemirror-highlight-bg`: sets background color\n *\n * @param options - The options to configure the Shiki highlighter.\n *\n * @public\n */\nexport function defineCodeBlockShiki({\n themes = ['one-dark-pro'],\n langs = ['text'],\n ...rest\n}: CodeBlockShikiOptions = {}): Extension {\n const parser = createLazyParser({ themes, langs, ...rest })\n return defineCodeBlockHighlight({ parser })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prosekit-extensions-code-block.js","names":["existCodeBlock: Command","loaded: ((options: HighlighterOptions) => HighlighterResult) | undefined","createOrGetHighlighter","parser: Parser | undefined"],"sources":["../src/code-block/code-block-commands.ts","../src/code-block/code-block-input-rule.ts","../src/code-block/code-block-keymap.ts","../src/code-block/code-block-spec.ts","../src/code-block/code-block.ts","../src/code-block/code-block-highlight.ts","../src/code-block/shiki-highlighter.ts","../src/code-block/shiki-parser.ts","../src/code-block/code-block-shiki.ts"],"sourcesContent":["import {\n defineCommands,\n insertNode,\n setBlockType,\n setNodeAttrs,\n toggleNode,\n type Extension,\n} from '@prosekit/core'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * @internal\n */\nexport type CodeBlockCommandsExtension = Extension<{\n Commands: {\n setCodeBlock: [attrs?: CodeBlockAttrs]\n insertCodeBlock: [attrs?: CodeBlockAttrs]\n toggleCodeBlock: [attrs?: CodeBlockAttrs]\n setCodeBlockAttrs: [attrs: CodeBlockAttrs]\n }\n}>\n\n/**\n * Adds commands for working with `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockCommands(): CodeBlockCommandsExtension {\n return defineCommands({\n setCodeBlock: (attrs?: CodeBlockAttrs) => {\n return setBlockType({ type: 'codeBlock', attrs })\n },\n insertCodeBlock: (attrs?: CodeBlockAttrs) => {\n return insertNode({ type: 'codeBlock', attrs })\n },\n toggleCodeBlock: (attrs?: CodeBlockAttrs) => {\n return toggleNode({ type: 'codeBlock', attrs })\n },\n setCodeBlockAttrs: (attrs: CodeBlockAttrs) => {\n return setNodeAttrs({ type: 'codeBlock', attrs })\n },\n })\n}\n","import type { PlainExtension } from '@prosekit/core'\n\nimport { defineTextBlockEnterRule } from '../enter-rule'\nimport { defineTextBlockInputRule } from '../input-rule'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * Adds input rules for `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockInputRule(): PlainExtension {\n return defineTextBlockInputRule({\n regex: /^```(\\S*)\\s$/,\n type: 'codeBlock',\n attrs: getAttrs,\n })\n}\n\n/**\n * Adds enter rules for `codeBlock` nodes.\n *\n * @public\n */\nexport function defineCodeBlockEnterRule(): PlainExtension {\n return defineTextBlockEnterRule({\n regex: /^```(\\S*)$/,\n type: 'codeBlock',\n attrs: getAttrs,\n })\n}\n\nfunction getAttrs(match: RegExpMatchArray): CodeBlockAttrs {\n return { language: match[1] || '' }\n}\n","import {\n defaultBlockAt,\n defineKeymap,\n type PlainExtension,\n} from '@prosekit/core'\nimport {\n TextSelection,\n type Command,\n} from '@prosekit/pm/state'\n\n/**\n * Defines the keymap for code blocks.\n */\nexport function defineCodeBlockKeymap(): PlainExtension {\n return defineKeymap({\n Enter: existCodeBlock,\n })\n}\n\n/**\n * Exit a code block and insert a default block below if the cursor is at the\n * end of the code block and the code block is ended with two new lines.\n */\nconst existCodeBlock: Command = (state, dispatch) => {\n if (!state.selection.empty) {\n return false\n }\n\n const { $head } = state.selection\n const parent = $head.parent\n if (\n parent.isTextblock\n && parent.type.spec.code\n && $head.parentOffset === parent.content.size\n && parent.textContent.endsWith('\\n\\n')\n ) {\n const grandParent = $head.node(-1)\n const insertIndex = $head.indexAfter(-1)\n const type = defaultBlockAt(grandParent.contentMatchAt(insertIndex))\n\n if (!type || !grandParent.canReplaceWith(insertIndex, insertIndex, type)) {\n return false\n }\n\n if (dispatch) {\n const { tr } = state\n tr.delete($head.pos - 2, $head.pos)\n const pos = tr.selection.$head.after()\n const node = type.createAndFill()\n if (node) {\n tr.replaceWith(pos, pos, node)\n tr.setSelection(TextSelection.near(tr.doc.resolve(pos), 1))\n dispatch(tr.scrollIntoView())\n }\n }\n\n return true\n }\n\n return false\n}\n","import {\n defineNodeSpec,\n type Extension,\n} from '@prosekit/core'\n\nimport type { CodeBlockAttrs } from './code-block-types'\n\n/**\n * @internal\n */\nexport type CodeBlockSpecExtension = Extension<{\n Nodes: {\n codeBlock: CodeBlockAttrs\n }\n}>\n\n/**\n * Defines the `codeBlock` node spec.\n *\n * @public\n */\nexport function defineCodeBlockSpec(): CodeBlockSpecExtension {\n return defineNodeSpec({\n name: 'codeBlock',\n content: 'text*',\n group: 'block',\n code: true,\n defining: true,\n marks: '',\n attrs: { language: { default: '', validate: 'string' } },\n parseDOM: [\n {\n tag: 'pre',\n preserveWhitespace: 'full',\n getAttrs: (node): CodeBlockAttrs => {\n const language = extractLanguageFromElement(node)\n || extractLanguageFromElement(node.querySelector('code'))\n return { language }\n },\n },\n ],\n toDOM(node) {\n const { language } = node.attrs as CodeBlockAttrs\n return [\n 'pre',\n { 'data-language': language || undefined },\n // `class: language-${language}` is used by remark-rehype to highlight the code block\n ['code', { class: language ? `language-${language}` : undefined }, 0],\n ]\n },\n })\n}\n\nfunction extractLanguageFromElement(element: HTMLElement | null | undefined): string {\n if (!element) {\n return ''\n }\n\n const attr = element.getAttribute('data-language')\n if (attr) {\n return attr\n }\n\n const className = element.className\n const match = className.match(/language-(\\w+)/)\n if (match) {\n return match[1]\n }\n\n return ''\n}\n","import {\n union,\n type Union,\n} from '@prosekit/core'\n\nimport {\n defineCodeBlockCommands,\n type CodeBlockCommandsExtension,\n} from './code-block-commands'\nimport {\n defineCodeBlockEnterRule,\n defineCodeBlockInputRule,\n} from './code-block-input-rule'\nimport { defineCodeBlockKeymap } from './code-block-keymap'\nimport {\n defineCodeBlockSpec,\n type CodeBlockSpecExtension,\n} from './code-block-spec'\n\n/**\n * @internal\n */\nexport type CodeBlockExtension = Union<\n [CodeBlockSpecExtension, CodeBlockCommandsExtension]\n>\n\n/**\n * Adds `codeBlock` nodes to the editor. This includes the following extensions:\n *\n * - {@link defineCodeBlockSpec}\n * - {@link defineCodeBlockInputRule}\n * - {@link defineCodeBlockEnterRule}\n * - {@link defineCodeBlockKeymap}\n * - {@link defineCodeBlockCommands}.\n *\n * @public\n */\nexport function defineCodeBlock(): CodeBlockExtension {\n return union(\n defineCodeBlockSpec(),\n defineCodeBlockInputRule(),\n defineCodeBlockEnterRule(),\n defineCodeBlockKeymap(),\n defineCodeBlockCommands(),\n )\n}\n","import {\n definePlugin,\n type Extension,\n} from '@prosekit/core'\nimport {\n createHighlightPlugin,\n type Parser,\n} from 'prosemirror-highlight'\n\n/**\n * @public\n *\n * An alias for the `Parser` type from the `prosemirror-highlight` package.\n */\nexport type HighlightParser = Parser\n\n/**\n * @public\n */\nexport type CodeBlockHighlightOptions = {\n parser: HighlightParser\n}\n\n/**\n * Adds syntax highlighting to code blocks. This function requires a `Parser`\n * instance from the `prosemirror-highlight` package. See the\n * [documentation](https://github.com/ocavue/prosemirror-highlight) for more\n * information.\n *\n * @param options\n *\n * @public\n */\nexport function defineCodeBlockHighlight({\n parser,\n}: CodeBlockHighlightOptions): Extension {\n return definePlugin(\n createHighlightPlugin({ parser }),\n )\n}\n","import type {\n HighlighterOptions,\n HighlighterResult,\n} from './shiki-highlighter-chunk'\n\nlet loaded: ((options: HighlighterOptions) => HighlighterResult) | undefined\n\nasync function load() {\n const { createOrGetHighlighter } = await import('./shiki-highlighter-chunk')\n loaded = createOrGetHighlighter\n}\n\nexport function createOrGetHighlighter(\n options: HighlighterOptions,\n): HighlighterResult {\n if (!loaded) {\n return { promise: load() }\n }\n return loaded(options)\n}\n\nexport type { HighlighterOptions }\n","import type { Parser } from 'prosemirror-highlight'\nimport { createParser } from 'prosemirror-highlight/shiki'\n\nimport type { ShikiBundledLanguage } from './shiki-bundle'\nimport {\n createOrGetHighlighter,\n type HighlighterOptions,\n} from './shiki-highlighter'\n\n/**\n * @internal\n */\nexport function createLazyParser(\n highlighterOptions: HighlighterOptions,\n): Parser {\n let parser: Parser | undefined\n\n return function lazyParser(options) {\n const language = (options.language || '') as ShikiBundledLanguage\n const { highlighter, promise } = createOrGetHighlighter({\n ...highlighterOptions,\n langs: [language],\n })\n\n if (!highlighter) {\n return promise\n }\n\n if (!parser) {\n parser = createParser(highlighter, {\n theme: highlighterOptions.themes[0],\n })\n }\n return parser(options)\n }\n}\n","import type { Extension } from '@prosekit/core'\nimport type { SpecialLanguage } from 'shiki'\n\nimport { defineCodeBlockHighlight } from './code-block-highlight'\nimport type {\n ShikiBundledLanguage,\n ShikiBundledTheme,\n} from './shiki-bundle'\nimport type { ShikiHighlighterOptions } from './shiki-highlighter-chunk'\nimport { createLazyParser } from './shiki-parser'\n\n/**\n * The options to configure the Shiki highlighter.\n *\n * @public\n */\nexport interface CodeBlockShikiOptions extends Omit<ShikiHighlighterOptions, 'themes' | 'langs' | 'engine'> {\n /**\n * A list of Shiki themes to pre-load. The first theme in the list will be\n * used to render the code block.\n *\n * @default ['one-dark-pro']\n */\n themes?: ShikiBundledTheme[]\n\n /**\n * A list of Shiki languages to pre-load.\n *\n * @default ['text']\n */\n langs?: (ShikiBundledLanguage | SpecialLanguage)[]\n\n /**\n * The RegExp engine to use. By default, the JavaScript engine is used.\n */\n engine?: ShikiHighlighterOptions['engine']\n}\n\n/**\n * Adds syntax highlighting to code blocks using the [Shiki](https://github.com/shikijs/shiki) package.\n *\n * It will set two CSS variables on the code block elements:\n *\n * - `--prosemirror-highlight`: sets text color\n * - `--prosemirror-highlight-bg`: sets background color\n *\n * @param options - The options to configure the Shiki highlighter.\n *\n * @public\n */\nexport function defineCodeBlockShiki({\n themes = ['one-dark-pro'],\n langs = ['text'],\n ...rest\n}: CodeBlockShikiOptions = {}): Extension {\n const parser = createLazyParser({ themes, langs, ...rest })\n return defineCodeBlockHighlight({ parser })\n}\n"],"mappings":"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"}
|
|
@@ -99,7 +99,7 @@ function defineCommitDecoration(commit) {
|
|
|
99
99
|
* Define an extension to display the changes from the given commit in the editor.
|
|
100
100
|
*/
|
|
101
101
|
function defineCommitViewer(commit) {
|
|
102
|
-
return union(defineDefaultState({
|
|
102
|
+
return union(defineDefaultState({ defaultContent: commit.doc }), defineCommitDecoration(commit));
|
|
103
103
|
}
|
|
104
104
|
var CommitRecorder = class {
|
|
105
105
|
constructor() {
|