@prosekit/extensions 0.0.0-next-20240421132240 → 0.0.0-next-20240427133255

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.
@@ -16,6 +16,7 @@ import { IndentListOptions } from 'prosemirror-flat-list';
16
16
  import { InputRule } from '@prosekit/pm/inputrules';
17
17
  import { ListAttributes } from 'prosemirror-flat-list';
18
18
  import { ListDOMSerializer } from 'prosemirror-flat-list';
19
+ import { MarkType } from '@prosekit/pm/model';
19
20
  import { NodeRange } from 'prosemirror-model';
20
21
  import { NodeType } from '@prosekit/pm/model';
21
22
  import { Options } from 'tsup';
@@ -23,6 +24,8 @@ import { Parser } from 'prosemirror-highlight';
23
24
  import { Plugin as Plugin_2 } from '@prosekit/pm/state';
24
25
  import { PluginKey } from '@prosekit/pm/state';
25
26
  import { ProseMirrorNode } from '@prosekit/pm/model';
27
+ import type { ResolvedPos } from '@prosekit/pm/model';
28
+ import type { Selection as Selection_2 } from '@prosekit/pm/state';
26
29
  import type { SpecialLanguage } from 'shiki';
27
30
  import { ToggleCollapsedOptions } from 'prosemirror-flat-list';
28
31
  import { Transaction } from '@prosekit/pm/state';
@@ -467,6 +470,15 @@ export declare function defineListSpec(): Extension<{
467
470
  NODES: "list";
468
471
  }>;
469
472
 
473
+ /**
474
+ * A mark rule is something that can automatically apply marks to text if it matches a certain pattern, and remove them if it doesn't match anymore.
475
+ *
476
+ * For every transaction that changes the document, the mark rule will be applied to the changed text.
477
+ *
478
+ *
479
+ */
480
+ export declare function defineMarkRule(options: MarkRuleOptions): Extension<ExtensionTyping<string, string, CommandArgs>>;
481
+
470
482
  /**
471
483
  * @public
472
484
  */
@@ -758,10 +770,20 @@ export declare type EnterRuleOptions = {
758
770
  */
759
771
  export declare const exitTable: Command;
760
772
 
773
+ /**
774
+ * @internal
775
+ */
776
+ export declare function findChangedTextRanges(selection: Selection_2): Array<[from: number, to: number]>;
777
+
778
+ /** @internal */
779
+ export declare function getAffectedRange(transactions: readonly Transaction[], oldState: EditorState, newState: EditorState): readonly [number, number];
780
+
761
781
  export { getHighlighter }
762
782
 
763
783
  export declare function getPluginState(state: EditorState): PredictionPluginState | undefined;
764
784
 
785
+ export declare function getSpanTextRanges($from: ResolvedPos, $to: ResolvedPos): [number, number][];
786
+
765
787
  export declare function getTrMeta(tr: Transaction): PredictionPluginState;
766
788
 
767
789
  export declare interface HeadingAttrs {
@@ -812,6 +834,23 @@ export declare interface LinkAttrs {
812
834
 
813
835
  export { ListDOMSerializer }
814
836
 
837
+ export declare interface MarkRuleOptions {
838
+ /**
839
+ * The regular expression to match against. It should have a `g` flag to match
840
+ * all instances of the mark.
841
+ */
842
+ regex: RegExp;
843
+ /**
844
+ * The mark type to apply to the matched text.
845
+ */
846
+ type: string | MarkType;
847
+ /**
848
+ * Attributes to set on the mark. If a function is provided, it will be called
849
+ * with the matched result from the regular expression.
850
+ */
851
+ attrs?: Attrs | null | ((match: RegExpMatchArray) => Attrs | null);
852
+ }
853
+
815
854
  declare type MatchHandler = (options: {
816
855
  state: EditorState;
817
856
  match: RegExpExecArray;
@@ -1,9 +1,9 @@
1
- import {
2
- defineTextBlockInputRule
3
- } from "./chunk-DYFRBXUX.js";
4
1
  import {
5
2
  defineTextBlockEnterRule
6
3
  } from "./chunk-ASTUC4KT.js";
4
+ import {
5
+ defineTextBlockInputRule
6
+ } from "./chunk-DYFRBXUX.js";
7
7
 
8
8
  // src/code-block/code-block.ts
9
9
  import { union } from "@prosekit/core";
@@ -1,9 +1,9 @@
1
- import {
2
- defineInputRule
3
- } from "./chunk-DYFRBXUX.js";
4
1
  import {
5
2
  defineEnterRule
6
3
  } from "./chunk-ASTUC4KT.js";
4
+ import {
5
+ defineInputRule
6
+ } from "./chunk-DYFRBXUX.js";
7
7
 
8
8
  // src/link/index.ts
9
9
  import {
@@ -0,0 +1,3 @@
1
+ export { defineMarkRule } from './_tsup-dts-rollup';
2
+ export { getAffectedRange } from './_tsup-dts-rollup';
3
+ export { MarkRuleOptions } from './_tsup-dts-rollup';
@@ -0,0 +1,173 @@
1
+ // src/mark-rule/index.ts
2
+ import {
3
+ Facet,
4
+ OBJECT_REPLACEMENT_CHARACTER,
5
+ getMarkType,
6
+ pluginFacet
7
+ } from "@prosekit/core";
8
+ import "@prosekit/pm/model";
9
+ import { ProseMirrorPlugin } from "@prosekit/pm/state";
10
+
11
+ // src/mark-rule/changed-range.ts
12
+ import { isTextSelection } from "@prosekit/core";
13
+ function getSpanTextRanges($from, $to) {
14
+ const nodeRange = $from.blockRange($to);
15
+ if (!nodeRange) {
16
+ return [];
17
+ }
18
+ const stack = [];
19
+ let start = nodeRange.start;
20
+ for (let i = nodeRange.startIndex; i < nodeRange.endIndex; i++) {
21
+ const child = nodeRange.parent.child(i);
22
+ stack.push([start, child]);
23
+ start += child.nodeSize;
24
+ }
25
+ const ranges = [];
26
+ while (stack.length > 0) {
27
+ const [start2, node] = stack.pop();
28
+ if (node.type.spec.code) {
29
+ continue;
30
+ }
31
+ if (node.type.isTextblock) {
32
+ ranges.push([start2 + 1, start2 + 1 + node.content.size]);
33
+ continue;
34
+ }
35
+ node.forEach((child, offset) => {
36
+ stack.push([start2 + offset + 1, child]);
37
+ });
38
+ }
39
+ return ranges;
40
+ }
41
+
42
+ // src/mark-rule/index.ts
43
+ function defineMarkRule(options) {
44
+ return markRuleFacet.extension([options]);
45
+ }
46
+ function getAffectedRange(transactions, oldState, newState) {
47
+ let lo = oldState.selection.from;
48
+ let hi = oldState.selection.to;
49
+ for (const tr of transactions) {
50
+ for (const map of tr.mapping.maps) {
51
+ lo = map.map(lo);
52
+ hi = map.map(hi);
53
+ map.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
54
+ lo = Math.min(lo, hi, newStart);
55
+ hi = Math.max(hi, hi, newEnd);
56
+ });
57
+ }
58
+ }
59
+ lo = Math.min(lo, hi, newState.selection.from);
60
+ hi = Math.min(lo, hi, newState.selection.to);
61
+ return [lo, hi];
62
+ }
63
+ function getCheckRanges(doc, from, to) {
64
+ const $from = doc.resolve(from);
65
+ const $to = doc.resolve(to);
66
+ if ($from.sameParent($to)) {
67
+ return [[$from.start(), $to.end()]];
68
+ } else {
69
+ const nodeRange = $from.blockRange($to);
70
+ if (!nodeRange) {
71
+ return [];
72
+ }
73
+ return getSpanTextRanges($from, $to);
74
+ }
75
+ }
76
+ function getExpectedMarkings(rules, doc, from, to) {
77
+ const text = doc.textBetween(from, to, OBJECT_REPLACEMENT_CHARACTER);
78
+ const result = [];
79
+ for (const rule of rules) {
80
+ rule.regex.lastIndex = 0;
81
+ const matches = text.matchAll(rule.regex);
82
+ const markType = getMarkType(doc.type.schema, rule.type);
83
+ const getAttrs = rule.attrs;
84
+ for (const match of matches) {
85
+ const index = match.index;
86
+ if (index == null)
87
+ continue;
88
+ const attrs = getAttrs ? typeof getAttrs === "function" ? getAttrs(match) : getAttrs : null;
89
+ const mark = markType.create(attrs);
90
+ result.push([from + index, from + index + match[0].length, mark]);
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ function getReceivedMarkings(rules, doc, from, to) {
96
+ const result = [];
97
+ const schema = doc.type.schema;
98
+ const markTypes = rules.map((rule) => getMarkType(schema, rule.type));
99
+ let seen = false;
100
+ doc.nodesBetween(from, to, (node) => {
101
+ if (!node.isTextblock || seen) {
102
+ return;
103
+ }
104
+ seen = true;
105
+ node.content.forEach((child, offset) => {
106
+ for (const markType of markTypes) {
107
+ const mark = child.marks.find((mark2) => mark2.type === markType);
108
+ if (mark) {
109
+ result.push([from + offset, from + offset + child.nodeSize, mark]);
110
+ }
111
+ }
112
+ });
113
+ });
114
+ return result;
115
+ }
116
+ function markingEquals(a, b) {
117
+ return a[0] === b[0] && a[1] === b[1] && a[2].eq(b[2]);
118
+ }
119
+ function markingDiffs(a, b) {
120
+ return a.filter((x) => !b.some((y) => markingEquals(x, y)));
121
+ }
122
+ function applyMarkRules(rules, transactions, oldState, newState) {
123
+ if (transactions.length === 0 || transactions.every((tr2) => !tr2.docChanged)) {
124
+ return null;
125
+ }
126
+ const [from, to] = getAffectedRange(transactions, oldState, newState);
127
+ const ranges = getCheckRanges(newState.doc, from, to);
128
+ const toRemove = [];
129
+ const toCreate = [];
130
+ for (const [from2, to2] of ranges) {
131
+ const expected = getExpectedMarkings(rules, newState.doc, from2, to2);
132
+ const received = getReceivedMarkings(rules, newState.doc, from2, to2);
133
+ toRemove.push(...markingDiffs(received, expected));
134
+ toCreate.push(...markingDiffs(expected, received));
135
+ }
136
+ if (toCreate.length === 0 && toRemove.length === 0) {
137
+ return null;
138
+ }
139
+ const tr = newState.tr;
140
+ for (const [from2, to2, mark] of toRemove) {
141
+ tr.removeMark(from2, to2, mark);
142
+ }
143
+ for (const [from2, to2, mark] of toCreate) {
144
+ tr.addMark(from2, to2, mark);
145
+ }
146
+ return tr;
147
+ }
148
+ var markRuleFacet = Facet.define({
149
+ converter: () => {
150
+ let rules = [];
151
+ const plugin = new ProseMirrorPlugin({
152
+ appendTransaction: (transactions, oldState, newState) => {
153
+ return applyMarkRules(rules, transactions, oldState, newState);
154
+ }
155
+ });
156
+ const pluginFunc = () => [plugin];
157
+ return {
158
+ create: (inputs) => {
159
+ rules = inputs;
160
+ return pluginFunc;
161
+ },
162
+ update: (inputs) => {
163
+ rules = inputs;
164
+ return null;
165
+ }
166
+ };
167
+ },
168
+ next: pluginFacet
169
+ });
170
+ export {
171
+ defineMarkRule,
172
+ getAffectedRange
173
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prosekit/extensions",
3
3
  "type": "module",
4
- "version": "0.0.0-next-20240421132240",
4
+ "version": "0.0.0-next-20240427133255",
5
5
  "private": false,
6
6
  "author": {
7
7
  "name": "ocavue",
@@ -30,6 +30,11 @@
30
30
  "import": "./dist/prosekit-extensions.js",
31
31
  "default": "./dist/prosekit-extensions.js"
32
32
  },
33
+ "./mark-rule": {
34
+ "types": "./dist/prosekit-extensions-mark-rule.d.ts",
35
+ "import": "./dist/prosekit-extensions-mark-rule.js",
36
+ "default": "./dist/prosekit-extensions-mark-rule.js"
37
+ },
33
38
  "./autocomplete": {
34
39
  "types": "./dist/prosekit-extensions-autocomplete.d.ts",
35
40
  "import": "./dist/prosekit-extensions-autocomplete.js",
@@ -147,8 +152,8 @@
147
152
  "dist"
148
153
  ],
149
154
  "dependencies": {
150
- "@prosekit/core": "0.0.0-next-20240421132240",
151
- "@prosekit/pm": "0.0.0-next-20240421132240",
155
+ "@prosekit/core": "0.0.0-next-20240427133255",
156
+ "@prosekit/pm": "0.0.0-next-20240427133255",
152
157
  "prosemirror-dropcursor": "^1.8.1",
153
158
  "prosemirror-flat-list": "^0.5.0",
154
159
  "prosemirror-highlight": "^0.5.0",
@@ -159,7 +164,7 @@
159
164
  "@prosekit/dev": "*",
160
165
  "tsup": "^8.0.2",
161
166
  "typescript": "^5.4.5",
162
- "vitest": "^1.5.0"
167
+ "vitest": "^1.5.2"
163
168
  },
164
169
  "scripts": {
165
170
  "build:tsup": "tsup",
@@ -171,6 +176,9 @@
171
176
  ".": [
172
177
  "./dist/prosekit-extensions.d.ts"
173
178
  ],
179
+ "mark-rule": [
180
+ "./dist/prosekit-extensions-mark-rule.d.ts"
181
+ ],
174
182
  "autocomplete": [
175
183
  "./dist/prosekit-extensions-autocomplete.d.ts"
176
184
  ],