@bsky.app/tapper 0.1.1 → 0.2.2
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 +13 -0
- package/README.md +204 -1
- package/build/index.d.ts +36 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +197 -8
- package/build/index.js.map +1 -1
- package/build/types.d.ts +26 -3
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/util.d.ts +10 -1
- package/build/util.d.ts.map +1 -1
- package/build/util.js +116 -30
- package/build/util.js.map +1 -1
- package/package.json +15 -15
- package/src/index.ts +257 -10
- package/src/types.ts +36 -9
- package/src/util.ts +131 -36
- package/build/core.d.ts +0 -30
- package/build/core.d.ts.map +0 -1
- package/build/core.js +0 -148
- package/build/core.js.map +0 -1
- package/build/index.web.d.ts +0 -17
- package/build/index.web.d.ts.map +0 -1
- package/build/index.web.js +0 -33
- package/build/index.web.js.map +0 -1
- package/src/core.ts +0 -185
- package/src/index.web.ts +0 -46
package/src/util.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {TapperFacetConfigMap, TapperNode, TapperFacet} from './types'
|
|
2
|
+
|
|
3
|
+
const WHITESPACE = /\s/
|
|
4
|
+
|
|
5
|
+
export type CompiledFacetRegexes = Map<string, RegExp>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pre-compile facet regexes once at init time. This avoids re-creating
|
|
9
|
+
* RegExp objects on every keystroke in parseNodesFromText. Each Tapper
|
|
10
|
+
* instance gets its own compiled copy so lastIndex state can't leak
|
|
11
|
+
* between instances.
|
|
12
|
+
*/
|
|
13
|
+
export function compileFacetRegexes(
|
|
14
|
+
config: TapperFacetConfigMap,
|
|
15
|
+
): CompiledFacetRegexes {
|
|
16
|
+
const compiled = new Map<string, RegExp>()
|
|
17
|
+
for (const [name, def] of Object.entries(config)) {
|
|
18
|
+
compiled.set(name, new RegExp(def.match.source, def.match.flags))
|
|
19
|
+
}
|
|
20
|
+
return compiled
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
export function parseNodesFromText(
|
|
8
24
|
text: string,
|
|
9
|
-
|
|
25
|
+
regexes: CompiledFacetRegexes,
|
|
10
26
|
prevNodes?: TapperNode[],
|
|
27
|
+
cursor?: number,
|
|
28
|
+
triggers?: Map<string, string>,
|
|
11
29
|
): TapperNode[] {
|
|
12
30
|
const allMatches: {
|
|
13
31
|
facetName: string
|
|
@@ -16,13 +34,15 @@ export function parseNodesFromText(
|
|
|
16
34
|
index: number
|
|
17
35
|
}[] = []
|
|
18
36
|
|
|
19
|
-
for (const [name,
|
|
20
|
-
|
|
37
|
+
for (const [name, re] of regexes) {
|
|
38
|
+
// Reset lastIndex so stateful (global) regexes don't carry over
|
|
39
|
+
// match positions from the previous parse call.
|
|
40
|
+
re.lastIndex = 0
|
|
21
41
|
for (const m of text.matchAll(re)) {
|
|
22
42
|
allMatches.push({
|
|
23
43
|
facetName: name,
|
|
24
44
|
fullMatch: m[0],
|
|
25
|
-
capture: m[0],
|
|
45
|
+
capture: m[1] ?? m[0],
|
|
26
46
|
index: m.index,
|
|
27
47
|
})
|
|
28
48
|
}
|
|
@@ -42,55 +62,113 @@ export function parseNodesFromText(
|
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
const nodes: TapperNode[] = []
|
|
45
|
-
let
|
|
65
|
+
let pos = 0
|
|
46
66
|
|
|
47
67
|
for (const m of accepted) {
|
|
48
|
-
if (m.index >
|
|
68
|
+
if (m.index > pos) {
|
|
69
|
+
const raw = text.slice(pos, m.index)
|
|
49
70
|
nodes.push({
|
|
50
71
|
type: 'text',
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
raw,
|
|
73
|
+
value: raw,
|
|
74
|
+
start: pos,
|
|
53
75
|
end: m.index,
|
|
54
76
|
})
|
|
55
77
|
}
|
|
56
78
|
nodes.push({
|
|
57
|
-
type:
|
|
79
|
+
type: 'facet',
|
|
80
|
+
facetType: m.facetName,
|
|
81
|
+
raw: m.fullMatch,
|
|
58
82
|
value: m.capture,
|
|
59
83
|
start: m.index,
|
|
60
84
|
end: m.index + m.fullMatch.length,
|
|
61
85
|
})
|
|
62
|
-
|
|
86
|
+
pos = m.index + m.fullMatch.length
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
if (
|
|
89
|
+
if (pos < text.length) {
|
|
90
|
+
const raw = text.slice(pos)
|
|
66
91
|
nodes.push({
|
|
67
92
|
type: 'text',
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
raw,
|
|
94
|
+
value: raw,
|
|
95
|
+
start: pos,
|
|
70
96
|
end: text.length,
|
|
71
97
|
})
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
//
|
|
100
|
+
// If the cursor is right after a trigger char that the regex didn't match,
|
|
101
|
+
// splice a 'trigger' node out of the containing text node.
|
|
102
|
+
if (cursor != null && triggers) {
|
|
103
|
+
for (let i = cursor - 1; i >= 0; i--) {
|
|
104
|
+
const ch = text[i]
|
|
105
|
+
if (WHITESPACE.test(ch)) break
|
|
106
|
+
const facetType = triggers.get(ch)
|
|
107
|
+
if (facetType) {
|
|
108
|
+
// Only create a trigger node if the trigger is inside a text node
|
|
109
|
+
// (i.e. the regex didn't already match it as a facet)
|
|
110
|
+
const textNodeIdx = nodes.findIndex(
|
|
111
|
+
n => n.type === 'text' && n.start <= i && n.end > i,
|
|
112
|
+
)
|
|
113
|
+
if (textNodeIdx !== -1) {
|
|
114
|
+
const node = nodes[textNodeIdx]
|
|
115
|
+
const spliced: TapperNode[] = []
|
|
116
|
+
if (node.start < i) {
|
|
117
|
+
const raw = text.slice(node.start, i)
|
|
118
|
+
spliced.push({
|
|
119
|
+
type: 'text',
|
|
120
|
+
raw,
|
|
121
|
+
value: raw,
|
|
122
|
+
start: node.start,
|
|
123
|
+
end: i,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
const triggerRaw = text.slice(i, cursor)
|
|
127
|
+
spliced.push({
|
|
128
|
+
type: 'trigger',
|
|
129
|
+
facetType,
|
|
130
|
+
raw: triggerRaw,
|
|
131
|
+
value: text.slice(i + ch.length, cursor),
|
|
132
|
+
start: i,
|
|
133
|
+
end: cursor,
|
|
134
|
+
})
|
|
135
|
+
if (cursor < node.end) {
|
|
136
|
+
const raw = text.slice(cursor, node.end)
|
|
137
|
+
spliced.push({
|
|
138
|
+
type: 'text',
|
|
139
|
+
raw,
|
|
140
|
+
value: raw,
|
|
141
|
+
start: cursor,
|
|
142
|
+
end: node.end,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
nodes.splice(textNodeIdx, 1, ...spliced)
|
|
146
|
+
}
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Transfer committed flags from previous nodes by facetType + occurrence index
|
|
75
153
|
if (prevNodes) {
|
|
76
154
|
const counts = new Map<string, number>()
|
|
77
155
|
const committedByTypeIndex = new Set<string>()
|
|
78
156
|
|
|
79
157
|
for (const node of prevNodes) {
|
|
80
|
-
if (node.type
|
|
81
|
-
const idx = counts.get(node.
|
|
82
|
-
counts.set(node.
|
|
158
|
+
if (node.type !== 'facet') continue
|
|
159
|
+
const idx = counts.get(node.facetType!) ?? 0
|
|
160
|
+
counts.set(node.facetType!, idx + 1)
|
|
83
161
|
if (node.committed) {
|
|
84
|
-
committedByTypeIndex.add(`${node.
|
|
162
|
+
committedByTypeIndex.add(`${node.facetType}:${idx}`)
|
|
85
163
|
}
|
|
86
164
|
}
|
|
87
165
|
|
|
88
166
|
counts.clear()
|
|
89
167
|
for (const node of nodes) {
|
|
90
|
-
if (node.type
|
|
91
|
-
const idx = counts.get(node.
|
|
92
|
-
counts.set(node.
|
|
93
|
-
if (committedByTypeIndex.has(`${node.
|
|
168
|
+
if (node.type !== 'facet') continue
|
|
169
|
+
const idx = counts.get(node.facetType!) ?? 0
|
|
170
|
+
counts.set(node.facetType!, idx + 1)
|
|
171
|
+
if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {
|
|
94
172
|
node.committed = true
|
|
95
173
|
}
|
|
96
174
|
}
|
|
@@ -99,7 +177,9 @@ export function parseNodesFromText(
|
|
|
99
177
|
return nodes
|
|
100
178
|
}
|
|
101
179
|
|
|
102
|
-
export function deriveTriggers(
|
|
180
|
+
export function deriveTriggers(
|
|
181
|
+
config: TapperFacetConfigMap,
|
|
182
|
+
): Map<string, string> {
|
|
103
183
|
const triggers = new Map<string, string>()
|
|
104
184
|
for (const [name, def] of Object.entries(config)) {
|
|
105
185
|
const m = def.match.source.match(/^[^\\([\]{}.*+?^$|]+/)
|
|
@@ -108,6 +188,14 @@ export function deriveTriggers(config: TapperFacetConfigMap): Map<string, string
|
|
|
108
188
|
return triggers
|
|
109
189
|
}
|
|
110
190
|
|
|
191
|
+
export function nodeToFacet(node: TapperNode): TapperFacet {
|
|
192
|
+
return {
|
|
193
|
+
type: node.facetType!,
|
|
194
|
+
value: node.value,
|
|
195
|
+
range: {start: node.start, end: node.end},
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
111
199
|
export function detectActiveFacet(
|
|
112
200
|
nodes: TapperNode[],
|
|
113
201
|
text: string,
|
|
@@ -115,10 +203,17 @@ export function detectActiveFacet(
|
|
|
115
203
|
triggers: Map<string, string>,
|
|
116
204
|
): TapperFacet | null {
|
|
117
205
|
for (const node of nodes) {
|
|
118
|
-
if (node.type
|
|
206
|
+
if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
|
|
207
|
+
return {
|
|
208
|
+
type: node.facetType,
|
|
209
|
+
value: node.value,
|
|
210
|
+
range: {start: node.start, end: node.end},
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
|
|
119
214
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
215
|
+
type: node.facetType,
|
|
216
|
+
value: node.value,
|
|
122
217
|
range: {start: node.start, end: node.end},
|
|
123
218
|
}
|
|
124
219
|
}
|
|
@@ -128,12 +223,12 @@ export function detectActiveFacet(
|
|
|
128
223
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
129
224
|
const ch = text[i]
|
|
130
225
|
// Stop at whitespace — triggers don't span across words
|
|
131
|
-
if (
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
226
|
+
if (WHITESPACE.test(ch)) break
|
|
227
|
+
const type = triggers.get(ch)
|
|
228
|
+
if (type) {
|
|
134
229
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
230
|
+
type,
|
|
231
|
+
value: text.slice(i + 1, cursor),
|
|
137
232
|
range: {start: i, end: cursor},
|
|
138
233
|
}
|
|
139
234
|
}
|
package/build/core.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { TapperConfig, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
|
|
2
|
-
export declare class Tapper {
|
|
3
|
-
private facetConfig;
|
|
4
|
-
private triggers;
|
|
5
|
-
text: string;
|
|
6
|
-
cursor: number;
|
|
7
|
-
nodes: TapperNode[];
|
|
8
|
-
activeFacet: TapperActiveFacet | null;
|
|
9
|
-
private updating;
|
|
10
|
-
private pendingCursor;
|
|
11
|
-
private storeListeners;
|
|
12
|
-
private snapshot;
|
|
13
|
-
constructor(config: TapperConfig);
|
|
14
|
-
subscribe: (listener: () => void) => () => boolean;
|
|
15
|
-
getSnapshot: () => TapperSnapshot;
|
|
16
|
-
private notify;
|
|
17
|
-
private update;
|
|
18
|
-
handleTextChange: (newText: string) => void;
|
|
19
|
-
handleSelectionChange: (newCursor: number) => void;
|
|
20
|
-
replaceText: (text: string, cursor?: number) => void;
|
|
21
|
-
private insert;
|
|
22
|
-
}
|
|
23
|
-
export declare function useTapperCore(config: TapperConfig): {
|
|
24
|
-
store: Tapper;
|
|
25
|
-
text: string;
|
|
26
|
-
cursor: number;
|
|
27
|
-
nodes: TapperNode[];
|
|
28
|
-
activeFacet: TapperActiveFacet | null;
|
|
29
|
-
};
|
|
30
|
-
//# sourceMappingURL=core.d.ts.map
|
package/build/core.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EAEZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,SAAS,CAAA;AAIhB,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,QAAQ,CAAqB;IAGrC,IAAI,SAAK;IACT,MAAM,SAAI;IACV,KAAK,EAAE,UAAU,EAAE,CAAK;IACxB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAO;IAG5C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,aAAa,CAAsB;IAG3C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,EAAE,YAAY;IAchC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IAiCd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,WAAW,MAAM,UAWzC;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UAqB3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY;;;;;;EAIjD"}
|
package/build/core.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
import { parseNodesFromText, detectActiveFacet, deriveTriggers } from './util';
|
|
3
|
-
export class Tapper {
|
|
4
|
-
facetConfig;
|
|
5
|
-
triggers;
|
|
6
|
-
// Public state
|
|
7
|
-
text = '';
|
|
8
|
-
cursor = 0;
|
|
9
|
-
nodes = [];
|
|
10
|
-
activeFacet = null;
|
|
11
|
-
// Internal tracking
|
|
12
|
-
updating = false;
|
|
13
|
-
pendingCursor = null;
|
|
14
|
-
// useSyncExternalStore plumbing
|
|
15
|
-
storeListeners = new Set();
|
|
16
|
-
snapshot;
|
|
17
|
-
constructor(config) {
|
|
18
|
-
this.facetConfig = config.facets;
|
|
19
|
-
this.triggers = deriveTriggers(config.facets);
|
|
20
|
-
this.snapshot = {
|
|
21
|
-
text: this.text,
|
|
22
|
-
cursor: this.cursor,
|
|
23
|
-
nodes: this.nodes,
|
|
24
|
-
activeFacet: this.activeFacet,
|
|
25
|
-
};
|
|
26
|
-
if (config.initialText) {
|
|
27
|
-
this.replaceText(config.initialText);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
subscribe = (listener) => {
|
|
31
|
-
this.storeListeners.add(listener);
|
|
32
|
-
return () => this.storeListeners.delete(listener);
|
|
33
|
-
};
|
|
34
|
-
getSnapshot = () => {
|
|
35
|
-
return this.snapshot;
|
|
36
|
-
};
|
|
37
|
-
notify() {
|
|
38
|
-
this.snapshot = {
|
|
39
|
-
text: this.text,
|
|
40
|
-
cursor: this.cursor,
|
|
41
|
-
nodes: this.nodes,
|
|
42
|
-
activeFacet: this.activeFacet,
|
|
43
|
-
};
|
|
44
|
-
this.storeListeners.forEach(l => l());
|
|
45
|
-
}
|
|
46
|
-
update(newText, cursor) {
|
|
47
|
-
// guard against circular updates when insert is called during an update cycle
|
|
48
|
-
if (this.updating)
|
|
49
|
-
return;
|
|
50
|
-
this.updating = true;
|
|
51
|
-
const nodes = parseNodesFromText(newText, this.facetConfig, this.nodes);
|
|
52
|
-
const prev = this.activeFacet;
|
|
53
|
-
const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
|
|
54
|
-
// When cursor leaves a facet, commit it
|
|
55
|
-
if (prev && (!detected || detected.facetType !== prev.facetType)) {
|
|
56
|
-
for (const node of nodes) {
|
|
57
|
-
if (node.type === prev.facetType && node.value === prev.query) {
|
|
58
|
-
node.committed = true;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Build activeFacet with insert baked in
|
|
64
|
-
const active = detected
|
|
65
|
-
? { ...detected, insert: this.insert }
|
|
66
|
-
: null;
|
|
67
|
-
this.text = newText;
|
|
68
|
-
this.cursor = cursor;
|
|
69
|
-
this.nodes = nodes;
|
|
70
|
-
this.activeFacet = active;
|
|
71
|
-
this.updating = false;
|
|
72
|
-
this.notify();
|
|
73
|
-
}
|
|
74
|
-
handleTextChange = (newText) => {
|
|
75
|
-
const diff = newText.length - this.text.length;
|
|
76
|
-
let newCursor = Math.max(this.cursor + diff, 0);
|
|
77
|
-
// Atomic deletion: backspace into a facet from outside
|
|
78
|
-
if (diff === -1 && newCursor < this.cursor) {
|
|
79
|
-
for (const node of this.nodes) {
|
|
80
|
-
if (node.type !== 'text' &&
|
|
81
|
-
node.committed &&
|
|
82
|
-
this.cursor === node.end) {
|
|
83
|
-
const remnant = node.end - node.start - 1;
|
|
84
|
-
newText =
|
|
85
|
-
newText.slice(0, node.start) + newText.slice(node.start + remnant);
|
|
86
|
-
newCursor = node.start;
|
|
87
|
-
this.pendingCursor = newCursor;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
this.update(newText, newCursor);
|
|
93
|
-
};
|
|
94
|
-
handleSelectionChange = (newCursor) => {
|
|
95
|
-
// After atomic deletion, ignore stale cursor values from the DOM
|
|
96
|
-
if (this.pendingCursor !== null) {
|
|
97
|
-
if (newCursor !== this.pendingCursor) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
this.pendingCursor = null;
|
|
101
|
-
}
|
|
102
|
-
this.cursor = newCursor;
|
|
103
|
-
this.update(this.text, newCursor);
|
|
104
|
-
};
|
|
105
|
-
replaceText = (text, cursor) => {
|
|
106
|
-
const nodes = parseNodesFromText(text, this.facetConfig);
|
|
107
|
-
// Mark all non-text nodes as committed (pre-existing facets)
|
|
108
|
-
for (const node of nodes) {
|
|
109
|
-
if (node.type !== 'text') {
|
|
110
|
-
node.committed = true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pos = cursor ?? text.length;
|
|
114
|
-
const detected = detectActiveFacet(nodes, text, pos, this.triggers);
|
|
115
|
-
const active = detected
|
|
116
|
-
? { ...detected, insert: this.insert }
|
|
117
|
-
: null;
|
|
118
|
-
this.text = text;
|
|
119
|
-
this.cursor = pos;
|
|
120
|
-
this.nodes = nodes;
|
|
121
|
-
this.activeFacet = active;
|
|
122
|
-
this.notify();
|
|
123
|
-
};
|
|
124
|
-
insert = (value, options) => {
|
|
125
|
-
const active = this.activeFacet;
|
|
126
|
-
if (!active)
|
|
127
|
-
return;
|
|
128
|
-
const replacement = value + (options?.noTrailingSpace ? '' : ' ');
|
|
129
|
-
const newText = this.text.slice(0, active.range.start) +
|
|
130
|
-
replacement +
|
|
131
|
-
this.text.slice(active.range.end);
|
|
132
|
-
const newCursor = active.range.start + replacement.length;
|
|
133
|
-
this.update(newText, newCursor);
|
|
134
|
-
// Mark the inserted facet as committed
|
|
135
|
-
for (const node of this.nodes) {
|
|
136
|
-
if (node.type !== 'text' && node.start === active.range.start) {
|
|
137
|
-
node.committed = true;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
export function useTapperCore(config) {
|
|
144
|
-
const store = useMemo(() => new Tapper(config), [config]);
|
|
145
|
-
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
146
|
-
return { ...snapshot, store };
|
|
147
|
-
}
|
|
148
|
-
//# sourceMappingURL=core.js.map
|
package/build/core.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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","sourcesContent":["import {useMemo, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperFacetConfigMap,\n TapperNode,\n TapperActiveFacet,\n TapperSnapshot,\n} from './types'\n\nimport {parseNodesFromText, detectActiveFacet, deriveTriggers} from './util'\n\nexport class Tapper {\n private facetConfig: TapperFacetConfigMap\n private triggers: Map<string, string>\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.facetConfig = 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 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 insert is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const nodes = parseNodesFromText(newText, this.facetConfig, this.nodes)\n const prev = this.activeFacet\n const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n if (prev && (!detected || detected.facetType !== prev.facetType)) {\n for (const node of nodes) {\n if (node.type === prev.facetType && node.value === prev.query) {\n node.committed = true\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.cursor = cursor\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\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 !== 'text' &&\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 = (newCursor: number) => {\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 this.cursor = newCursor\n this.update(this.text, newCursor)\n }\n\n replaceText = (text: string, cursor?: number) => {\n const nodes = parseNodesFromText(text, this.facetConfig)\n // Mark all non-text nodes as committed (pre-existing facets)\n for (const node of nodes) {\n if (node.type !== 'text') {\n node.committed = true\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\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n const active = this.activeFacet\n if (!active) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, active.range.start) +\n replacement +\n this.text.slice(active.range.end)\n const newCursor = active.range.start + replacement.length\n\n this.update(newText, newCursor)\n\n // Mark the inserted facet as committed\n for (const node of this.nodes) {\n if (node.type !== 'text' && node.start === active.range.start) {\n node.committed = true\n break\n }\n }\n }\n}\n\nexport function useTapperCore(config: TapperConfig) {\n const store = useMemo(() => new Tapper(config), [config])\n const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot)\n return {...snapshot, store}\n}\n"]}
|
package/build/index.web.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type TapperConfig } from './types';
|
|
2
|
-
export * from './types';
|
|
3
|
-
export * from './core';
|
|
4
|
-
export declare function useTapper(config: TapperConfig): {
|
|
5
|
-
inputProps: {
|
|
6
|
-
ref: import("react").RefObject<HTMLTextAreaElement | null>;
|
|
7
|
-
value: string;
|
|
8
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
9
|
-
onSelect: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;
|
|
10
|
-
};
|
|
11
|
-
store: import("./core").Tapper;
|
|
12
|
-
text: string;
|
|
13
|
-
cursor: number;
|
|
14
|
-
nodes: import("./types").TapperNode[];
|
|
15
|
-
activeFacet: import("./types").TapperActiveFacet | null;
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=index.web.d.ts.map
|
package/build/index.web.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,YAAY,EAAC,MAAM,SAAS,CAAA;AAEzC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;;;;sBAatC,KAAK,CAAC,WAAW,CAAC,mBAAmB,CAAC;sBAQtC,KAAK,CAAC,cAAc,CAAC,mBAAmB,CAAC;;;;;;;EAgBhD"}
|
package/build/index.web.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
-
import { useTapperCore } from './core';
|
|
3
|
-
export * from './types';
|
|
4
|
-
export * from './core';
|
|
5
|
-
export function useTapper(config) {
|
|
6
|
-
const tapper = useTapperCore(config);
|
|
7
|
-
const textareaRef = useRef(null);
|
|
8
|
-
// After atomic deletion (or insert), restore cursor position in the DOM
|
|
9
|
-
useLayoutEffect(() => {
|
|
10
|
-
const ta = textareaRef.current;
|
|
11
|
-
if (ta && ta.selectionStart !== tapper.cursor) {
|
|
12
|
-
ta.setSelectionRange(tapper.cursor, tapper.cursor);
|
|
13
|
-
}
|
|
14
|
-
}, [tapper.cursor, tapper.text]);
|
|
15
|
-
const onChange = useCallback((e) => {
|
|
16
|
-
tapper.store.handleTextChange(e.target.value);
|
|
17
|
-
tapper.store.handleSelectionChange(e.target.selectionStart ?? 0);
|
|
18
|
-
}, [tapper.store]);
|
|
19
|
-
const onSelect = useCallback((e) => {
|
|
20
|
-
const target = e.target;
|
|
21
|
-
tapper.store.handleSelectionChange(target.selectionStart ?? 0);
|
|
22
|
-
}, [tapper.store]);
|
|
23
|
-
return {
|
|
24
|
-
...tapper,
|
|
25
|
-
inputProps: {
|
|
26
|
-
ref: textareaRef,
|
|
27
|
-
value: tapper.text,
|
|
28
|
-
onChange,
|
|
29
|
-
onSelect,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=index.web.js.map
|
package/build/index.web.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAC,MAAM,OAAO,CAAA;AAE1D,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAA;AAGpC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,MAAM,WAAW,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAA;IAErD,wEAAwE;IACxE,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAA;QAC9B,IAAI,EAAE,IAAI,EAAE,CAAC,cAAc,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;YAC9C,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QACpD,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAEhC,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,CAAyC,EAAE,EAAE;QAC5C,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,CAAA;IAClE,CAAC,EACD,CAAC,MAAM,CAAC,KAAK,CAAC,CACf,CAAA;IAED,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,CAA4C,EAAE,EAAE;QAC/C,MAAM,MAAM,GAAG,CAAC,CAAC,MAA6B,CAAA;QAC9C,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,CAAA;IAChE,CAAC,EACD,CAAC,MAAM,CAAC,KAAK,CAAC,CACf,CAAA;IAED,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,QAAQ;YACR,QAAQ;SACT;KACF,CAAA;AACH,CAAC","sourcesContent":["import {useCallback, useLayoutEffect, useRef} from 'react'\n\nimport {useTapperCore} from './core'\nimport {type TapperConfig} from './types'\n\nexport * from './types'\nexport * from './core'\n\nexport function useTapper(config: TapperConfig) {\n const tapper = useTapperCore(config)\n const textareaRef = useRef<HTMLTextAreaElement>(null)\n\n // After atomic deletion (or insert), restore cursor position in the DOM\n useLayoutEffect(() => {\n const ta = textareaRef.current\n if (ta && ta.selectionStart !== tapper.cursor) {\n ta.setSelectionRange(tapper.cursor, tapper.cursor)\n }\n }, [tapper.cursor, tapper.text])\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n tapper.store.handleTextChange(e.target.value)\n tapper.store.handleSelectionChange(e.target.selectionStart ?? 0)\n },\n [tapper.store],\n )\n\n const onSelect = useCallback(\n (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n const target = e.target as HTMLTextAreaElement\n tapper.store.handleSelectionChange(target.selectionStart ?? 0)\n },\n [tapper.store],\n )\n\n return {\n ...tapper,\n inputProps: {\n ref: textareaRef,\n value: tapper.text,\n onChange,\n onSelect,\n },\n }\n}\n"]}
|