@bsky.app/tapper 0.2.0 → 0.2.3
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 +19 -0
- package/README.md +204 -1
- 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 +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -11
- package/build/index.js.map +1 -1
- package/build/types.d.ts +22 -7
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/util.d.ts +4 -4
- package/build/util.d.ts.map +1 -1
- package/build/util.js +90 -27
- package/build/util.js.map +1 -1
- package/package.json +15 -15
- package/src/facets.ts +6 -0
- package/src/index.ts +21 -12
- package/src/types.ts +31 -14
- package/src/util.ts +96 -31
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @bsky.app/tapper
|
|
2
|
+
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`5952994`](https://github.com/bluesky-social/toolbox/commit/59529944ec895f43b30928b078edd5f3868753ae) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adjust tapper facet config
|
|
8
|
+
|
|
9
|
+
## 0.2.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`255ae92`](https://github.com/bluesky-social/toolbox/commit/255ae923d552c12b6ad36dedde37a9bf44d02866) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump sift/tapper for publish
|
|
14
|
+
|
|
15
|
+
## 0.2.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [`06c8141`](https://github.com/bluesky-social/toolbox/commit/06c81411f8420240c8c794d41398df034c514fda) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump packages
|
package/README.md
CHANGED
|
@@ -1,3 +1,206 @@
|
|
|
1
1
|
# @bsky.app/tapper
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A facet-aware text input engine for React Native (including web). Parses
|
|
4
|
+
configurable patterns (mentions, emoji, tags, URLs) from text as the user types,
|
|
5
|
+
producing a node list you render as a colored preview overlay on top of a
|
|
6
|
+
transparent `TextInput`.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @bsky.app/tapper
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Peer dependencies: `react`, `react-native`.
|
|
15
|
+
|
|
16
|
+
## Basic example
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {useEffect} from 'react'
|
|
20
|
+
import {View, Text, TextInput} from 'react-native'
|
|
21
|
+
import {useTapper} from '@bsky.app/tapper'
|
|
22
|
+
|
|
23
|
+
function Composer() {
|
|
24
|
+
const {text, nodes, inputProps, focus, on} = useTapper({
|
|
25
|
+
facets: {
|
|
26
|
+
mention: /@([a-zA-Z0-9._]+)/g,
|
|
27
|
+
emoji: /:([a-zA-Z0-9_]+):/g,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Re-focus after an autocomplete insertion
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
return on('afterInsert', () => focus())
|
|
34
|
+
}, [on, focus])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={{position: 'relative'}}>
|
|
38
|
+
{/* Preview overlay — renders colored facets */}
|
|
39
|
+
<View
|
|
40
|
+
style={{position: 'absolute', top: 0, left: 0, right: 0, bottom: 0}}>
|
|
41
|
+
<Text>
|
|
42
|
+
{nodes.map((node, i) => (
|
|
43
|
+
<Text
|
|
44
|
+
key={i}
|
|
45
|
+
style={{
|
|
46
|
+
color:
|
|
47
|
+
node.type === 'facet'
|
|
48
|
+
? '#2e7de9'
|
|
49
|
+
: node.type === 'trigger'
|
|
50
|
+
? '#999'
|
|
51
|
+
: '#000',
|
|
52
|
+
}}>
|
|
53
|
+
{node.raw}
|
|
54
|
+
</Text>
|
|
55
|
+
))}
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
{/* Actual input — transparent text, visible caret */}
|
|
60
|
+
<TextInput
|
|
61
|
+
{...inputProps}
|
|
62
|
+
multiline
|
|
63
|
+
style={{
|
|
64
|
+
color: 'transparent',
|
|
65
|
+
caretColor: '#000',
|
|
66
|
+
}}
|
|
67
|
+
placeholder="Type here..."
|
|
68
|
+
/>
|
|
69
|
+
</View>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The preview overlay and `TextInput` must share the same text styles (font
|
|
75
|
+
family, size, line height, padding) so they align exactly.
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
`useTapper` returns:
|
|
80
|
+
|
|
81
|
+
| Property | Description |
|
|
82
|
+
| ------------- | --------------------------------------------------- |
|
|
83
|
+
| `text` | Current text value |
|
|
84
|
+
| `nodes` | Parsed node list for rendering |
|
|
85
|
+
| `activeFacet` | The facet the cursor is currently inside, or `null` |
|
|
86
|
+
| `inputProps` | Spread onto your `TextInput` |
|
|
87
|
+
| `on` | Subscribe to events |
|
|
88
|
+
| `focus` | Re-focus the input (useful after `commit`) |
|
|
89
|
+
|
|
90
|
+
### Nodes
|
|
91
|
+
|
|
92
|
+
Each node has a `type` discriminant:
|
|
93
|
+
|
|
94
|
+
- **`'text'`** — plain text, render as-is.
|
|
95
|
+
- **`'trigger'`** — the user just typed a trigger character (e.g. `@`) but
|
|
96
|
+
hasn't typed any content yet. Has `facetType` so you know which kind.
|
|
97
|
+
- **`'facet'`** — a matched pattern. Has `facetType` (e.g. `'mention'`,
|
|
98
|
+
`'emoji'`).
|
|
99
|
+
|
|
100
|
+
All nodes have `raw` (the full matched text including trigger) and `value`
|
|
101
|
+
(content only, trigger stripped). For display, use `node.raw`.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// Typing "@eric" produces:
|
|
105
|
+
{type: 'facet', facetType: 'mention', raw: '@eric', value: 'eric', start: 0, end: 5}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Active facet
|
|
109
|
+
|
|
110
|
+
When the cursor is inside a facet or right after a trigger character,
|
|
111
|
+
`activeFacet` is non-null:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
activeFacet: {
|
|
115
|
+
type: 'mention', // the facet type
|
|
116
|
+
value: 'er', // content after trigger (no @)
|
|
117
|
+
range: {start: 0, end: 3},
|
|
118
|
+
insert: (value, options?) => void,
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Call `activeFacet.insert('@eric')` to replace the in-progress facet with a
|
|
123
|
+
final value. A trailing space is appended by default; pass
|
|
124
|
+
`{noTrailingSpace: true}` to suppress it.
|
|
125
|
+
|
|
126
|
+
## Emoji auto-commit example
|
|
127
|
+
|
|
128
|
+
A common pattern: when the user types a closing `:` on an emoji shortcode
|
|
129
|
+
(e.g. `:wave:`), immediately replace it with the emoji character.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
const {on, activeFacet, focus} = useTapper({
|
|
133
|
+
facets: {
|
|
134
|
+
emoji: /:([a-zA-Z0-9_]+):/g,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const EMOJI: Record<string, string> = {
|
|
139
|
+
wave: '👋',
|
|
140
|
+
tada: '🎉',
|
|
141
|
+
v: '✌️',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
return on('activeFacet', facet => {
|
|
146
|
+
if (facet?.type === 'emoji' && facet.value.endsWith(':')) {
|
|
147
|
+
const name = facet.value.slice(0, -1) // strip closing ":"
|
|
148
|
+
const emoji = EMOJI[name]
|
|
149
|
+
if (emoji) {
|
|
150
|
+
facet.insert(emoji, {noTrailingSpace: true})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}, [on])
|
|
155
|
+
|
|
156
|
+
// Re-focus after insertion
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
return on('afterInsert', () => focus())
|
|
159
|
+
}, [on, focus])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Events
|
|
163
|
+
|
|
164
|
+
Subscribe with `on(event, callback)`. Returns an unsubscribe function — use it
|
|
165
|
+
in a `useEffect` cleanup.
|
|
166
|
+
|
|
167
|
+
| Event | Data | When |
|
|
168
|
+
| ---------------- | --------------------------- | ----------------------------------------------- |
|
|
169
|
+
| `activeFacet` | `TapperActiveFacet \| null` | Cursor enters or leaves a facet |
|
|
170
|
+
| `facetCommitted` | `TapperFacet` | A facet is finalized (cursor left or committed) |
|
|
171
|
+
| `afterInsert` | `TapperFacet` | After `insert()` replaces text |
|
|
172
|
+
|
|
173
|
+
### `focus()`
|
|
174
|
+
|
|
175
|
+
After `insert()` replaces text, the input may lose focus on some platforms.
|
|
176
|
+
Listen for `afterInsert` and call `focus()` to restore it:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
return on('afterInsert', () => focus())
|
|
181
|
+
}, [on, focus])
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Atomic deletion
|
|
185
|
+
|
|
186
|
+
Committed facets are deleted atomically — backspacing into a committed facet
|
|
187
|
+
from outside removes the entire facet in one keystroke, rather than deleting
|
|
188
|
+
character by character.
|
|
189
|
+
|
|
190
|
+
## `initialText`
|
|
191
|
+
|
|
192
|
+
Pre-populate the input with text. Any matched facets are automatically marked
|
|
193
|
+
as committed.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
const {nodes} = useTapper({
|
|
197
|
+
facets: {mention: /@([a-zA-Z0-9._]+)/g},
|
|
198
|
+
initialText: 'Hello @eric',
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## `replaceText`
|
|
203
|
+
|
|
204
|
+
The `Tapper` class (used internally by `useTapper`) exposes `replaceText(text,
|
|
205
|
+
cursor?)` for programmatic text replacement. All matched facets in the new text
|
|
206
|
+
are marked as committed.
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, 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;
|
|
@@ -12,7 +13,7 @@ export declare class Tapper {
|
|
|
12
13
|
private pendingCursor;
|
|
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;
|
|
@@ -28,7 +29,7 @@ export declare class Tapper {
|
|
|
28
29
|
};
|
|
29
30
|
}) => void;
|
|
30
31
|
replaceText: (text: string, cursor?: number) => void;
|
|
31
|
-
private
|
|
32
|
+
private insert;
|
|
32
33
|
}
|
|
33
34
|
export declare function useTapper(config: TapperConfig): {
|
|
34
35
|
on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
|
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,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,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,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;IA8Dd,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;SAxLtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aAgMhB,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,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;
|
|
@@ -19,15 +21,16 @@ export class Tapper {
|
|
|
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
|
cursor: this.cursor,
|
|
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
|
}
|
|
@@ -65,15 +68,15 @@ export class Tapper {
|
|
|
65
68
|
this.updating = true;
|
|
66
69
|
const nodes = newText === this.text
|
|
67
70
|
? this.nodes
|
|
68
|
-
: parseNodesFromText(newText, this.facetRegexes, this.nodes);
|
|
71
|
+
: parseNodesFromText(newText, this.facetRegexes, this.nodes, cursor, this.triggers);
|
|
69
72
|
const prev = this.activeFacet;
|
|
70
73
|
const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
|
|
71
74
|
// When cursor leaves a facet, commit it
|
|
72
75
|
let committedNode = null;
|
|
73
76
|
if (prev && (!detected || detected.type !== prev.type)) {
|
|
74
77
|
for (const node of nodes) {
|
|
75
|
-
if (node.type ===
|
|
76
|
-
node.
|
|
78
|
+
if (node.type === 'facet' &&
|
|
79
|
+
node.facetType === prev.type &&
|
|
77
80
|
node.start === prev.range.start &&
|
|
78
81
|
node.end === prev.range.end) {
|
|
79
82
|
node.committed = true;
|
|
@@ -84,7 +87,7 @@ export class Tapper {
|
|
|
84
87
|
}
|
|
85
88
|
// Build activeFacet with commit() baked in
|
|
86
89
|
const active = detected
|
|
87
|
-
? { ...detected,
|
|
90
|
+
? { ...detected, insert: this.insert }
|
|
88
91
|
: null;
|
|
89
92
|
this.text = newText;
|
|
90
93
|
this.cursor = cursor;
|
|
@@ -110,7 +113,7 @@ export class Tapper {
|
|
|
110
113
|
// Atomic deletion: backspace into a facet from outside
|
|
111
114
|
if (diff === -1 && newCursor < this.cursor) {
|
|
112
115
|
for (const node of this.nodes) {
|
|
113
|
-
if (node.type
|
|
116
|
+
if (node.type === 'facet' &&
|
|
114
117
|
node.committed &&
|
|
115
118
|
this.cursor === node.end) {
|
|
116
119
|
const remnant = node.end - node.start - 1;
|
|
@@ -144,7 +147,7 @@ export class Tapper {
|
|
|
144
147
|
// Mark all non-text nodes as committed (pre-existing facets)
|
|
145
148
|
const newlyCommitted = [];
|
|
146
149
|
for (const node of nodes) {
|
|
147
|
-
if (node.type
|
|
150
|
+
if (node.type === 'facet') {
|
|
148
151
|
node.committed = true;
|
|
149
152
|
newlyCommitted.push(node);
|
|
150
153
|
}
|
|
@@ -152,7 +155,7 @@ export class Tapper {
|
|
|
152
155
|
const pos = cursor ?? text.length;
|
|
153
156
|
const detected = detectActiveFacet(nodes, text, pos, this.triggers);
|
|
154
157
|
const active = detected
|
|
155
|
-
? { ...detected,
|
|
158
|
+
? { ...detected, insert: this.insert }
|
|
156
159
|
: null;
|
|
157
160
|
this.text = text;
|
|
158
161
|
this.cursor = pos;
|
|
@@ -166,7 +169,7 @@ export class Tapper {
|
|
|
166
169
|
this.emit('facetCommitted', nodeToFacet(node));
|
|
167
170
|
}
|
|
168
171
|
};
|
|
169
|
-
|
|
172
|
+
insert = (value, options) => {
|
|
170
173
|
if (!this.activeFacet)
|
|
171
174
|
return;
|
|
172
175
|
const replacement = value + (options?.noTrailingSpace ? '' : ' ');
|
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(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"]}
|
|
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'\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 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 const facets = config?.facets || defaultFacets\n this.facetRegexes = compileFacetRegexes(facets)\n this.triggers = deriveTriggers(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"]}
|
package/build/types.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
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
|
-
type:
|
|
3
|
+
type: 'text';
|
|
4
|
+
raw: string;
|
|
5
|
+
value: string;
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
committed?: boolean;
|
|
9
|
+
facetType?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'trigger';
|
|
12
|
+
raw: string;
|
|
13
|
+
value: string;
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
committed?: boolean;
|
|
17
|
+
facetType: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'facet';
|
|
20
|
+
raw: string;
|
|
7
21
|
value: string;
|
|
8
22
|
start: number;
|
|
9
23
|
end: number;
|
|
10
24
|
committed?: boolean;
|
|
25
|
+
facetType: string;
|
|
11
26
|
};
|
|
12
27
|
export type TapperFacet = {
|
|
13
28
|
type: string;
|
|
@@ -18,7 +33,7 @@ export type TapperFacet = {
|
|
|
18
33
|
};
|
|
19
34
|
};
|
|
20
35
|
export type TapperActiveFacet = TapperFacet & {
|
|
21
|
-
|
|
36
|
+
insert: (value: string, options?: {
|
|
22
37
|
noTrailingSpace?: boolean;
|
|
23
38
|
}) => void;
|
|
24
39
|
};
|
|
@@ -29,7 +44,7 @@ export type TapperSnapshot = {
|
|
|
29
44
|
activeFacet: TapperActiveFacet | null;
|
|
30
45
|
};
|
|
31
46
|
export type TapperConfig = {
|
|
32
|
-
facets
|
|
47
|
+
facets?: TapperFacetConfig;
|
|
33
48
|
initialText?: string;
|
|
34
49
|
};
|
|
35
50
|
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,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,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 = {\n
|
|
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 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 TapperSnapshot = {\n text: string\n cursor: number\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:
|
|
10
|
-
export declare function parseNodesFromText(text: string, regexes: CompiledFacetRegexes, prevNodes?: TapperNode[]): TapperNode[];
|
|
11
|
-
export declare function deriveTriggers(config:
|
|
9
|
+
export declare function compileFacetRegexes(config: TapperFacetConfig): CompiledFacetRegexes;
|
|
10
|
+
export declare function parseNodesFromText(text: string, regexes: CompiledFacetRegexes, prevNodes?: TapperNode[], cursor?: number, triggers?: Map<string, string>): TapperNode[];
|
|
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,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,CAkCpB"}
|
package/build/util.js
CHANGED
|
@@ -7,12 +7,12 @@ 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
|
}
|
|
15
|
-
export function parseNodesFromText(text, regexes, prevNodes) {
|
|
15
|
+
export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
|
|
16
16
|
const allMatches = [];
|
|
17
17
|
for (const [name, re] of regexes) {
|
|
18
18
|
// Reset lastIndex so stateful (global) regexes don't carry over
|
|
@@ -22,7 +22,7 @@ export function parseNodesFromText(text, regexes, prevNodes) {
|
|
|
22
22
|
allMatches.push({
|
|
23
23
|
facetName: name,
|
|
24
24
|
fullMatch: m[0],
|
|
25
|
-
capture: m[0],
|
|
25
|
+
capture: m[1] ?? m[0],
|
|
26
26
|
index: m.index,
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -37,52 +37,108 @@ export function parseNodesFromText(text, regexes, prevNodes) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
const nodes = [];
|
|
40
|
-
let
|
|
40
|
+
let pos = 0;
|
|
41
41
|
for (const m of accepted) {
|
|
42
|
-
if (m.index >
|
|
42
|
+
if (m.index > pos) {
|
|
43
|
+
const raw = text.slice(pos, m.index);
|
|
43
44
|
nodes.push({
|
|
44
45
|
type: 'text',
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
raw,
|
|
47
|
+
value: raw,
|
|
48
|
+
start: pos,
|
|
47
49
|
end: m.index,
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
nodes.push({
|
|
51
|
-
type:
|
|
53
|
+
type: 'facet',
|
|
54
|
+
facetType: m.facetName,
|
|
55
|
+
raw: m.fullMatch,
|
|
52
56
|
value: m.capture,
|
|
53
57
|
start: m.index,
|
|
54
58
|
end: m.index + m.fullMatch.length,
|
|
55
59
|
});
|
|
56
|
-
|
|
60
|
+
pos = m.index + m.fullMatch.length;
|
|
57
61
|
}
|
|
58
|
-
if (
|
|
62
|
+
if (pos < text.length) {
|
|
63
|
+
const raw = text.slice(pos);
|
|
59
64
|
nodes.push({
|
|
60
65
|
type: 'text',
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
raw,
|
|
67
|
+
value: raw,
|
|
68
|
+
start: pos,
|
|
63
69
|
end: text.length,
|
|
64
70
|
});
|
|
65
71
|
}
|
|
66
|
-
//
|
|
72
|
+
// If the cursor is right after a trigger char that the regex didn't match,
|
|
73
|
+
// splice a 'trigger' node out of the containing text node.
|
|
74
|
+
if (cursor != null && triggers) {
|
|
75
|
+
for (let i = cursor - 1; i >= 0; i--) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
if (WHITESPACE.test(ch))
|
|
78
|
+
break;
|
|
79
|
+
const facetType = triggers.get(ch);
|
|
80
|
+
if (facetType) {
|
|
81
|
+
// Only create a trigger node if the trigger is inside a text node
|
|
82
|
+
// (i.e. the regex didn't already match it as a facet)
|
|
83
|
+
const textNodeIdx = nodes.findIndex(n => n.type === 'text' && n.start <= i && n.end > i);
|
|
84
|
+
if (textNodeIdx !== -1) {
|
|
85
|
+
const node = nodes[textNodeIdx];
|
|
86
|
+
const spliced = [];
|
|
87
|
+
if (node.start < i) {
|
|
88
|
+
const raw = text.slice(node.start, i);
|
|
89
|
+
spliced.push({
|
|
90
|
+
type: 'text',
|
|
91
|
+
raw,
|
|
92
|
+
value: raw,
|
|
93
|
+
start: node.start,
|
|
94
|
+
end: i,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const triggerRaw = text.slice(i, cursor);
|
|
98
|
+
spliced.push({
|
|
99
|
+
type: 'trigger',
|
|
100
|
+
facetType,
|
|
101
|
+
raw: triggerRaw,
|
|
102
|
+
value: text.slice(i + ch.length, cursor),
|
|
103
|
+
start: i,
|
|
104
|
+
end: cursor,
|
|
105
|
+
});
|
|
106
|
+
if (cursor < node.end) {
|
|
107
|
+
const raw = text.slice(cursor, node.end);
|
|
108
|
+
spliced.push({
|
|
109
|
+
type: 'text',
|
|
110
|
+
raw,
|
|
111
|
+
value: raw,
|
|
112
|
+
start: cursor,
|
|
113
|
+
end: node.end,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
nodes.splice(textNodeIdx, 1, ...spliced);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Transfer committed flags from previous nodes by facetType + occurrence index
|
|
67
123
|
if (prevNodes) {
|
|
68
124
|
const counts = new Map();
|
|
69
125
|
const committedByTypeIndex = new Set();
|
|
70
126
|
for (const node of prevNodes) {
|
|
71
|
-
if (node.type
|
|
127
|
+
if (node.type !== 'facet')
|
|
72
128
|
continue;
|
|
73
|
-
const idx = counts.get(node.
|
|
74
|
-
counts.set(node.
|
|
129
|
+
const idx = counts.get(node.facetType) ?? 0;
|
|
130
|
+
counts.set(node.facetType, idx + 1);
|
|
75
131
|
if (node.committed) {
|
|
76
|
-
committedByTypeIndex.add(`${node.
|
|
132
|
+
committedByTypeIndex.add(`${node.facetType}:${idx}`);
|
|
77
133
|
}
|
|
78
134
|
}
|
|
79
135
|
counts.clear();
|
|
80
136
|
for (const node of nodes) {
|
|
81
|
-
if (node.type
|
|
137
|
+
if (node.type !== 'facet')
|
|
82
138
|
continue;
|
|
83
|
-
const idx = counts.get(node.
|
|
84
|
-
counts.set(node.
|
|
85
|
-
if (committedByTypeIndex.has(`${node.
|
|
139
|
+
const idx = counts.get(node.facetType) ?? 0;
|
|
140
|
+
counts.set(node.facetType, idx + 1);
|
|
141
|
+
if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {
|
|
86
142
|
node.committed = true;
|
|
87
143
|
}
|
|
88
144
|
}
|
|
@@ -91,8 +147,8 @@ export function parseNodesFromText(text, regexes, prevNodes) {
|
|
|
91
147
|
}
|
|
92
148
|
export function deriveTriggers(config) {
|
|
93
149
|
const triggers = new Map();
|
|
94
|
-
for (const [name,
|
|
95
|
-
const m =
|
|
150
|
+
for (const [name, re] of Object.entries(config)) {
|
|
151
|
+
const m = re.source.match(/^[^\\([\]{}.*+?^$|]+/);
|
|
96
152
|
if (m)
|
|
97
153
|
triggers.set(m[0], name);
|
|
98
154
|
}
|
|
@@ -100,16 +156,23 @@ export function deriveTriggers(config) {
|
|
|
100
156
|
}
|
|
101
157
|
export function nodeToFacet(node) {
|
|
102
158
|
return {
|
|
103
|
-
type: node.
|
|
159
|
+
type: node.facetType,
|
|
104
160
|
value: node.value,
|
|
105
161
|
range: { start: node.start, end: node.end },
|
|
106
162
|
};
|
|
107
163
|
}
|
|
108
164
|
export function detectActiveFacet(nodes, text, cursor, triggers) {
|
|
109
165
|
for (const node of nodes) {
|
|
110
|
-
if (node.type
|
|
166
|
+
if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
|
|
167
|
+
return {
|
|
168
|
+
type: node.facetType,
|
|
169
|
+
value: node.value,
|
|
170
|
+
range: { start: node.start, end: node.end },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
|
|
111
174
|
return {
|
|
112
|
-
type: node.
|
|
175
|
+
type: node.facetType,
|
|
113
176
|
value: node.value,
|
|
114
177
|
range: { start: node.start, end: node.end },
|
|
115
178
|
};
|
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): 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"]}
|
|
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 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"]}
|
package/package.json
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsky.app/tapper",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"description": "A minimal rich text editor for React Native and web.",
|
|
6
|
+
"repository": "https://github.com/bluesky-social/toolbox",
|
|
7
|
+
"author": "Eric Bailey <git@esb.lol> (https://github.com/estrattonbailey)",
|
|
8
|
+
"homepage": "https://github.com/bluesky-social/toolbox/tree/main/packages/tapper",
|
|
5
9
|
"main": "build/index.js",
|
|
6
10
|
"types": "build/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "expo-module build",
|
|
9
|
-
"clean": "expo-module clean",
|
|
10
|
-
"lint": "expo-module lint",
|
|
11
|
-
"test": "vitest run",
|
|
12
|
-
"prepare": "expo-module prepare",
|
|
13
|
-
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
-
"expo-module": "expo-module",
|
|
15
|
-
"open:ios": "xed example/ios",
|
|
16
|
-
"open:android": "open -a \"Android Studio\" example/android"
|
|
17
|
-
},
|
|
18
11
|
"keywords": [
|
|
19
12
|
"react-native",
|
|
20
13
|
"expo",
|
|
21
14
|
"tapper",
|
|
22
15
|
"Tapper"
|
|
23
16
|
],
|
|
24
|
-
"author": "Bluesky Social, PBLLC",
|
|
25
|
-
"license": "MIT",
|
|
26
17
|
"dependencies": {},
|
|
27
18
|
"devDependencies": {
|
|
28
19
|
"@types/react": "~19.1.1",
|
|
@@ -35,5 +26,14 @@
|
|
|
35
26
|
"expo": "*",
|
|
36
27
|
"react": "*",
|
|
37
28
|
"react-native": "*"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "expo-module build",
|
|
32
|
+
"clean": "expo-module clean",
|
|
33
|
+
"lint": "expo-module lint",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"expo-module": "expo-module",
|
|
36
|
+
"open:ios": "xed example/ios",
|
|
37
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
38
38
|
}
|
|
39
|
-
}
|
|
39
|
+
}
|
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
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
compileFacetRegexes,
|
|
16
16
|
type CompiledFacetRegexes,
|
|
17
17
|
} from './util'
|
|
18
|
+
import * as defaultFacets from './facets'
|
|
18
19
|
|
|
19
20
|
export * from './types'
|
|
21
|
+
export * from './facets'
|
|
20
22
|
|
|
21
23
|
export class Tapper {
|
|
22
24
|
private facetRegexes: CompiledFacetRegexes
|
|
@@ -40,16 +42,17 @@ export class Tapper {
|
|
|
40
42
|
private storeListeners = new Set<() => void>()
|
|
41
43
|
private snapshot: TapperSnapshot
|
|
42
44
|
|
|
43
|
-
constructor(config
|
|
44
|
-
|
|
45
|
-
this.
|
|
45
|
+
constructor(config?: TapperConfig) {
|
|
46
|
+
const facets = config?.facets || defaultFacets
|
|
47
|
+
this.facetRegexes = compileFacetRegexes(facets)
|
|
48
|
+
this.triggers = deriveTriggers(facets)
|
|
46
49
|
this.snapshot = {
|
|
47
50
|
text: this.text,
|
|
48
51
|
cursor: this.cursor,
|
|
49
52
|
nodes: this.nodes,
|
|
50
53
|
activeFacet: this.activeFacet,
|
|
51
54
|
}
|
|
52
|
-
if (config
|
|
55
|
+
if (config?.initialText) {
|
|
53
56
|
this.replaceText(config.initialText)
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -96,7 +99,13 @@ export class Tapper {
|
|
|
96
99
|
const nodes =
|
|
97
100
|
newText === this.text
|
|
98
101
|
? this.nodes
|
|
99
|
-
: parseNodesFromText(
|
|
102
|
+
: parseNodesFromText(
|
|
103
|
+
newText,
|
|
104
|
+
this.facetRegexes,
|
|
105
|
+
this.nodes,
|
|
106
|
+
cursor,
|
|
107
|
+
this.triggers,
|
|
108
|
+
)
|
|
100
109
|
const prev = this.activeFacet
|
|
101
110
|
const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)
|
|
102
111
|
|
|
@@ -105,8 +114,8 @@ export class Tapper {
|
|
|
105
114
|
if (prev && (!detected || detected.type !== prev.type)) {
|
|
106
115
|
for (const node of nodes) {
|
|
107
116
|
if (
|
|
108
|
-
node.type ===
|
|
109
|
-
node.
|
|
117
|
+
node.type === 'facet' &&
|
|
118
|
+
node.facetType === prev.type &&
|
|
110
119
|
node.start === prev.range.start &&
|
|
111
120
|
node.end === prev.range.end
|
|
112
121
|
) {
|
|
@@ -119,7 +128,7 @@ export class Tapper {
|
|
|
119
128
|
|
|
120
129
|
// Build activeFacet with commit() baked in
|
|
121
130
|
const active: TapperActiveFacet | null = detected
|
|
122
|
-
? {...detected,
|
|
131
|
+
? {...detected, insert: this.insert}
|
|
123
132
|
: null
|
|
124
133
|
|
|
125
134
|
this.text = newText
|
|
@@ -152,7 +161,7 @@ export class Tapper {
|
|
|
152
161
|
if (diff === -1 && newCursor < this.cursor) {
|
|
153
162
|
for (const node of this.nodes) {
|
|
154
163
|
if (
|
|
155
|
-
node.type
|
|
164
|
+
node.type === 'facet' &&
|
|
156
165
|
node.committed &&
|
|
157
166
|
this.cursor === node.end
|
|
158
167
|
) {
|
|
@@ -191,7 +200,7 @@ export class Tapper {
|
|
|
191
200
|
// Mark all non-text nodes as committed (pre-existing facets)
|
|
192
201
|
const newlyCommitted: TapperNode[] = []
|
|
193
202
|
for (const node of nodes) {
|
|
194
|
-
if (node.type
|
|
203
|
+
if (node.type === 'facet') {
|
|
195
204
|
node.committed = true
|
|
196
205
|
newlyCommitted.push(node)
|
|
197
206
|
}
|
|
@@ -200,7 +209,7 @@ export class Tapper {
|
|
|
200
209
|
const pos = cursor ?? text.length
|
|
201
210
|
const detected = detectActiveFacet(nodes, text, pos, this.triggers)
|
|
202
211
|
const active: TapperActiveFacet | null = detected
|
|
203
|
-
? {...detected,
|
|
212
|
+
? {...detected, insert: this.insert}
|
|
204
213
|
: null
|
|
205
214
|
|
|
206
215
|
this.text = text
|
|
@@ -218,7 +227,7 @@ export class Tapper {
|
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
|
|
221
|
-
private
|
|
230
|
+
private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {
|
|
222
231
|
if (!this.activeFacet) return
|
|
223
232
|
|
|
224
233
|
const replacement = value + (options?.noTrailingSpace ? '' : ' ')
|
package/src/types.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
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
|
-
export type TapperNode =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
export type TapperNode =
|
|
4
|
+
| {
|
|
5
|
+
type: 'text'
|
|
6
|
+
raw: string
|
|
7
|
+
value: string
|
|
8
|
+
start: number
|
|
9
|
+
end: number
|
|
10
|
+
committed?: boolean
|
|
11
|
+
facetType?: undefined
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
type: 'trigger'
|
|
15
|
+
raw: string
|
|
16
|
+
value: string
|
|
17
|
+
start: number
|
|
18
|
+
end: number
|
|
19
|
+
committed?: boolean
|
|
20
|
+
facetType: string
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: 'facet'
|
|
24
|
+
raw: string
|
|
25
|
+
value: string
|
|
26
|
+
start: number
|
|
27
|
+
end: number
|
|
28
|
+
committed?: boolean
|
|
29
|
+
facetType: string
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
export type TapperFacet = {
|
|
16
33
|
type: string
|
|
@@ -19,7 +36,7 @@ export type TapperFacet = {
|
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
export type TapperActiveFacet = TapperFacet & {
|
|
22
|
-
|
|
39
|
+
insert: (value: string, options?: {noTrailingSpace?: boolean}) => void
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
export type TapperSnapshot = {
|
|
@@ -30,7 +47,7 @@ export type TapperSnapshot = {
|
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
export type TapperConfig = {
|
|
33
|
-
facets
|
|
50
|
+
facets?: TapperFacetConfig
|
|
34
51
|
initialText?: string
|
|
35
52
|
}
|
|
36
53
|
|
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
|
}
|
|
@@ -24,6 +24,8 @@ export function parseNodesFromText(
|
|
|
24
24
|
text: string,
|
|
25
25
|
regexes: CompiledFacetRegexes,
|
|
26
26
|
prevNodes?: TapperNode[],
|
|
27
|
+
cursor?: number,
|
|
28
|
+
triggers?: Map<string, string>,
|
|
27
29
|
): TapperNode[] {
|
|
28
30
|
const allMatches: {
|
|
29
31
|
facetName: string
|
|
@@ -40,7 +42,7 @@ export function parseNodesFromText(
|
|
|
40
42
|
allMatches.push({
|
|
41
43
|
facetName: name,
|
|
42
44
|
fullMatch: m[0],
|
|
43
|
-
capture: m[0],
|
|
45
|
+
capture: m[1] ?? m[0],
|
|
44
46
|
index: m.index,
|
|
45
47
|
})
|
|
46
48
|
}
|
|
@@ -60,55 +62,113 @@ export function parseNodesFromText(
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const nodes: TapperNode[] = []
|
|
63
|
-
let
|
|
65
|
+
let pos = 0
|
|
64
66
|
|
|
65
67
|
for (const m of accepted) {
|
|
66
|
-
if (m.index >
|
|
68
|
+
if (m.index > pos) {
|
|
69
|
+
const raw = text.slice(pos, m.index)
|
|
67
70
|
nodes.push({
|
|
68
71
|
type: 'text',
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
raw,
|
|
73
|
+
value: raw,
|
|
74
|
+
start: pos,
|
|
71
75
|
end: m.index,
|
|
72
76
|
})
|
|
73
77
|
}
|
|
74
78
|
nodes.push({
|
|
75
|
-
type:
|
|
79
|
+
type: 'facet',
|
|
80
|
+
facetType: m.facetName,
|
|
81
|
+
raw: m.fullMatch,
|
|
76
82
|
value: m.capture,
|
|
77
83
|
start: m.index,
|
|
78
84
|
end: m.index + m.fullMatch.length,
|
|
79
85
|
})
|
|
80
|
-
|
|
86
|
+
pos = m.index + m.fullMatch.length
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
if (
|
|
89
|
+
if (pos < text.length) {
|
|
90
|
+
const raw = text.slice(pos)
|
|
84
91
|
nodes.push({
|
|
85
92
|
type: 'text',
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
raw,
|
|
94
|
+
value: raw,
|
|
95
|
+
start: pos,
|
|
88
96
|
end: text.length,
|
|
89
97
|
})
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
//
|
|
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
|
|
93
153
|
if (prevNodes) {
|
|
94
154
|
const counts = new Map<string, number>()
|
|
95
155
|
const committedByTypeIndex = new Set<string>()
|
|
96
156
|
|
|
97
157
|
for (const node of prevNodes) {
|
|
98
|
-
if (node.type
|
|
99
|
-
const idx = counts.get(node.
|
|
100
|
-
counts.set(node.
|
|
158
|
+
if (node.type !== 'facet') continue
|
|
159
|
+
const idx = counts.get(node.facetType!) ?? 0
|
|
160
|
+
counts.set(node.facetType!, idx + 1)
|
|
101
161
|
if (node.committed) {
|
|
102
|
-
committedByTypeIndex.add(`${node.
|
|
162
|
+
committedByTypeIndex.add(`${node.facetType}:${idx}`)
|
|
103
163
|
}
|
|
104
164
|
}
|
|
105
165
|
|
|
106
166
|
counts.clear()
|
|
107
167
|
for (const node of nodes) {
|
|
108
|
-
if (node.type
|
|
109
|
-
const idx = counts.get(node.
|
|
110
|
-
counts.set(node.
|
|
111
|
-
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}`)) {
|
|
112
172
|
node.committed = true
|
|
113
173
|
}
|
|
114
174
|
}
|
|
@@ -117,12 +177,10 @@ export function parseNodesFromText(
|
|
|
117
177
|
return nodes
|
|
118
178
|
}
|
|
119
179
|
|
|
120
|
-
export function deriveTriggers(
|
|
121
|
-
config: TapperFacetConfigMap,
|
|
122
|
-
): Map<string, string> {
|
|
180
|
+
export function deriveTriggers(config: TapperFacetConfig): Map<string, string> {
|
|
123
181
|
const triggers = new Map<string, string>()
|
|
124
|
-
for (const [name,
|
|
125
|
-
const m =
|
|
182
|
+
for (const [name, re] of Object.entries(config)) {
|
|
183
|
+
const m = re.source.match(/^[^\\([\]{}.*+?^$|]+/)
|
|
126
184
|
if (m) triggers.set(m[0], name)
|
|
127
185
|
}
|
|
128
186
|
return triggers
|
|
@@ -130,7 +188,7 @@ export function deriveTriggers(
|
|
|
130
188
|
|
|
131
189
|
export function nodeToFacet(node: TapperNode): TapperFacet {
|
|
132
190
|
return {
|
|
133
|
-
type: node.
|
|
191
|
+
type: node.facetType!,
|
|
134
192
|
value: node.value,
|
|
135
193
|
range: {start: node.start, end: node.end},
|
|
136
194
|
}
|
|
@@ -143,9 +201,16 @@ export function detectActiveFacet(
|
|
|
143
201
|
triggers: Map<string, string>,
|
|
144
202
|
): TapperFacet | null {
|
|
145
203
|
for (const node of nodes) {
|
|
146
|
-
if (node.type
|
|
204
|
+
if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
|
|
205
|
+
return {
|
|
206
|
+
type: node.facetType,
|
|
207
|
+
value: node.value,
|
|
208
|
+
range: {start: node.start, end: node.end},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
|
|
147
212
|
return {
|
|
148
|
-
type: node.
|
|
213
|
+
type: node.facetType,
|
|
149
214
|
value: node.value,
|
|
150
215
|
range: {start: node.start, end: node.end},
|
|
151
216
|
}
|