@bsky.app/tapper 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.d.ts CHANGED
@@ -1,8 +1,40 @@
1
- import { type TapperConfig } from './types';
1
+ import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
2
2
  export * from './types';
3
- export * from './core';
3
+ export declare class Tapper {
4
+ private facetRegexes;
5
+ private triggers;
6
+ private listeners;
7
+ text: string;
8
+ cursor: number;
9
+ nodes: TapperNode[];
10
+ activeFacet: TapperActiveFacet | null;
11
+ private updating;
12
+ private pendingCursor;
13
+ private storeListeners;
14
+ private snapshot;
15
+ constructor(config: TapperConfig);
16
+ subscribe: (listener: () => void) => () => boolean;
17
+ getSnapshot: () => TapperSnapshot;
18
+ on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
19
+ private emit;
20
+ private notify;
21
+ private update;
22
+ handleTextChange: (newText: string) => void;
23
+ handleSelectionChange: (e: {
24
+ nativeEvent: {
25
+ selection: {
26
+ start: number;
27
+ };
28
+ };
29
+ }) => void;
30
+ replaceText: (text: string, cursor?: number) => void;
31
+ private commit;
32
+ }
4
33
  export declare function useTapper(config: TapperConfig): {
34
+ on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
35
+ focus: () => void | undefined;
5
36
  inputProps: {
37
+ ref: React.RefObject<any>;
6
38
  value: string;
7
39
  selection: {
8
40
  start: number;
@@ -17,10 +49,9 @@ export declare function useTapper(config: TapperConfig): {
17
49
  };
18
50
  }) => void;
19
51
  };
20
- store: import("./core").Tapper;
21
52
  text: string;
22
53
  cursor: number;
23
- nodes: import("./types").TapperNode[];
24
- activeFacet: import("./types").TapperActiveFacet | null;
54
+ nodes: TapperNode[];
55
+ activeFacet: TapperActiveFacet | null;
25
56
  };
26
57
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,YAAY,EAAC,MAAM,SAAS,CAAA;AAEzC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;;;;;;;;+BASjB;YAAC,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SAAC;;;;;;;EAIvE"}
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;AAUhB,cAAc,SAAS,CAAA;AAEvB,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAqB;IAIrC,OAAO,CAAC,SAAS,CAA0D;IAG3E,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,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;IAwDd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,GAAG;QAAC,WAAW,EAAE;YAAC,SAAS,EAAE;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAC,CAAA;SAAC,CAAA;KAAC,UActE;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;SAlLtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aA0LhB,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;;;;;;gCA3GZ,MAAM;+BAyBP;YAAC,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SAAC;;;;;;EAyFxE"}
package/build/index.js CHANGED
@@ -1,15 +1,204 @@
1
- import { useTapperCore } from './core';
1
+ import { useRef, useState, useSyncExternalStore } from 'react';
2
+ import { parseNodesFromText, detectActiveFacet, deriveTriggers, nodeToFacet, compileFacetRegexes, } from './util';
2
3
  export * from './types';
3
- export * from './core';
4
+ export class Tapper {
5
+ facetRegexes;
6
+ triggers;
7
+ // Event emitters
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ listeners = new Map();
10
+ // Public state
11
+ text = '';
12
+ cursor = 0;
13
+ nodes = [];
14
+ activeFacet = null;
15
+ // Internal tracking
16
+ updating = false;
17
+ pendingCursor = null;
18
+ // useSyncExternalStore plumbing
19
+ storeListeners = new Set();
20
+ snapshot;
21
+ constructor(config) {
22
+ this.facetRegexes = compileFacetRegexes(config.facets);
23
+ this.triggers = deriveTriggers(config.facets);
24
+ this.snapshot = {
25
+ text: this.text,
26
+ cursor: this.cursor,
27
+ nodes: this.nodes,
28
+ activeFacet: this.activeFacet,
29
+ };
30
+ if (config.initialText) {
31
+ this.replaceText(config.initialText);
32
+ }
33
+ }
34
+ subscribe = (listener) => {
35
+ this.storeListeners.add(listener);
36
+ return () => this.storeListeners.delete(listener);
37
+ };
38
+ getSnapshot = () => {
39
+ return this.snapshot;
40
+ };
41
+ on = (event, cb) => {
42
+ if (!this.listeners.has(event))
43
+ this.listeners.set(event, new Set());
44
+ this.listeners.get(event).add(cb);
45
+ return () => {
46
+ this.listeners.get(event)?.delete(cb);
47
+ };
48
+ };
49
+ emit(event, data) {
50
+ this.listeners.get(event)?.forEach(cb => cb(data));
51
+ }
52
+ notify() {
53
+ this.snapshot = {
54
+ text: this.text,
55
+ cursor: this.cursor,
56
+ nodes: this.nodes,
57
+ activeFacet: this.activeFacet,
58
+ };
59
+ this.storeListeners.forEach(l => l());
60
+ }
61
+ update(newText, cursor) {
62
+ // guard against circular updates when commit() is called during an update cycle
63
+ if (this.updating)
64
+ return;
65
+ this.updating = true;
66
+ const nodes = newText === this.text
67
+ ? this.nodes
68
+ : parseNodesFromText(newText, this.facetRegexes, this.nodes);
69
+ const prev = this.activeFacet;
70
+ const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
71
+ // When cursor leaves a facet, commit it
72
+ let committedNode = null;
73
+ if (prev && (!detected || detected.type !== prev.type)) {
74
+ for (const node of nodes) {
75
+ if (node.type === prev.type &&
76
+ node.value === prev.value &&
77
+ node.start === prev.range.start &&
78
+ node.end === prev.range.end) {
79
+ node.committed = true;
80
+ committedNode = node;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ // Build activeFacet with commit() baked in
86
+ const active = detected
87
+ ? { ...detected, commit: this.commit }
88
+ : null;
89
+ this.text = newText;
90
+ this.cursor = cursor;
91
+ this.nodes = nodes;
92
+ this.activeFacet = active;
93
+ this.updating = false;
94
+ this.notify();
95
+ // Fire events after state is finalized
96
+ const facetChanged = active?.type !== prev?.type ||
97
+ active?.value !== prev?.value ||
98
+ active?.range.start !== prev?.range.start ||
99
+ active?.range.end !== prev?.range.end;
100
+ if (facetChanged) {
101
+ this.emit('activeFacet', active);
102
+ }
103
+ if (committedNode) {
104
+ this.emit('facetCommitted', nodeToFacet(committedNode));
105
+ }
106
+ }
107
+ handleTextChange = (newText) => {
108
+ const diff = newText.length - this.text.length;
109
+ let newCursor = Math.max(this.cursor + diff, 0);
110
+ // Atomic deletion: backspace into a facet from outside
111
+ if (diff === -1 && newCursor < this.cursor) {
112
+ for (const node of this.nodes) {
113
+ if (node.type !== 'text' &&
114
+ node.committed &&
115
+ this.cursor === node.end) {
116
+ const remnant = node.end - node.start - 1;
117
+ newText =
118
+ newText.slice(0, node.start) + newText.slice(node.start + remnant);
119
+ newCursor = node.start;
120
+ this.pendingCursor = newCursor;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ this.update(newText, newCursor);
126
+ };
127
+ handleSelectionChange = (e) => {
128
+ const newCursor = e.nativeEvent.selection.start;
129
+ // After atomic deletion, ignore stale cursor values from the DOM
130
+ if (this.pendingCursor !== null) {
131
+ if (newCursor !== this.pendingCursor) {
132
+ return;
133
+ }
134
+ this.pendingCursor = null;
135
+ }
136
+ if (newCursor === this.cursor)
137
+ return;
138
+ this.cursor = newCursor;
139
+ this.update(this.text, newCursor);
140
+ };
141
+ replaceText = (text, cursor) => {
142
+ const prevActive = this.activeFacet;
143
+ const nodes = parseNodesFromText(text, this.facetRegexes);
144
+ // Mark all non-text nodes as committed (pre-existing facets)
145
+ const newlyCommitted = [];
146
+ for (const node of nodes) {
147
+ if (node.type !== 'text') {
148
+ node.committed = true;
149
+ newlyCommitted.push(node);
150
+ }
151
+ }
152
+ const pos = cursor ?? text.length;
153
+ const detected = detectActiveFacet(nodes, text, pos, this.triggers);
154
+ const active = detected
155
+ ? { ...detected, commit: this.commit }
156
+ : null;
157
+ this.text = text;
158
+ this.cursor = pos;
159
+ this.nodes = nodes;
160
+ this.activeFacet = active;
161
+ this.notify();
162
+ if (active !== prevActive) {
163
+ this.emit('activeFacet', active);
164
+ }
165
+ for (const node of newlyCommitted) {
166
+ this.emit('facetCommitted', nodeToFacet(node));
167
+ }
168
+ };
169
+ commit = (value, options) => {
170
+ if (!this.activeFacet)
171
+ return;
172
+ const replacement = value + (options?.noTrailingSpace ? '' : ' ');
173
+ const newText = this.text.slice(0, this.activeFacet.range.start) +
174
+ replacement +
175
+ this.text.slice(this.activeFacet.range.end);
176
+ const newCursor = this.activeFacet.range.start + replacement.length;
177
+ if (this.activeFacet) {
178
+ this.activeFacet.value = value;
179
+ this.activeFacet.range = {
180
+ start: this.activeFacet.range.start,
181
+ end: this.activeFacet.range.start + value.length,
182
+ };
183
+ this.emit('afterInsert', this.activeFacet);
184
+ }
185
+ this.update(newText, newCursor);
186
+ };
187
+ }
4
188
  export function useTapper(config) {
5
- const tapper = useTapperCore(config);
189
+ const [store] = useState(() => new Tapper(config));
190
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
191
+ const inputRef = useRef(null);
6
192
  return {
7
- ...tapper,
193
+ ...state,
194
+ on: store.on,
195
+ focus: () => inputRef.current?.focus(),
8
196
  inputProps: {
9
- value: tapper.text,
10
- selection: { start: tapper.cursor, end: tapper.cursor },
11
- onChangeText: tapper.store.handleTextChange,
12
- onSelectionChange: (e) => tapper.store.handleSelectionChange(e.nativeEvent.selection.start),
197
+ ref: inputRef,
198
+ value: state.text,
199
+ selection: { start: state.cursor, end: state.cursor },
200
+ onChangeText: store.handleTextChange,
201
+ onSelectionChange: store.handleSelectionChange,
13
202
  },
14
203
  };
15
204
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,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;IAEpC,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,SAAS,EAAE,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAC;YACrD,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,gBAAgB;YAC3C,iBAAiB,EAAE,CAAC,CAA8C,EAAE,EAAE,CACpE,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC;SACpE;KACF,CAAA;AACH,CAAC","sourcesContent":["import {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\n return {\n ...tapper,\n inputProps: {\n value: tapper.text,\n selection: {start: tapper.cursor, end: tapper.cursor},\n onChangeText: tapper.store.handleTextChange,\n onSelectionChange: (e: {nativeEvent: {selection: {start: number}}}) =>\n tapper.store.handleSelectionChange(e.nativeEvent.selection.start),\n },\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAC,MAAM,OAAO,CAAA;AAS5D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,mBAAmB,GAEpB,MAAM,QAAQ,CAAA;AAEf,cAAc,SAAS,CAAA;AAEvB,MAAM,OAAO,MAAM;IACT,YAAY,CAAsB;IAClC,QAAQ,CAAqB;IAErC,iBAAiB;IACjB,8DAA8D;IACtD,SAAS,GAAG,IAAI,GAAG,EAAgD,CAAA;IAE3E,eAAe;IACf,IAAI,GAAG,EAAE,CAAA;IACT,MAAM,GAAG,CAAC,CAAA;IACV,KAAK,GAAiB,EAAE,CAAA;IACxB,WAAW,GAA6B,IAAI,CAAA;IAE5C,oBAAoB;IACZ,QAAQ,GAAG,KAAK,CAAA;IAChB,aAAa,GAAkB,IAAI,CAAA;IAE3C,gCAAgC;IACxB,cAAc,GAAG,IAAI,GAAG,EAAc,CAAA;IACtC,QAAQ,CAAgB;IAEhC,YAAY,MAAoB;QAC9B,IAAI,CAAC,YAAY,GAAG,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC7C,IAAI,CAAC,QAAQ,GAAG;YACd,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAA;QACD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QACtC,CAAC;IACH,CAAC;IAED,SAAS,GAAG,CAAC,QAAoB,EAAE,EAAE;QACnC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACjC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC,CAAA;IAED,WAAW,GAAG,GAAmB,EAAE;QACjC,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC,CAAA;IAED,EAAE,GAAG,CACH,KAAQ,EACR,EAAmC,EACnC,EAAE;QACF,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAA;QACpE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAClC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;QACvC,CAAC,CAAA;IACH,CAAC,CAAA;IAEO,IAAI,CAA+B,KAAQ,EAAE,IAAqB;QACxE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,QAAQ,GAAG;YACd,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAA;QACD,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC;IAEO,MAAM,CAAC,OAAe,EAAE,MAAc;QAC5C,gFAAgF;QAChF,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QAEpB,MAAM,KAAK,GACT,OAAO,KAAK,IAAI,CAAC,IAAI;YACnB,CAAC,CAAC,IAAI,CAAC,KAAK;YACZ,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAA;QAC7B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QAEzE,wCAAwC;QACxC,IAAI,aAAa,GAAsB,IAAI,CAAA;QAC3C,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IACE,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;oBACvB,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK;oBACzB,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK;oBAC/B,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,GAAG,EAC3B,CAAC;oBACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;oBACrB,aAAa,GAAG,IAAI,CAAA;oBACpB,MAAK;gBACP,CAAC;YACH,CAAC;QACH,CAAC;QAED,2CAA2C;QAC3C,MAAM,MAAM,GAA6B,QAAQ;YAC/C,CAAC,CAAC,EAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC;YACpC,CAAC,CAAC,IAAI,CAAA;QAER,IAAI,CAAC,IAAI,GAAG,OAAO,CAAA;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAA;QAEzB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC,MAAM,EAAE,CAAA;QAEb,uCAAuC;QACvC,MAAM,YAAY,GAChB,MAAM,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI;YAC3B,MAAM,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK;YAC7B,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC,KAAK;YACzC,MAAM,EAAE,KAAK,CAAC,GAAG,KAAK,IAAI,EAAE,KAAK,CAAC,GAAG,CAAA;QACvC,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;QAClC,CAAC;QACD,IAAI,aAAa,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC,CAAA;QACzD,CAAC;IACH,CAAC;IAED,gBAAgB,GAAG,CAAC,OAAe,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAA;QAC9C,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QAE/C,uDAAuD;QACvD,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC9B,IACE,IAAI,CAAC,IAAI,KAAK,MAAM;oBACpB,IAAI,CAAC,SAAS;oBACd,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,GAAG,EACxB,CAAC;oBACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;oBACzC,OAAO;wBACL,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,CAAA;oBACpE,SAAS,GAAG,IAAI,CAAC,KAAK,CAAA;oBACtB,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;oBAC9B,MAAK;gBACP,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACjC,CAAC,CAAA;IAED,qBAAqB,GAAG,CAAC,CAA8C,EAAE,EAAE;QACzE,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAA;QAC/C,iEAAiE;QACjE,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAChC,IAAI,SAAS,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrC,OAAM;YACR,CAAC;YACD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;QAC3B,CAAC;QAED,IAAI,SAAS,KAAK,IAAI,CAAC,MAAM;YAAE,OAAM;QAErC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IACnC,CAAC,CAAA;IAED,WAAW,GAAG,CAAC,IAAY,EAAE,MAAe,EAAE,EAAE;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAA;QACnC,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QACzD,6DAA6D;QAC7D,MAAM,cAAc,GAAiB,EAAE,CAAA;QACvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACzB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;gBACrB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;QACjC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QACnE,MAAM,MAAM,GAA6B,QAAQ;YAC/C,CAAC,CAAC,EAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC;YACpC,CAAC,CAAC,IAAI,CAAA;QAER,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAA;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAA;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAA;QAEb,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;QAClC,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAA;QAChD,CAAC;IACH,CAAC,CAAA;IAEO,MAAM,GAAG,CAAC,KAAa,EAAE,OAAqC,EAAE,EAAE;QACxE,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAM;QAE7B,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACjE,MAAM,OAAO,GACX,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC;YAChD,WAAW;YACX,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,GAAG,WAAW,CAAC,MAAM,CAAA;QAEnE,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,IAAI,CAAC,WAAW,CAAC,KAAK,GAAG,KAAK,CAAA;YAC9B,IAAI,CAAC,WAAW,CAAC,KAAK,GAAG;gBACvB,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK;gBACnC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM;aACjD,CAAA;YACD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QAC5C,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACjC,CAAC,CAAA;CACF;AAED,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,MAAM,KAAK,GAAG,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACtE,MAAM,QAAQ,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAA;IAE9C,OAAO;QACL,GAAG,KAAK;QACR,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE;QACtC,UAAU,EAAE;YACV,GAAG,EAAE,QAAgC;YACrC,KAAK,EAAE,KAAK,CAAC,IAAI;YACjB,SAAS,EAAE,EAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,EAAC;YACnD,YAAY,EAAE,KAAK,CAAC,gBAAgB;YACpC,iBAAiB,EAAE,KAAK,CAAC,qBAAqB;SAC/C;KACF,CAAA;AACH,CAAC","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(newText, this.facetRegexes, 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 let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === prev.type &&\n node.value === prev.value &&\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, commit: this.commit}\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 !== '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 = (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 !== 'text') {\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, commit: this.commit}\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 commit = (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"]}
package/build/types.d.ts CHANGED
@@ -10,15 +10,15 @@ export type TapperNode = {
10
10
  committed?: boolean;
11
11
  };
12
12
  export type TapperFacet = {
13
- facetType: string;
14
- query: string;
13
+ type: string;
14
+ value: string;
15
15
  range: {
16
16
  start: number;
17
17
  end: number;
18
18
  };
19
19
  };
20
20
  export type TapperActiveFacet = TapperFacet & {
21
- insert: (value: string, options?: {
21
+ commit: (value: string, options?: {
22
22
  noTrailingSpace?: boolean;
23
23
  }) => void;
24
24
  };
@@ -32,4 +32,9 @@ export type TapperConfig = {
32
32
  facets: TapperFacetConfigMap;
33
33
  initialText?: string;
34
34
  };
35
+ export type TapperEvents = {
36
+ activeFacet: TapperActiveFacet | null;
37
+ facetCommitted: TapperFacet;
38
+ afterInsert: TapperFacet;
39
+ };
35
40
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,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,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,oBAAoB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,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,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,oBAAoB,CAAA;IAC5B,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = {\n match: RegExp\n}\n\nexport type TapperFacetConfigMap = Record<string, TapperFacetConfig>\n\nexport type TapperNode = {\n type: string\n value: string\n start: number\n end: number\n committed?: boolean\n}\n\nexport type TapperFacet = {\n facetType: string\n query: 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 TapperSnapshot = {\n text: string\n cursor: number\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets: TapperFacetConfigMap\n initialText?: string\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = {\n match: RegExp\n}\n\nexport type TapperFacetConfigMap = Record<string, TapperFacetConfig>\n\nexport type TapperNode = {\n type: string\n value: string\n start: number\n end: number\n committed?: boolean\n}\n\nexport type TapperFacet = {\n type: string\n value: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n commit: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSnapshot = {\n text: string\n cursor: number\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets: TapperFacetConfigMap\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,5 +1,14 @@
1
1
  import { TapperFacetConfigMap, TapperNode, TapperFacet } from './types';
2
- export declare function parseNodesFromText(text: string, config: TapperFacetConfigMap, prevNodes?: TapperNode[]): TapperNode[];
2
+ export type CompiledFacetRegexes = Map<string, RegExp>;
3
+ /**
4
+ * Pre-compile facet regexes once at init time. This avoids re-creating
5
+ * RegExp objects on every keystroke in parseNodesFromText. Each Tapper
6
+ * instance gets its own compiled copy so lastIndex state can't leak
7
+ * between instances.
8
+ */
9
+ export declare function compileFacetRegexes(config: TapperFacetConfigMap): CompiledFacetRegexes;
10
+ export declare function parseNodesFromText(text: string, regexes: CompiledFacetRegexes, prevNodes?: TapperNode[]): TapperNode[];
3
11
  export declare function deriveTriggers(config: TapperFacetConfigMap): Map<string, string>;
12
+ export declare function nodeToFacet(node: TapperNode): TapperFacet;
4
13
  export declare function detectActiveFacet(nodes: TapperNode[], text: string, cursor: number, triggers: Map<string, string>): TapperFacet | null;
5
14
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,WAAW,EACZ,MAAM,SAAS,CAAA;AAEhB,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,SAAS,CAAC,EAAE,UAAU,EAAE,GACvB,UAAU,EAAE,CAyFd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOhF;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,CA2BpB"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;AAIrE,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,GAC3B,oBAAoB,CAMtB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,SAAS,CAAC,EAAE,UAAU,EAAE,GACvB,UAAU,EAAE,CA2Fd;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,oBAAoB,GAC3B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOrB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAMzD;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,CA2BpB"}
package/build/util.js CHANGED
@@ -1,7 +1,23 @@
1
- export function parseNodesFromText(text, config, prevNodes) {
2
- const allMatches = [];
1
+ const WHITESPACE = /\s/;
2
+ /**
3
+ * Pre-compile facet regexes once at init time. This avoids re-creating
4
+ * RegExp objects on every keystroke in parseNodesFromText. Each Tapper
5
+ * instance gets its own compiled copy so lastIndex state can't leak
6
+ * between instances.
7
+ */
8
+ export function compileFacetRegexes(config) {
9
+ const compiled = new Map();
3
10
  for (const [name, def] of Object.entries(config)) {
4
- const re = new RegExp(def.match.source, def.match.flags);
11
+ compiled.set(name, new RegExp(def.match.source, def.match.flags));
12
+ }
13
+ return compiled;
14
+ }
15
+ export function parseNodesFromText(text, regexes, prevNodes) {
16
+ const allMatches = [];
17
+ for (const [name, re] of regexes) {
18
+ // Reset lastIndex so stateful (global) regexes don't carry over
19
+ // match positions from the previous parse call.
20
+ re.lastIndex = 0;
5
21
  for (const m of text.matchAll(re)) {
6
22
  allMatches.push({
7
23
  facetName: name,
@@ -82,12 +98,19 @@ export function deriveTriggers(config) {
82
98
  }
83
99
  return triggers;
84
100
  }
101
+ export function nodeToFacet(node) {
102
+ return {
103
+ type: node.type,
104
+ value: node.value,
105
+ range: { start: node.start, end: node.end },
106
+ };
107
+ }
85
108
  export function detectActiveFacet(nodes, text, cursor, triggers) {
86
109
  for (const node of nodes) {
87
110
  if (node.type !== 'text' && node.start < cursor && cursor <= node.end) {
88
111
  return {
89
- facetType: node.type,
90
- query: node.value,
112
+ type: node.type,
113
+ value: node.value,
91
114
  range: { start: node.start, end: node.end },
92
115
  };
93
116
  }
@@ -96,13 +119,13 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
96
119
  for (let i = cursor - 1; i >= 0; i--) {
97
120
  const ch = text[i];
98
121
  // Stop at whitespace — triggers don't span across words
99
- if (/\s/.test(ch))
122
+ if (WHITESPACE.test(ch))
100
123
  break;
101
- const facetType = triggers.get(ch);
102
- if (facetType) {
124
+ const type = triggers.get(ch);
125
+ if (type) {
103
126
  return {
104
- facetType,
105
- query: text.slice(i + 1, cursor),
127
+ type,
128
+ value: text.slice(i + 1, cursor),
106
129
  range: { start: i, end: cursor },
107
130
  };
108
131
  }
package/build/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,MAA4B,EAC5B,SAAwB;IAExB,MAAM,UAAU,GAKV,EAAE,CAAA;IAER,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAClC,UAAU,CAAC,IAAI,CAAC;gBACd,SAAS,EAAE,IAAI;gBACf,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;gBACf,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;gBACb,KAAK,EAAE,CAAC,CAAC,KAAK;aACf,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,UAAU,CAAC,IAAI,CACb,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CACvE,CAAA;IAED,MAAM,QAAQ,GAAsB,EAAE,CAAA;IACtC,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YAChB,OAAO,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAA;QACxC,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAA;IAC9B,IAAI,MAAM,GAAG,CAAC,CAAA;IAEd,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,MAAM;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,MAAM;gBACb,GAAG,EAAE,CAAC,CAAC,KAAK;aACb,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,CAAC,CAAC,SAAS;YACjB,KAAK,EAAE,CAAC,CAAC,OAAO;YAChB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM;SAClC,CAAC,CAAA;QACF,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAA;IACvC,CAAC;IAED,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YACzB,KAAK,EAAE,MAAM;YACb,GAAG,EAAE,IAAI,CAAC,MAAM;SACjB,CAAC,CAAA;IACJ,CAAC;IAED,0EAA0E;IAC1E,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;QACxC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAA;QAE9C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAQ;YAClC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;YAC9B,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACnB,oBAAoB,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,CAAA;YACjD,CAAC;QACH,CAAC;QAED,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAQ;YAClC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACtC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;YAC9B,IAAI,oBAAoB,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC;gBACpD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAA4B;IACzD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAA;QACxD,IAAI,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACjC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAmB,EACnB,IAAY,EACZ,MAAc,EACd,QAA6B;IAE7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,GAAG,MAAM,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACtE,OAAO;gBACL,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAC;aAC1C,CAAA;QACH,CAAC;IACH,CAAC;IAED,wFAAwF;IACxF,KAAK,IAAI,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QAClB,wDAAwD;QACxD,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,MAAK;QACxB,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAClC,IAAI,SAAS,EAAE,CAAC;YACd,OAAO;gBACL,SAAS;gBACT,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;gBAChC,KAAK,EAAE,EAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAC;aAC/B,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import {\n TapperFacetConfigMap,\n TapperNode,\n TapperFacet,\n} from './types'\n\nexport function parseNodesFromText(\n text: string,\n config: TapperFacetConfigMap,\n prevNodes?: TapperNode[],\n): TapperNode[] {\n const allMatches: {\n facetName: string\n fullMatch: string\n capture: string\n index: number\n }[] = []\n\n for (const [name, def] of Object.entries(config)) {\n const re = new RegExp(def.match.source, def.match.flags)\n for (const m of text.matchAll(re)) {\n allMatches.push({\n facetName: name,\n fullMatch: m[0],\n capture: 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 cursor = 0\n\n for (const m of accepted) {\n if (m.index > cursor) {\n nodes.push({\n type: 'text',\n value: text.slice(cursor, m.index),\n start: cursor,\n end: m.index,\n })\n }\n nodes.push({\n type: m.facetName,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n cursor = m.index + m.fullMatch.length\n }\n\n if (cursor < text.length) {\n nodes.push({\n type: 'text',\n value: text.slice(cursor),\n start: cursor,\n end: text.length,\n })\n }\n\n // Transfer committed flags from previous nodes by type + 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 === 'text') continue\n const idx = counts.get(node.type) ?? 0\n counts.set(node.type, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.type}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type === 'text') continue\n const idx = counts.get(node.type) ?? 0\n counts.set(node.type, idx + 1)\n if (committedByTypeIndex.has(`${node.type}:${idx}`)) {\n node.committed = true\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(config: TapperFacetConfigMap): 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 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 !== 'text' && node.start < cursor && cursor <= node.end) {\n return {\n facetType: node.type,\n query: 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 (/\\s/.test(ch)) break\n const facetType = triggers.get(ch)\n if (facetType) {\n return {\n facetType,\n query: 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 {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): 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[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 cursor = 0\n\n for (const m of accepted) {\n if (m.index > cursor) {\n nodes.push({\n type: 'text',\n value: text.slice(cursor, m.index),\n start: cursor,\n end: m.index,\n })\n }\n nodes.push({\n type: m.facetName,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n cursor = m.index + m.fullMatch.length\n }\n\n if (cursor < text.length) {\n nodes.push({\n type: 'text',\n value: text.slice(cursor),\n start: cursor,\n end: text.length,\n })\n }\n\n // Transfer committed flags from previous nodes by type + 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 === 'text') continue\n const idx = counts.get(node.type) ?? 0\n counts.set(node.type, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.type}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type === 'text') continue\n const idx = counts.get(node.type) ?? 0\n counts.set(node.type, idx + 1)\n if (committedByTypeIndex.has(`${node.type}:${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.type,\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 !== 'text' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.type,\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsky.app/tapper",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A minimal rich text editor for React Native and web.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",