@bsky.app/tapper 0.2.2 → 0.2.4
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/CHANGELOG.md +14 -0
- package/README.md +4 -4
- package/build/facets.d.ts +5 -0
- package/build/facets.d.ts.map +1 -0
- package/build/facets.js +5 -0
- package/build/facets.js.map +1 -0
- package/build/index.d.ts +9 -9
- package/build/index.d.ts.map +1 -1
- package/build/index.js +36 -29
- package/build/index.js.map +1 -1
- package/build/types.d.ts +8 -6
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/util.d.ts +3 -3
- package/build/util.d.ts.map +1 -1
- package/build/util.js +9 -4
- package/build/util.js.map +1 -1
- package/package.json +1 -1
- package/src/facets.ts +6 -0
- package/src/index.ts +42 -31
- package/src/types.ts +6 -7
- package/src/util.ts +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bsky.app/tapper
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`de1fb73`](https://github.com/bluesky-social/toolbox/commit/de1fb73c07745fd3c5d9e55332a27ef0c113ed6a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add a `raw` field to facets to match `Node`
|
|
8
|
+
|
|
9
|
+
- [`c2a6730`](https://github.com/bluesky-social/toolbox/commit/c2a67303aca2c388c060f24187db8e41345d720c) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix selection state to allow for ranged selections
|
|
10
|
+
|
|
11
|
+
## 0.2.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`5952994`](https://github.com/bluesky-social/toolbox/commit/59529944ec895f43b30928b078edd5f3868753ae) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adjust tapper facet config
|
|
16
|
+
|
|
3
17
|
## 0.2.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ import {useTapper} from '@bsky.app/tapper'
|
|
|
23
23
|
function Composer() {
|
|
24
24
|
const {text, nodes, inputProps, focus, on} = useTapper({
|
|
25
25
|
facets: {
|
|
26
|
-
mention:
|
|
27
|
-
emoji:
|
|
26
|
+
mention: /@([a-zA-Z0-9._]+)/g,
|
|
27
|
+
emoji: /:([a-zA-Z0-9_]+):/g,
|
|
28
28
|
},
|
|
29
29
|
})
|
|
30
30
|
|
|
@@ -131,7 +131,7 @@ A common pattern: when the user types a closing `:` on an emoji shortcode
|
|
|
131
131
|
```tsx
|
|
132
132
|
const {on, activeFacet, focus} = useTapper({
|
|
133
133
|
facets: {
|
|
134
|
-
emoji:
|
|
134
|
+
emoji: /:([a-zA-Z0-9_]+):/g,
|
|
135
135
|
},
|
|
136
136
|
})
|
|
137
137
|
|
|
@@ -194,7 +194,7 @@ as committed.
|
|
|
194
194
|
|
|
195
195
|
```tsx
|
|
196
196
|
const {nodes} = useTapper({
|
|
197
|
-
facets: {mention:
|
|
197
|
+
facets: {mention: /@([a-zA-Z0-9._]+)/g},
|
|
198
198
|
initialText: 'Hello @eric',
|
|
199
199
|
})
|
|
200
200
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,QAAuB,CAAA;AAC3C,eAAO,MAAM,KAAK,QAAuB,CAAA;AACzC,eAAO,MAAM,GAAG,QACmK,CAAA;AACnL,eAAO,MAAM,GAAG,QAC0D,CAAA"}
|
package/build/facets.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const mention = /@([a-zA-Z0-9.-]+)/g;
|
|
2
|
+
export const emoji = /:([a-zA-Z0-9_]+):/g;
|
|
3
|
+
export const tag = /[##]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2\p{P}]*)?/gu;
|
|
4
|
+
export const url = /((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim;
|
|
5
|
+
//# sourceMappingURL=facets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facets.js","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,oBAAoB,CAAA;AAC3C,MAAM,CAAC,MAAM,KAAK,GAAG,oBAAoB,CAAA;AACzC,MAAM,CAAC,MAAM,GAAG,GACd,iLAAiL,CAAA;AACnL,MAAM,CAAC,MAAM,GAAG,GACd,wEAAwE,CAAA","sourcesContent":["export const mention = /@([a-zA-Z0-9.-]+)/g\nexport const emoji = /:([a-zA-Z0-9_]+):/g\nexport const tag =\n /[##]((?!\\ufe0f)[^\\s\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2]*[^\\d\\s\\p{P}\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2]+[^\\s\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2\\p{P}]*)?/gu\nexport const url =\n /((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
|
|
1
|
+
import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSelection, TapperSnapshot } from './types';
|
|
2
2
|
export * from './types';
|
|
3
|
+
export * from './facets';
|
|
3
4
|
export declare class Tapper {
|
|
4
5
|
private facetRegexes;
|
|
5
6
|
private triggers;
|
|
6
7
|
private listeners;
|
|
7
8
|
text: string;
|
|
8
|
-
|
|
9
|
+
selection: TapperSelection;
|
|
9
10
|
nodes: TapperNode[];
|
|
10
11
|
activeFacet: TapperActiveFacet | null;
|
|
11
12
|
private updating;
|
|
12
|
-
private
|
|
13
|
+
private pendingEnd;
|
|
13
14
|
private storeListeners;
|
|
14
15
|
private snapshot;
|
|
15
|
-
constructor(config
|
|
16
|
+
constructor(config?: TapperConfig);
|
|
16
17
|
subscribe: (listener: () => void) => () => boolean;
|
|
17
18
|
getSnapshot: () => TapperSnapshot;
|
|
18
19
|
on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
|
|
@@ -24,6 +25,7 @@ export declare class Tapper {
|
|
|
24
25
|
nativeEvent: {
|
|
25
26
|
selection: {
|
|
26
27
|
start: number;
|
|
28
|
+
end: number;
|
|
27
29
|
};
|
|
28
30
|
};
|
|
29
31
|
}) => void;
|
|
@@ -36,21 +38,19 @@ export declare function useTapper(config: TapperConfig): {
|
|
|
36
38
|
inputProps: {
|
|
37
39
|
ref: React.RefObject<any>;
|
|
38
40
|
value: string;
|
|
39
|
-
selection:
|
|
40
|
-
start: number;
|
|
41
|
-
end: number;
|
|
42
|
-
};
|
|
41
|
+
selection: TapperSelection;
|
|
43
42
|
onChangeText: (newText: string) => void;
|
|
44
43
|
onSelectionChange: (e: {
|
|
45
44
|
nativeEvent: {
|
|
46
45
|
selection: {
|
|
47
46
|
start: number;
|
|
47
|
+
end: number;
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
}) => void;
|
|
51
51
|
};
|
|
52
52
|
text: string;
|
|
53
|
-
|
|
53
|
+
selection: TapperSelection;
|
|
54
54
|
nodes: TapperNode[];
|
|
55
55
|
activeFacet: TapperActiveFacet | null;
|
|
56
56
|
};
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAA;AAWhB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AAExB,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAqB;IAIrC,OAAO,CAAC,SAAS,CAA0D;IAG3E,IAAI,SAAK;IACT,SAAS,EAAE,eAAe,CAAqB;IAC/C,KAAK,EAAE,UAAU,EAAE,CAAK;IACxB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAO;IAG5C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,UAAU,CAAsB;IAGxC,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,CAAC,EAAE,YAAY;IAejC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,EAAE,GAAI,CAAC,SAAS,MAAM,YAAY,EAChC,OAAO,CAAC,EACR,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,gBAOpC;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IAmEd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,GAAG;QAC1B,WAAW,EAAE;YAAC,SAAS,EAAE;gBAAC,KAAK,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAA;aAAC,CAAA;SAAC,CAAA;KACvD,UAcA;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;SA/LtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aAuMhB,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;;;gCA7GZ,MAAM;+BAyBP;YAC1B,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAC;oBAAC,GAAG,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SACvD;;;;;;EAyFF"}
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { parseNodesFromText, detectActiveFacet, deriveTriggers, nodeToFacet, compileFacetRegexes, } from './util';
|
|
3
|
+
import * as defaultFacets from './facets';
|
|
3
4
|
export * from './types';
|
|
5
|
+
export * from './facets';
|
|
4
6
|
export class Tapper {
|
|
5
7
|
facetRegexes;
|
|
6
8
|
triggers;
|
|
@@ -9,25 +11,26 @@ export class Tapper {
|
|
|
9
11
|
listeners = new Map();
|
|
10
12
|
// Public state
|
|
11
13
|
text = '';
|
|
12
|
-
|
|
14
|
+
selection = { start: 0, end: 0 };
|
|
13
15
|
nodes = [];
|
|
14
16
|
activeFacet = null;
|
|
15
17
|
// Internal tracking
|
|
16
18
|
updating = false;
|
|
17
|
-
|
|
19
|
+
pendingEnd = null;
|
|
18
20
|
// useSyncExternalStore plumbing
|
|
19
21
|
storeListeners = new Set();
|
|
20
22
|
snapshot;
|
|
21
23
|
constructor(config) {
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
+
const facets = config?.facets || defaultFacets;
|
|
25
|
+
this.facetRegexes = compileFacetRegexes(facets);
|
|
26
|
+
this.triggers = deriveTriggers(facets);
|
|
24
27
|
this.snapshot = {
|
|
25
28
|
text: this.text,
|
|
26
|
-
|
|
29
|
+
selection: this.selection,
|
|
27
30
|
nodes: this.nodes,
|
|
28
31
|
activeFacet: this.activeFacet,
|
|
29
32
|
};
|
|
30
|
-
if (config
|
|
33
|
+
if (config?.initialText) {
|
|
31
34
|
this.replaceText(config.initialText);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -52,22 +55,26 @@ export class Tapper {
|
|
|
52
55
|
notify() {
|
|
53
56
|
this.snapshot = {
|
|
54
57
|
text: this.text,
|
|
55
|
-
|
|
58
|
+
selection: this.selection,
|
|
56
59
|
nodes: this.nodes,
|
|
57
60
|
activeFacet: this.activeFacet,
|
|
58
61
|
};
|
|
59
62
|
this.storeListeners.forEach(l => l());
|
|
60
63
|
}
|
|
61
|
-
update(newText,
|
|
62
|
-
// guard against circular updates when
|
|
64
|
+
update(newText, selection) {
|
|
65
|
+
// guard against circular updates when insert() is called during an update cycle
|
|
63
66
|
if (this.updating)
|
|
64
67
|
return;
|
|
65
68
|
this.updating = true;
|
|
69
|
+
const cursor = selection.end;
|
|
70
|
+
const isRange = selection.start !== selection.end;
|
|
66
71
|
const nodes = newText === this.text
|
|
67
72
|
? this.nodes
|
|
68
73
|
: parseNodesFromText(newText, this.facetRegexes, this.nodes, cursor, this.triggers);
|
|
69
74
|
const prev = this.activeFacet;
|
|
70
|
-
const detected =
|
|
75
|
+
const detected = isRange
|
|
76
|
+
? null
|
|
77
|
+
: detectActiveFacet(nodes, newText, cursor, this.triggers);
|
|
71
78
|
// When cursor leaves a facet, commit it
|
|
72
79
|
let committedNode = null;
|
|
73
80
|
if (prev && (!detected || detected.type !== prev.type)) {
|
|
@@ -82,12 +89,12 @@ export class Tapper {
|
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
|
-
// Build activeFacet with
|
|
92
|
+
// Build activeFacet with insert() baked in
|
|
86
93
|
const active = detected
|
|
87
94
|
? { ...detected, insert: this.insert }
|
|
88
95
|
: null;
|
|
89
96
|
this.text = newText;
|
|
90
|
-
this.
|
|
97
|
+
this.selection = selection;
|
|
91
98
|
this.nodes = nodes;
|
|
92
99
|
this.activeFacet = active;
|
|
93
100
|
this.updating = false;
|
|
@@ -106,37 +113,37 @@ export class Tapper {
|
|
|
106
113
|
}
|
|
107
114
|
handleTextChange = (newText) => {
|
|
108
115
|
const diff = newText.length - this.text.length;
|
|
109
|
-
let
|
|
116
|
+
let newEnd = Math.max(this.selection.end + diff, 0);
|
|
110
117
|
// Atomic deletion: backspace into a facet from outside
|
|
111
|
-
if (diff === -1 &&
|
|
118
|
+
if (diff === -1 && newEnd < this.selection.end) {
|
|
112
119
|
for (const node of this.nodes) {
|
|
113
120
|
if (node.type === 'facet' &&
|
|
114
121
|
node.committed &&
|
|
115
|
-
this.
|
|
122
|
+
this.selection.end === node.end) {
|
|
116
123
|
const remnant = node.end - node.start - 1;
|
|
117
124
|
newText =
|
|
118
125
|
newText.slice(0, node.start) + newText.slice(node.start + remnant);
|
|
119
|
-
|
|
120
|
-
this.
|
|
126
|
+
newEnd = node.start;
|
|
127
|
+
this.pendingEnd = newEnd;
|
|
121
128
|
break;
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
|
-
this.update(newText,
|
|
132
|
+
this.update(newText, { start: newEnd, end: newEnd });
|
|
126
133
|
};
|
|
127
134
|
handleSelectionChange = (e) => {
|
|
128
|
-
const
|
|
135
|
+
const { start, end } = e.nativeEvent.selection;
|
|
129
136
|
// After atomic deletion, ignore stale cursor values from the DOM
|
|
130
|
-
if (this.
|
|
131
|
-
if (
|
|
137
|
+
if (this.pendingEnd !== null) {
|
|
138
|
+
if (end !== this.pendingEnd) {
|
|
132
139
|
return;
|
|
133
140
|
}
|
|
134
|
-
this.
|
|
141
|
+
this.pendingEnd = null;
|
|
135
142
|
}
|
|
136
|
-
if (
|
|
143
|
+
if (start === this.selection.start && end === this.selection.end)
|
|
137
144
|
return;
|
|
138
|
-
this.
|
|
139
|
-
this.update(this.text,
|
|
145
|
+
this.selection = { start, end };
|
|
146
|
+
this.update(this.text, { start, end });
|
|
140
147
|
};
|
|
141
148
|
replaceText = (text, cursor) => {
|
|
142
149
|
const prevActive = this.activeFacet;
|
|
@@ -155,7 +162,7 @@ export class Tapper {
|
|
|
155
162
|
? { ...detected, insert: this.insert }
|
|
156
163
|
: null;
|
|
157
164
|
this.text = text;
|
|
158
|
-
this.
|
|
165
|
+
this.selection = { start: pos, end: pos };
|
|
159
166
|
this.nodes = nodes;
|
|
160
167
|
this.activeFacet = active;
|
|
161
168
|
this.notify();
|
|
@@ -173,7 +180,7 @@ export class Tapper {
|
|
|
173
180
|
const newText = this.text.slice(0, this.activeFacet.range.start) +
|
|
174
181
|
replacement +
|
|
175
182
|
this.text.slice(this.activeFacet.range.end);
|
|
176
|
-
const
|
|
183
|
+
const newEnd = this.activeFacet.range.start + replacement.length;
|
|
177
184
|
if (this.activeFacet) {
|
|
178
185
|
this.activeFacet.value = value;
|
|
179
186
|
this.activeFacet.range = {
|
|
@@ -182,7 +189,7 @@ export class Tapper {
|
|
|
182
189
|
};
|
|
183
190
|
this.emit('afterInsert', this.activeFacet);
|
|
184
191
|
}
|
|
185
|
-
this.update(newText,
|
|
192
|
+
this.update(newText, { start: newEnd, end: newEnd });
|
|
186
193
|
};
|
|
187
194
|
}
|
|
188
195
|
export function useTapper(config) {
|
|
@@ -196,7 +203,7 @@ export function useTapper(config) {
|
|
|
196
203
|
inputProps: {
|
|
197
204
|
ref: inputRef,
|
|
198
205
|
value: state.text,
|
|
199
|
-
selection:
|
|
206
|
+
selection: state.selection,
|
|
200
207
|
onChangeText: store.handleTextChange,
|
|
201
208
|
onSelectionChange: store.handleSelectionChange,
|
|
202
209
|
},
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useRef, useState, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperEvents,\n TapperNode,\n TapperActiveFacet,\n TapperSnapshot,\n} from './types'\nimport {\n parseNodesFromText,\n detectActiveFacet,\n deriveTriggers,\n nodeToFacet,\n compileFacetRegexes,\n type CompiledFacetRegexes,\n} from './util'\n\nexport * from './types'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()\n\n // Public state\n text = ''\n cursor = 0\n nodes: TapperNode[] = []\n activeFacet: TapperActiveFacet | null = null\n\n // Internal tracking\n private updating = false\n private pendingCursor: number | null = null\n\n // useSyncExternalStore plumbing\n private storeListeners = new Set<() => void>()\n private snapshot: TapperSnapshot\n\n constructor(config: TapperConfig) {\n this.facetRegexes = compileFacetRegexes(config.facets)\n this.triggers = deriveTriggers(config.facets)\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n if (config.initialText) {\n this.replaceText(config.initialText)\n }\n }\n\n subscribe = (listener: () => void) => {\n this.storeListeners.add(listener)\n return () => this.storeListeners.delete(listener)\n }\n\n getSnapshot = (): TapperSnapshot => {\n return this.snapshot\n }\n\n on = <K extends keyof TapperEvents>(\n event: K,\n cb: (data: TapperEvents[K]) => void,\n ) => {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set())\n this.listeners.get(event)!.add(cb)\n return () => {\n this.listeners.get(event)?.delete(cb)\n }\n }\n\n private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {\n this.listeners.get(event)?.forEach(cb => cb(data))\n }\n\n private notify() {\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n this.storeListeners.forEach(l => l())\n }\n\n private update(newText: string, cursor: number) {\n // guard against circular updates when commit() is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const nodes =\n newText === this.text\n ? this.nodes\n : parseNodesFromText(\n newText,\n this.facetRegexes,\n this.nodes,\n cursor,\n this.triggers,\n )\n const prev = this.activeFacet\n const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === 'facet' &&\n node.facetType === prev.type &&\n node.start === prev.range.start &&\n node.end === prev.range.end\n ) {\n node.committed = true\n committedNode = node\n break\n }\n }\n }\n\n // Build activeFacet with commit() baked in\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = newText\n this.cursor = cursor\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\n\n // Fire events after state is finalized\n const facetChanged =\n active?.type !== prev?.type ||\n active?.value !== prev?.value ||\n active?.range.start !== prev?.range.start ||\n active?.range.end !== prev?.range.end\n if (facetChanged) {\n this.emit('activeFacet', active)\n }\n if (committedNode) {\n this.emit('facetCommitted', nodeToFacet(committedNode))\n }\n }\n\n handleTextChange = (newText: string) => {\n const diff = newText.length - this.text.length\n let newCursor = Math.max(this.cursor + diff, 0)\n\n // Atomic deletion: backspace into a facet from outside\n if (diff === -1 && newCursor < this.cursor) {\n for (const node of this.nodes) {\n if (\n node.type === 'facet' &&\n node.committed &&\n this.cursor === node.end\n ) {\n const remnant = node.end - node.start - 1\n newText =\n newText.slice(0, node.start) + newText.slice(node.start + remnant)\n newCursor = node.start\n this.pendingCursor = newCursor\n break\n }\n }\n }\n\n this.update(newText, newCursor)\n }\n\n handleSelectionChange = (e: {nativeEvent: {selection: {start: number}}}) => {\n const newCursor = e.nativeEvent.selection.start\n // After atomic deletion, ignore stale cursor values from the DOM\n if (this.pendingCursor !== null) {\n if (newCursor !== this.pendingCursor) {\n return\n }\n this.pendingCursor = null\n }\n\n if (newCursor === this.cursor) return\n\n this.cursor = newCursor\n this.update(this.text, newCursor)\n }\n\n replaceText = (text: string, cursor?: number) => {\n const prevActive = this.activeFacet\n const nodes = parseNodesFromText(text, this.facetRegexes)\n // Mark all non-text nodes as committed (pre-existing facets)\n const newlyCommitted: TapperNode[] = []\n for (const node of nodes) {\n if (node.type === 'facet') {\n node.committed = true\n newlyCommitted.push(node)\n }\n }\n\n const pos = cursor ?? text.length\n const detected = detectActiveFacet(nodes, text, pos, this.triggers)\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = text\n this.cursor = pos\n this.nodes = nodes\n this.activeFacet = active\n\n this.notify()\n\n if (active !== prevActive) {\n this.emit('activeFacet', active)\n }\n for (const node of newlyCommitted) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n if (!this.activeFacet) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, this.activeFacet.range.start) +\n replacement +\n this.text.slice(this.activeFacet.range.end)\n const newCursor = this.activeFacet.range.start + replacement.length\n\n if (this.activeFacet) {\n this.activeFacet.value = value\n this.activeFacet.range = {\n start: this.activeFacet.range.start,\n end: this.activeFacet.range.start + value.length,\n }\n this.emit('afterInsert', this.activeFacet)\n }\n\n this.update(newText, newCursor)\n }\n}\n\nexport function useTapper(config: TapperConfig) {\n const [store] = useState(() => new Tapper(config))\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot)\n const inputRef = useRef<{focus(): void}>(null)\n\n return {\n ...state,\n on: store.on,\n focus: () => inputRef.current?.focus(),\n inputProps: {\n ref: inputRef as React.RefObject<any>,\n value: state.text,\n selection: {start: state.cursor, end: state.cursor},\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useRef, useState, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperEvents,\n TapperNode,\n TapperActiveFacet,\n TapperSelection,\n TapperSnapshot,\n} from './types'\nimport {\n parseNodesFromText,\n detectActiveFacet,\n deriveTriggers,\n nodeToFacet,\n compileFacetRegexes,\n type CompiledFacetRegexes,\n} from './util'\nimport * as defaultFacets from './facets'\n\nexport * from './types'\nexport * from './facets'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()\n\n // Public state\n text = ''\n selection: TapperSelection = {start: 0, end: 0}\n nodes: TapperNode[] = []\n activeFacet: TapperActiveFacet | null = null\n\n // Internal tracking\n private updating = false\n private pendingEnd: number | null = null\n\n // useSyncExternalStore plumbing\n private storeListeners = new Set<() => void>()\n private snapshot: TapperSnapshot\n\n constructor(config?: TapperConfig) {\n const facets = config?.facets || defaultFacets\n this.facetRegexes = compileFacetRegexes(facets)\n this.triggers = deriveTriggers(facets)\n this.snapshot = {\n text: this.text,\n selection: this.selection,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n if (config?.initialText) {\n this.replaceText(config.initialText)\n }\n }\n\n subscribe = (listener: () => void) => {\n this.storeListeners.add(listener)\n return () => this.storeListeners.delete(listener)\n }\n\n getSnapshot = (): TapperSnapshot => {\n return this.snapshot\n }\n\n on = <K extends keyof TapperEvents>(\n event: K,\n cb: (data: TapperEvents[K]) => void,\n ) => {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set())\n this.listeners.get(event)!.add(cb)\n return () => {\n this.listeners.get(event)?.delete(cb)\n }\n }\n\n private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {\n this.listeners.get(event)?.forEach(cb => cb(data))\n }\n\n private notify() {\n this.snapshot = {\n text: this.text,\n selection: this.selection,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n this.storeListeners.forEach(l => l())\n }\n\n private update(newText: string, selection: TapperSelection) {\n // guard against circular updates when insert() is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const cursor = selection.end\n const isRange = selection.start !== selection.end\n\n const nodes =\n newText === this.text\n ? this.nodes\n : parseNodesFromText(\n newText,\n this.facetRegexes,\n this.nodes,\n cursor,\n this.triggers,\n )\n const prev = this.activeFacet\n const detected = isRange\n ? null\n : detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === 'facet' &&\n node.facetType === prev.type &&\n node.start === prev.range.start &&\n node.end === prev.range.end\n ) {\n node.committed = true\n committedNode = node\n break\n }\n }\n }\n\n // Build activeFacet with insert() baked in\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = newText\n this.selection = selection\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\n\n // Fire events after state is finalized\n const facetChanged =\n active?.type !== prev?.type ||\n active?.value !== prev?.value ||\n active?.range.start !== prev?.range.start ||\n active?.range.end !== prev?.range.end\n if (facetChanged) {\n this.emit('activeFacet', active)\n }\n if (committedNode) {\n this.emit('facetCommitted', nodeToFacet(committedNode))\n }\n }\n\n handleTextChange = (newText: string) => {\n const diff = newText.length - this.text.length\n let newEnd = Math.max(this.selection.end + diff, 0)\n\n // Atomic deletion: backspace into a facet from outside\n if (diff === -1 && newEnd < this.selection.end) {\n for (const node of this.nodes) {\n if (\n node.type === 'facet' &&\n node.committed &&\n this.selection.end === node.end\n ) {\n const remnant = node.end - node.start - 1\n newText =\n newText.slice(0, node.start) + newText.slice(node.start + remnant)\n newEnd = node.start\n this.pendingEnd = newEnd\n break\n }\n }\n }\n\n this.update(newText, {start: newEnd, end: newEnd})\n }\n\n handleSelectionChange = (e: {\n nativeEvent: {selection: {start: number; end: number}}\n }) => {\n const {start, end} = e.nativeEvent.selection\n // After atomic deletion, ignore stale cursor values from the DOM\n if (this.pendingEnd !== null) {\n if (end !== this.pendingEnd) {\n return\n }\n this.pendingEnd = null\n }\n\n if (start === this.selection.start && end === this.selection.end) return\n\n this.selection = {start, end}\n this.update(this.text, {start, end})\n }\n\n replaceText = (text: string, cursor?: number) => {\n const prevActive = this.activeFacet\n const nodes = parseNodesFromText(text, this.facetRegexes)\n // Mark all non-text nodes as committed (pre-existing facets)\n const newlyCommitted: TapperNode[] = []\n for (const node of nodes) {\n if (node.type === 'facet') {\n node.committed = true\n newlyCommitted.push(node)\n }\n }\n\n const pos = cursor ?? text.length\n const detected = detectActiveFacet(nodes, text, pos, this.triggers)\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = text\n this.selection = {start: pos, end: pos}\n this.nodes = nodes\n this.activeFacet = active\n\n this.notify()\n\n if (active !== prevActive) {\n this.emit('activeFacet', active)\n }\n for (const node of newlyCommitted) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n if (!this.activeFacet) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, this.activeFacet.range.start) +\n replacement +\n this.text.slice(this.activeFacet.range.end)\n const newEnd = this.activeFacet.range.start + replacement.length\n\n if (this.activeFacet) {\n this.activeFacet.value = value\n this.activeFacet.range = {\n start: this.activeFacet.range.start,\n end: this.activeFacet.range.start + value.length,\n }\n this.emit('afterInsert', this.activeFacet)\n }\n\n this.update(newText, {start: newEnd, end: newEnd})\n }\n}\n\nexport function useTapper(config: TapperConfig) {\n const [store] = useState(() => new Tapper(config))\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot)\n const inputRef = useRef<{focus(): void}>(null)\n\n return {\n ...state,\n on: store.on,\n focus: () => inputRef.current?.focus(),\n inputProps: {\n ref: inputRef as React.RefObject<any>,\n value: state.text,\n selection: state.selection,\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
|
package/build/types.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
export type TapperFacetConfig =
|
|
2
|
-
match: RegExp;
|
|
3
|
-
};
|
|
4
|
-
export type TapperFacetConfigMap = Record<string, TapperFacetConfig>;
|
|
1
|
+
export type TapperFacetConfig = Record<string, RegExp>;
|
|
5
2
|
export type TapperNode = {
|
|
6
3
|
type: 'text';
|
|
7
4
|
raw: string;
|
|
@@ -29,6 +26,7 @@ export type TapperNode = {
|
|
|
29
26
|
};
|
|
30
27
|
export type TapperFacet = {
|
|
31
28
|
type: string;
|
|
29
|
+
raw: string;
|
|
32
30
|
value: string;
|
|
33
31
|
range: {
|
|
34
32
|
start: number;
|
|
@@ -40,14 +38,18 @@ export type TapperActiveFacet = TapperFacet & {
|
|
|
40
38
|
noTrailingSpace?: boolean;
|
|
41
39
|
}) => void;
|
|
42
40
|
};
|
|
41
|
+
export type TapperSelection = {
|
|
42
|
+
start: number;
|
|
43
|
+
end: number;
|
|
44
|
+
};
|
|
43
45
|
export type TapperSnapshot = {
|
|
44
46
|
text: string;
|
|
45
|
-
|
|
47
|
+
selection: TapperSelection;
|
|
46
48
|
nodes: TapperNode[];
|
|
47
49
|
activeFacet: TapperActiveFacet | null;
|
|
48
50
|
};
|
|
49
51
|
export type TapperConfig = {
|
|
50
|
-
facets
|
|
52
|
+
facets?: TapperFacetConfig;
|
|
51
53
|
initialText?: string;
|
|
52
54
|
};
|
|
53
55
|
export type TapperEvents = {
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAA;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,eAAe,CAAA;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,cAAc,EAAE,WAAW,CAAA;IAC3B,WAAW,EAAE,WAAW,CAAA;CACzB,CAAA"}
|
package/build/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig =
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = Record<string, RegExp>\n\nexport type TapperNode =\n | {\n type: 'text'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType?: undefined\n }\n | {\n type: 'trigger'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n | {\n type: 'facet'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n\nexport type TapperFacet = {\n type: string\n raw: string\n value: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n insert: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSelection = {start: number; end: number}\n\nexport type TapperSnapshot = {\n text: string\n selection: TapperSelection\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets?: TapperFacetConfig\n initialText?: string\n}\n\nexport type TapperEvents = {\n activeFacet: TapperActiveFacet | null\n facetCommitted: TapperFacet\n afterInsert: TapperFacet\n}\n"]}
|
package/build/util.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TapperFacetConfig, TapperNode, TapperFacet } from './types';
|
|
2
2
|
export type CompiledFacetRegexes = Map<string, RegExp>;
|
|
3
3
|
/**
|
|
4
4
|
* Pre-compile facet regexes once at init time. This avoids re-creating
|
|
@@ -6,9 +6,9 @@ export type CompiledFacetRegexes = Map<string, RegExp>;
|
|
|
6
6
|
* instance gets its own compiled copy so lastIndex state can't leak
|
|
7
7
|
* between instances.
|
|
8
8
|
*/
|
|
9
|
-
export declare function compileFacetRegexes(config:
|
|
9
|
+
export declare function compileFacetRegexes(config: TapperFacetConfig): CompiledFacetRegexes;
|
|
10
10
|
export declare function parseNodesFromText(text: string, regexes: CompiledFacetRegexes, prevNodes?: TapperNode[], cursor?: number, triggers?: Map<string, string>): TapperNode[];
|
|
11
|
-
export declare function deriveTriggers(config:
|
|
11
|
+
export declare function deriveTriggers(config: TapperFacetConfig): Map<string, string>;
|
|
12
12
|
export declare function nodeToFacet(node: TapperNode): TapperFacet;
|
|
13
13
|
export declare function detectActiveFacet(nodes: TapperNode[], text: string, cursor: number, triggers: Map<string, string>): TapperFacet | null;
|
|
14
14
|
//# sourceMappingURL=util.d.ts.map
|
package/build/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;AAIlE,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAMtB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,SAAS,CAAC,EAAE,UAAU,EAAE,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,UAAU,EAAE,CAqJd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAO7E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAOzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CAsCpB"}
|
package/build/util.js
CHANGED
|
@@ -7,8 +7,8 @@ const WHITESPACE = /\s/;
|
|
|
7
7
|
*/
|
|
8
8
|
export function compileFacetRegexes(config) {
|
|
9
9
|
const compiled = new Map();
|
|
10
|
-
for (const [name,
|
|
11
|
-
compiled.set(name, new RegExp(
|
|
10
|
+
for (const [name, re] of Object.entries(config)) {
|
|
11
|
+
compiled.set(name, new RegExp(re.source, re.flags));
|
|
12
12
|
}
|
|
13
13
|
return compiled;
|
|
14
14
|
}
|
|
@@ -147,8 +147,8 @@ export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
|
|
|
147
147
|
}
|
|
148
148
|
export function deriveTriggers(config) {
|
|
149
149
|
const triggers = new Map();
|
|
150
|
-
for (const [name,
|
|
151
|
-
const m =
|
|
150
|
+
for (const [name, re] of Object.entries(config)) {
|
|
151
|
+
const m = re.source.match(/^[^\\([\]{}.*+?^$|]+/);
|
|
152
152
|
if (m)
|
|
153
153
|
triggers.set(m[0], name);
|
|
154
154
|
}
|
|
@@ -157,6 +157,7 @@ export function deriveTriggers(config) {
|
|
|
157
157
|
export function nodeToFacet(node) {
|
|
158
158
|
return {
|
|
159
159
|
type: node.facetType,
|
|
160
|
+
raw: node.raw,
|
|
160
161
|
value: node.value,
|
|
161
162
|
range: { start: node.start, end: node.end },
|
|
162
163
|
};
|
|
@@ -166,6 +167,7 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
|
|
|
166
167
|
if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
|
|
167
168
|
return {
|
|
168
169
|
type: node.facetType,
|
|
170
|
+
raw: node.raw,
|
|
169
171
|
value: node.value,
|
|
170
172
|
range: { start: node.start, end: node.end },
|
|
171
173
|
};
|
|
@@ -173,6 +175,7 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
|
|
|
173
175
|
if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
|
|
174
176
|
return {
|
|
175
177
|
type: node.facetType,
|
|
178
|
+
raw: node.raw,
|
|
176
179
|
value: node.value,
|
|
177
180
|
range: { start: node.start, end: node.end },
|
|
178
181
|
};
|
|
@@ -186,8 +189,10 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
|
|
|
186
189
|
break;
|
|
187
190
|
const type = triggers.get(ch);
|
|
188
191
|
if (type) {
|
|
192
|
+
const raw = text.slice(i, cursor);
|
|
189
193
|
return {
|
|
190
194
|
type,
|
|
195
|
+
raw,
|
|
191
196
|
value: text.slice(i + 1, cursor),
|
|
192
197
|
range: { start: i, end: cursor },
|
|
193
198
|
};
|
package/build/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import {TapperFacetConfigMap, TapperNode, TapperFacet} from './types'\n\nconst WHITESPACE = /\\s/\n\nexport type CompiledFacetRegexes = Map<string, RegExp>\n\n/**\n * Pre-compile facet regexes once at init time. This avoids re-creating\n * RegExp objects on every keystroke in parseNodesFromText. Each Tapper\n * instance gets its own compiled copy so lastIndex state can't leak\n * between instances.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfigMap,\n): CompiledFacetRegexes {\n const compiled = new Map<string, RegExp>()\n for (const [name, def] of Object.entries(config)) {\n compiled.set(name, new RegExp(def.match.source, def.match.flags))\n }\n return compiled\n}\n\nexport function parseNodesFromText(\n text: string,\n regexes: CompiledFacetRegexes,\n prevNodes?: TapperNode[],\n cursor?: number,\n triggers?: Map<string, string>,\n): TapperNode[] {\n const allMatches: {\n facetName: string\n fullMatch: string\n capture: string\n index: number\n }[] = []\n\n for (const [name, re] of regexes) {\n // Reset lastIndex so stateful (global) regexes don't carry over\n // match positions from the previous parse call.\n re.lastIndex = 0\n for (const m of text.matchAll(re)) {\n allMatches.push({\n facetName: name,\n fullMatch: m[0],\n capture: m[1] ?? m[0],\n index: m.index,\n })\n }\n }\n\n allMatches.sort(\n (a, b) => a.index - b.index || b.fullMatch.length - a.fullMatch.length,\n )\n\n const accepted: typeof allMatches = []\n let lastEnd = 0\n for (const m of allMatches) {\n if (m.index >= lastEnd) {\n accepted.push(m)\n lastEnd = m.index + m.fullMatch.length\n }\n }\n\n const nodes: TapperNode[] = []\n let pos = 0\n\n for (const m of accepted) {\n if (m.index > pos) {\n const raw = text.slice(pos, m.index)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n type: 'facet',\n facetType: m.facetName,\n raw: m.fullMatch,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n pos = m.index + m.fullMatch.length\n }\n\n if (pos < text.length) {\n const raw = text.slice(pos)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: text.length,\n })\n }\n\n // If the cursor is right after a trigger char that the regex didn't match,\n // splice a 'trigger' node out of the containing text node.\n if (cursor != null && triggers) {\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n if (WHITESPACE.test(ch)) break\n const facetType = triggers.get(ch)\n if (facetType) {\n // Only create a trigger node if the trigger is inside a text node\n // (i.e. the regex didn't already match it as a facet)\n const textNodeIdx = nodes.findIndex(\n n => n.type === 'text' && n.start <= i && n.end > i,\n )\n if (textNodeIdx !== -1) {\n const node = nodes[textNodeIdx]\n const spliced: TapperNode[] = []\n if (node.start < i) {\n const raw = text.slice(node.start, i)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: node.start,\n end: i,\n })\n }\n const triggerRaw = text.slice(i, cursor)\n spliced.push({\n type: 'trigger',\n facetType,\n raw: triggerRaw,\n value: text.slice(i + ch.length, cursor),\n start: i,\n end: cursor,\n })\n if (cursor < node.end) {\n const raw = text.slice(cursor, node.end)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: cursor,\n end: node.end,\n })\n }\n nodes.splice(textNodeIdx, 1, ...spliced)\n }\n break\n }\n }\n }\n\n // Transfer committed flags from previous nodes by facetType + occurrence index\n if (prevNodes) {\n const counts = new Map<string, number>()\n const committedByTypeIndex = new Set<string>()\n\n for (const node of prevNodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.facetType}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {\n node.committed = true\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(\n config: TapperFacetConfigMap,\n): Map<string, string> {\n const triggers = new Map<string, string>()\n for (const [name, def] of Object.entries(config)) {\n const m = def.match.source.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n // Stop at whitespace — triggers don't span across words\n if (WHITESPACE.test(ch)) break\n const type = triggers.get(ch)\n if (type) {\n return {\n type,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n\n return null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import {TapperFacetConfig, TapperNode, TapperFacet} from './types'\n\nconst WHITESPACE = /\\s/\n\nexport type CompiledFacetRegexes = Map<string, RegExp>\n\n/**\n * Pre-compile facet regexes once at init time. This avoids re-creating\n * RegExp objects on every keystroke in parseNodesFromText. Each Tapper\n * instance gets its own compiled copy so lastIndex state can't leak\n * between instances.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfig,\n): CompiledFacetRegexes {\n const compiled = new Map<string, RegExp>()\n for (const [name, re] of Object.entries(config)) {\n compiled.set(name, new RegExp(re.source, re.flags))\n }\n return compiled\n}\n\nexport function parseNodesFromText(\n text: string,\n regexes: CompiledFacetRegexes,\n prevNodes?: TapperNode[],\n cursor?: number,\n triggers?: Map<string, string>,\n): TapperNode[] {\n const allMatches: {\n facetName: string\n fullMatch: string\n capture: string\n index: number\n }[] = []\n\n for (const [name, re] of regexes) {\n // Reset lastIndex so stateful (global) regexes don't carry over\n // match positions from the previous parse call.\n re.lastIndex = 0\n for (const m of text.matchAll(re)) {\n allMatches.push({\n facetName: name,\n fullMatch: m[0],\n capture: m[1] ?? m[0],\n index: m.index,\n })\n }\n }\n\n allMatches.sort(\n (a, b) => a.index - b.index || b.fullMatch.length - a.fullMatch.length,\n )\n\n const accepted: typeof allMatches = []\n let lastEnd = 0\n for (const m of allMatches) {\n if (m.index >= lastEnd) {\n accepted.push(m)\n lastEnd = m.index + m.fullMatch.length\n }\n }\n\n const nodes: TapperNode[] = []\n let pos = 0\n\n for (const m of accepted) {\n if (m.index > pos) {\n const raw = text.slice(pos, m.index)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n type: 'facet',\n facetType: m.facetName,\n raw: m.fullMatch,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n pos = m.index + m.fullMatch.length\n }\n\n if (pos < text.length) {\n const raw = text.slice(pos)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: text.length,\n })\n }\n\n // If the cursor is right after a trigger char that the regex didn't match,\n // splice a 'trigger' node out of the containing text node.\n if (cursor != null && triggers) {\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n if (WHITESPACE.test(ch)) break\n const facetType = triggers.get(ch)\n if (facetType) {\n // Only create a trigger node if the trigger is inside a text node\n // (i.e. the regex didn't already match it as a facet)\n const textNodeIdx = nodes.findIndex(\n n => n.type === 'text' && n.start <= i && n.end > i,\n )\n if (textNodeIdx !== -1) {\n const node = nodes[textNodeIdx]\n const spliced: TapperNode[] = []\n if (node.start < i) {\n const raw = text.slice(node.start, i)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: node.start,\n end: i,\n })\n }\n const triggerRaw = text.slice(i, cursor)\n spliced.push({\n type: 'trigger',\n facetType,\n raw: triggerRaw,\n value: text.slice(i + ch.length, cursor),\n start: i,\n end: cursor,\n })\n if (cursor < node.end) {\n const raw = text.slice(cursor, node.end)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: cursor,\n end: node.end,\n })\n }\n nodes.splice(textNodeIdx, 1, ...spliced)\n }\n break\n }\n }\n }\n\n // Transfer committed flags from previous nodes by facetType + occurrence index\n if (prevNodes) {\n const counts = new Map<string, number>()\n const committedByTypeIndex = new Set<string>()\n\n for (const node of prevNodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.facetType}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {\n node.committed = true\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(config: TapperFacetConfig): Map<string, string> {\n const triggers = new Map<string, string>()\n for (const [name, re] of Object.entries(config)) {\n const m = re.source.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n // Stop at whitespace — triggers don't span across words\n if (WHITESPACE.test(ch)) break\n const type = triggers.get(ch)\n if (type) {\n const raw = text.slice(i, cursor)\n return {\n type,\n raw,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n\n return null\n}\n"]}
|
package/package.json
CHANGED
package/src/facets.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const mention = /@([a-zA-Z0-9.-]+)/g
|
|
2
|
+
export const emoji = /:([a-zA-Z0-9_]+):/g
|
|
3
|
+
export const tag =
|
|
4
|
+
/[##]((?!\ufe0f)[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]*[^\d\s\p{P}\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2]+[^\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2\p{P}]*)?/gu
|
|
5
|
+
export const url =
|
|
6
|
+
/((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TapperEvents,
|
|
6
6
|
TapperNode,
|
|
7
7
|
TapperActiveFacet,
|
|
8
|
+
TapperSelection,
|
|
8
9
|
TapperSnapshot,
|
|
9
10
|
} from './types'
|
|
10
11
|
import {
|
|
@@ -15,8 +16,10 @@ import {
|
|
|
15
16
|
compileFacetRegexes,
|
|
16
17
|
type CompiledFacetRegexes,
|
|
17
18
|
} from './util'
|
|
19
|
+
import * as defaultFacets from './facets'
|
|
18
20
|
|
|
19
21
|
export * from './types'
|
|
22
|
+
export * from './facets'
|
|
20
23
|
|
|
21
24
|
export class Tapper {
|
|
22
25
|
private facetRegexes: CompiledFacetRegexes
|
|
@@ -28,28 +31,29 @@ export class Tapper {
|
|
|
28
31
|
|
|
29
32
|
// Public state
|
|
30
33
|
text = ''
|
|
31
|
-
|
|
34
|
+
selection: TapperSelection = {start: 0, end: 0}
|
|
32
35
|
nodes: TapperNode[] = []
|
|
33
36
|
activeFacet: TapperActiveFacet | null = null
|
|
34
37
|
|
|
35
38
|
// Internal tracking
|
|
36
39
|
private updating = false
|
|
37
|
-
private
|
|
40
|
+
private pendingEnd: number | null = null
|
|
38
41
|
|
|
39
42
|
// useSyncExternalStore plumbing
|
|
40
43
|
private storeListeners = new Set<() => void>()
|
|
41
44
|
private snapshot: TapperSnapshot
|
|
42
45
|
|
|
43
|
-
constructor(config
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
+
constructor(config?: TapperConfig) {
|
|
47
|
+
const facets = config?.facets || defaultFacets
|
|
48
|
+
this.facetRegexes = compileFacetRegexes(facets)
|
|
49
|
+
this.triggers = deriveTriggers(facets)
|
|
46
50
|
this.snapshot = {
|
|
47
51
|
text: this.text,
|
|
48
|
-
|
|
52
|
+
selection: this.selection,
|
|
49
53
|
nodes: this.nodes,
|
|
50
54
|
activeFacet: this.activeFacet,
|
|
51
55
|
}
|
|
52
|
-
if (config
|
|
56
|
+
if (config?.initialText) {
|
|
53
57
|
this.replaceText(config.initialText)
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -81,18 +85,21 @@ export class Tapper {
|
|
|
81
85
|
private notify() {
|
|
82
86
|
this.snapshot = {
|
|
83
87
|
text: this.text,
|
|
84
|
-
|
|
88
|
+
selection: this.selection,
|
|
85
89
|
nodes: this.nodes,
|
|
86
90
|
activeFacet: this.activeFacet,
|
|
87
91
|
}
|
|
88
92
|
this.storeListeners.forEach(l => l())
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
private update(newText: string,
|
|
92
|
-
// guard against circular updates when
|
|
95
|
+
private update(newText: string, selection: TapperSelection) {
|
|
96
|
+
// guard against circular updates when insert() is called during an update cycle
|
|
93
97
|
if (this.updating) return
|
|
94
98
|
this.updating = true
|
|
95
99
|
|
|
100
|
+
const cursor = selection.end
|
|
101
|
+
const isRange = selection.start !== selection.end
|
|
102
|
+
|
|
96
103
|
const nodes =
|
|
97
104
|
newText === this.text
|
|
98
105
|
? this.nodes
|
|
@@ -104,7 +111,9 @@ export class Tapper {
|
|
|
104
111
|
this.triggers,
|
|
105
112
|
)
|
|
106
113
|
const prev = this.activeFacet
|
|
107
|
-
const detected =
|
|
114
|
+
const detected = isRange
|
|
115
|
+
? null
|
|
116
|
+
: detectActiveFacet(nodes, newText, cursor, this.triggers)
|
|
108
117
|
|
|
109
118
|
// When cursor leaves a facet, commit it
|
|
110
119
|
let committedNode: TapperNode | null = null
|
|
@@ -123,13 +132,13 @@ export class Tapper {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
// Build activeFacet with
|
|
135
|
+
// Build activeFacet with insert() baked in
|
|
127
136
|
const active: TapperActiveFacet | null = detected
|
|
128
137
|
? {...detected, insert: this.insert}
|
|
129
138
|
: null
|
|
130
139
|
|
|
131
140
|
this.text = newText
|
|
132
|
-
this.
|
|
141
|
+
this.selection = selection
|
|
133
142
|
this.nodes = nodes
|
|
134
143
|
this.activeFacet = active
|
|
135
144
|
|
|
@@ -152,43 +161,45 @@ export class Tapper {
|
|
|
152
161
|
|
|
153
162
|
handleTextChange = (newText: string) => {
|
|
154
163
|
const diff = newText.length - this.text.length
|
|
155
|
-
let
|
|
164
|
+
let newEnd = Math.max(this.selection.end + diff, 0)
|
|
156
165
|
|
|
157
166
|
// Atomic deletion: backspace into a facet from outside
|
|
158
|
-
if (diff === -1 &&
|
|
167
|
+
if (diff === -1 && newEnd < this.selection.end) {
|
|
159
168
|
for (const node of this.nodes) {
|
|
160
169
|
if (
|
|
161
170
|
node.type === 'facet' &&
|
|
162
171
|
node.committed &&
|
|
163
|
-
this.
|
|
172
|
+
this.selection.end === node.end
|
|
164
173
|
) {
|
|
165
174
|
const remnant = node.end - node.start - 1
|
|
166
175
|
newText =
|
|
167
176
|
newText.slice(0, node.start) + newText.slice(node.start + remnant)
|
|
168
|
-
|
|
169
|
-
this.
|
|
177
|
+
newEnd = node.start
|
|
178
|
+
this.pendingEnd = newEnd
|
|
170
179
|
break
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
this.update(newText,
|
|
184
|
+
this.update(newText, {start: newEnd, end: newEnd})
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
handleSelectionChange = (e: {
|
|
179
|
-
|
|
187
|
+
handleSelectionChange = (e: {
|
|
188
|
+
nativeEvent: {selection: {start: number; end: number}}
|
|
189
|
+
}) => {
|
|
190
|
+
const {start, end} = e.nativeEvent.selection
|
|
180
191
|
// After atomic deletion, ignore stale cursor values from the DOM
|
|
181
|
-
if (this.
|
|
182
|
-
if (
|
|
192
|
+
if (this.pendingEnd !== null) {
|
|
193
|
+
if (end !== this.pendingEnd) {
|
|
183
194
|
return
|
|
184
195
|
}
|
|
185
|
-
this.
|
|
196
|
+
this.pendingEnd = null
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
if (
|
|
199
|
+
if (start === this.selection.start && end === this.selection.end) return
|
|
189
200
|
|
|
190
|
-
this.
|
|
191
|
-
this.update(this.text,
|
|
201
|
+
this.selection = {start, end}
|
|
202
|
+
this.update(this.text, {start, end})
|
|
192
203
|
}
|
|
193
204
|
|
|
194
205
|
replaceText = (text: string, cursor?: number) => {
|
|
@@ -210,7 +221,7 @@ export class Tapper {
|
|
|
210
221
|
: null
|
|
211
222
|
|
|
212
223
|
this.text = text
|
|
213
|
-
this.
|
|
224
|
+
this.selection = {start: pos, end: pos}
|
|
214
225
|
this.nodes = nodes
|
|
215
226
|
this.activeFacet = active
|
|
216
227
|
|
|
@@ -232,7 +243,7 @@ export class Tapper {
|
|
|
232
243
|
this.text.slice(0, this.activeFacet.range.start) +
|
|
233
244
|
replacement +
|
|
234
245
|
this.text.slice(this.activeFacet.range.end)
|
|
235
|
-
const
|
|
246
|
+
const newEnd = this.activeFacet.range.start + replacement.length
|
|
236
247
|
|
|
237
248
|
if (this.activeFacet) {
|
|
238
249
|
this.activeFacet.value = value
|
|
@@ -243,7 +254,7 @@ export class Tapper {
|
|
|
243
254
|
this.emit('afterInsert', this.activeFacet)
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
this.update(newText,
|
|
257
|
+
this.update(newText, {start: newEnd, end: newEnd})
|
|
247
258
|
}
|
|
248
259
|
}
|
|
249
260
|
|
|
@@ -259,7 +270,7 @@ export function useTapper(config: TapperConfig) {
|
|
|
259
270
|
inputProps: {
|
|
260
271
|
ref: inputRef as React.RefObject<any>,
|
|
261
272
|
value: state.text,
|
|
262
|
-
selection:
|
|
273
|
+
selection: state.selection,
|
|
263
274
|
onChangeText: store.handleTextChange,
|
|
264
275
|
onSelectionChange: store.handleSelectionChange,
|
|
265
276
|
},
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
export type TapperFacetConfig =
|
|
2
|
-
match: RegExp
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export type TapperFacetConfigMap = Record<string, TapperFacetConfig>
|
|
1
|
+
export type TapperFacetConfig = Record<string, RegExp>
|
|
6
2
|
|
|
7
3
|
export type TapperNode =
|
|
8
4
|
| {
|
|
@@ -35,6 +31,7 @@ export type TapperNode =
|
|
|
35
31
|
|
|
36
32
|
export type TapperFacet = {
|
|
37
33
|
type: string
|
|
34
|
+
raw: string
|
|
38
35
|
value: string
|
|
39
36
|
range: {start: number; end: number}
|
|
40
37
|
}
|
|
@@ -43,15 +40,17 @@ export type TapperActiveFacet = TapperFacet & {
|
|
|
43
40
|
insert: (value: string, options?: {noTrailingSpace?: boolean}) => void
|
|
44
41
|
}
|
|
45
42
|
|
|
43
|
+
export type TapperSelection = {start: number; end: number}
|
|
44
|
+
|
|
46
45
|
export type TapperSnapshot = {
|
|
47
46
|
text: string
|
|
48
|
-
|
|
47
|
+
selection: TapperSelection
|
|
49
48
|
nodes: TapperNode[]
|
|
50
49
|
activeFacet: TapperActiveFacet | null
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
export type TapperConfig = {
|
|
54
|
-
facets
|
|
53
|
+
facets?: TapperFacetConfig
|
|
55
54
|
initialText?: string
|
|
56
55
|
}
|
|
57
56
|
|
package/src/util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {TapperFacetConfig, TapperNode, TapperFacet} from './types'
|
|
2
2
|
|
|
3
3
|
const WHITESPACE = /\s/
|
|
4
4
|
|
|
@@ -11,11 +11,11 @@ export type CompiledFacetRegexes = Map<string, RegExp>
|
|
|
11
11
|
* between instances.
|
|
12
12
|
*/
|
|
13
13
|
export function compileFacetRegexes(
|
|
14
|
-
config:
|
|
14
|
+
config: TapperFacetConfig,
|
|
15
15
|
): CompiledFacetRegexes {
|
|
16
16
|
const compiled = new Map<string, RegExp>()
|
|
17
|
-
for (const [name,
|
|
18
|
-
compiled.set(name, new RegExp(
|
|
17
|
+
for (const [name, re] of Object.entries(config)) {
|
|
18
|
+
compiled.set(name, new RegExp(re.source, re.flags))
|
|
19
19
|
}
|
|
20
20
|
return compiled
|
|
21
21
|
}
|
|
@@ -177,12 +177,10 @@ export function parseNodesFromText(
|
|
|
177
177
|
return nodes
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
export function deriveTriggers(
|
|
181
|
-
config: TapperFacetConfigMap,
|
|
182
|
-
): Map<string, string> {
|
|
180
|
+
export function deriveTriggers(config: TapperFacetConfig): Map<string, string> {
|
|
183
181
|
const triggers = new Map<string, string>()
|
|
184
|
-
for (const [name,
|
|
185
|
-
const m =
|
|
182
|
+
for (const [name, re] of Object.entries(config)) {
|
|
183
|
+
const m = re.source.match(/^[^\\([\]{}.*+?^$|]+/)
|
|
186
184
|
if (m) triggers.set(m[0], name)
|
|
187
185
|
}
|
|
188
186
|
return triggers
|
|
@@ -191,6 +189,7 @@ export function deriveTriggers(
|
|
|
191
189
|
export function nodeToFacet(node: TapperNode): TapperFacet {
|
|
192
190
|
return {
|
|
193
191
|
type: node.facetType!,
|
|
192
|
+
raw: node.raw,
|
|
194
193
|
value: node.value,
|
|
195
194
|
range: {start: node.start, end: node.end},
|
|
196
195
|
}
|
|
@@ -206,6 +205,7 @@ export function detectActiveFacet(
|
|
|
206
205
|
if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
|
|
207
206
|
return {
|
|
208
207
|
type: node.facetType,
|
|
208
|
+
raw: node.raw,
|
|
209
209
|
value: node.value,
|
|
210
210
|
range: {start: node.start, end: node.end},
|
|
211
211
|
}
|
|
@@ -213,6 +213,7 @@ export function detectActiveFacet(
|
|
|
213
213
|
if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
|
|
214
214
|
return {
|
|
215
215
|
type: node.facetType,
|
|
216
|
+
raw: node.raw,
|
|
216
217
|
value: node.value,
|
|
217
218
|
range: {start: node.start, end: node.end},
|
|
218
219
|
}
|
|
@@ -226,8 +227,10 @@ export function detectActiveFacet(
|
|
|
226
227
|
if (WHITESPACE.test(ch)) break
|
|
227
228
|
const type = triggers.get(ch)
|
|
228
229
|
if (type) {
|
|
230
|
+
const raw = text.slice(i, cursor)
|
|
229
231
|
return {
|
|
230
232
|
type,
|
|
233
|
+
raw,
|
|
231
234
|
value: text.slice(i + 1, cursor),
|
|
232
235
|
range: {start: i, end: cursor},
|
|
233
236
|
}
|