@bsky.app/tapper 0.5.2 → 0.5.5
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 +18 -0
- package/build/facets.d.ts.map +1 -1
- package/build/facets.js +8 -4
- package/build/facets.js.map +1 -1
- package/build/util.d.ts.map +1 -1
- package/build/util.js +29 -6
- package/build/util.js.map +1 -1
- package/package.json +6 -7
- package/src/facets.ts +8 -4
- package/src/util.ts +29 -6
- package/tsconfig.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @bsky.app/tapper
|
|
2
2
|
|
|
3
|
+
## 0.5.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#45](https://github.com/bluesky-social/toolbox/pull/45) [`3edc0b3`](https://github.com/bluesky-social/toolbox/commit/3edc0b369f8a563763a240ba829fae20e0cf54e5) Thanks [@mozzius](https://github.com/mozzius)! - Fix trusted publishing
|
|
8
|
+
|
|
9
|
+
## 0.5.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#43](https://github.com/bluesky-social/toolbox/pull/43) [`a5feb85`](https://github.com/bluesky-social/toolbox/commit/a5feb85c7eef69385d459d2b0d26a474548ea6db) Thanks [@mozzius](https://github.com/mozzius)! - Enable trusted publishing
|
|
14
|
+
|
|
15
|
+
## 0.5.3
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#32](https://github.com/bluesky-social/toolbox/pull/32) [`f90f35f`](https://github.com/bluesky-social/toolbox/commit/f90f35fbcabb0e4c959d60f273ecf6df38a20534) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Ensure facets handle leading whitespace/chars before triggering
|
|
20
|
+
|
|
3
21
|
## 0.5.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/build/facets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,QAAkC,CAAA;AACtD,eAAO,MAAM,KAAK,QAAgC,CAAA;AAClD,eAAO,MAAM,GAAG,QAC4K,CAAA;AAC5L,eAAO,MAAM,GAAG,QACmE,CAAA"}
|
package/build/facets.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// Each regex begins with a `(^|\s|\()` capture group: a backwards-compatible
|
|
2
|
+
// alternative to lookbehind (Safari < 16.4 throws on lookbehind at parse time).
|
|
3
|
+
// The boundary char is captured into m[1]; util.ts strips it so positions and
|
|
4
|
+
// `raw` reflect the facet itself.
|
|
5
|
+
export const mention = /(^|\s|\()@([a-zA-Z0-9.-]+)\b/g;
|
|
6
|
+
export const emoji = /(^|\s|\():([a-zA-Z0-9_]+):/g;
|
|
7
|
+
export const tag = /(^|\s|\()[##]((?!\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;
|
|
8
|
+
export const url = /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim;
|
|
5
9
|
//# sourceMappingURL=facets.js.map
|
package/build/facets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facets.js","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"facets.js","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,gFAAgF;AAChF,8EAA8E;AAC9E,kCAAkC;AAClC,MAAM,CAAC,MAAM,OAAO,GAAG,+BAA+B,CAAA;AACtD,MAAM,CAAC,MAAM,KAAK,GAAG,6BAA6B,CAAA;AAClD,MAAM,CAAC,MAAM,GAAG,GACd,0LAA0L,CAAA;AAC5L,MAAM,CAAC,MAAM,GAAG,GACd,iFAAiF,CAAA","sourcesContent":["// Each regex begins with a `(^|\\s|\\()` capture group: a backwards-compatible\n// alternative to lookbehind (Safari < 16.4 throws on lookbehind at parse time).\n// The boundary char is captured into m[1]; util.ts strips it so positions and\n// `raw` reflect the facet itself.\nexport const mention = /(^|\\s|\\()@([a-zA-Z0-9.-]+)\\b/g\nexport const emoji = /(^|\\s|\\():([a-zA-Z0-9_]+):/g\nexport const tag =\n /(^|\\s|\\()[##]((?!\\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 /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim\n"]}
|
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,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;
|
|
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;AAmBlE,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,CA6Kd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAW7E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAOzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CAkDpB"}
|
package/build/util.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
const WHITESPACE = /\s/;
|
|
2
|
+
// Regexes whose source starts with this prefix use a leading capture group
|
|
3
|
+
// to gate matches on a boundary (start-of-string, whitespace, or `(`) without
|
|
4
|
+
// lookbehind. The captured boundary char is stripped from the match so
|
|
5
|
+
// positions and `raw` reflect the facet itself.
|
|
6
|
+
const BOUNDARY_PREFIX = '(^|\\s|\\()';
|
|
7
|
+
// Mirror the regex's `(^|\s|\()` gate so trigger detection (for chars typed
|
|
8
|
+
// before any regex match exists) doesn't fire mid-word — e.g. the `@` inside
|
|
9
|
+
// `eric@blueskyweb.xyz` should not synthesize a mention trigger.
|
|
10
|
+
function isBoundaryBefore(text, i) {
|
|
11
|
+
if (i === 0)
|
|
12
|
+
return true;
|
|
13
|
+
const prev = text[i - 1];
|
|
14
|
+
return prev === '(' || WHITESPACE.test(prev);
|
|
15
|
+
}
|
|
2
16
|
let nextNodeId = 0;
|
|
3
17
|
/**
|
|
4
18
|
* Pre-compile facet regexes once at init time. This avoids re-creating
|
|
@@ -19,12 +33,17 @@ export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
|
|
|
19
33
|
// Reset lastIndex so stateful (global) regexes don't carry over
|
|
20
34
|
// match positions from the previous parse call.
|
|
21
35
|
re.lastIndex = 0;
|
|
36
|
+
// Boundary-gated regexes shift the effective start past the captured
|
|
37
|
+
// boundary char (m[1]) and bump the value capture index by one.
|
|
38
|
+
const hasBoundary = re.source.startsWith(BOUNDARY_PREFIX);
|
|
22
39
|
for (const m of text.matchAll(re)) {
|
|
40
|
+
const boundaryLen = hasBoundary ? (m[1]?.length ?? 0) : 0;
|
|
41
|
+
const fullMatch = m[0].slice(boundaryLen);
|
|
23
42
|
allMatches.push({
|
|
24
43
|
facetName: name,
|
|
25
|
-
fullMatch
|
|
26
|
-
capture: m[
|
|
27
|
-
index: m.index,
|
|
44
|
+
fullMatch,
|
|
45
|
+
capture: (hasBoundary ? m[2] : m[1]) ?? fullMatch,
|
|
46
|
+
index: m.index + boundaryLen,
|
|
28
47
|
});
|
|
29
48
|
}
|
|
30
49
|
}
|
|
@@ -81,7 +100,7 @@ export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
|
|
|
81
100
|
if (WHITESPACE.test(ch))
|
|
82
101
|
break;
|
|
83
102
|
const facetType = triggers.get(ch);
|
|
84
|
-
if (facetType) {
|
|
103
|
+
if (facetType && isBoundaryBefore(text, i)) {
|
|
85
104
|
// Only create a trigger node if the trigger is inside a text node
|
|
86
105
|
// (i.e. the regex didn't already match it as a facet)
|
|
87
106
|
const textNodeIdx = nodes.findIndex(n => n.type === 'text' && n.start <= i && n.end > i);
|
|
@@ -170,7 +189,11 @@ export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
|
|
|
170
189
|
export function deriveTriggers(config) {
|
|
171
190
|
const triggers = new Map();
|
|
172
191
|
for (const [name, re] of Object.entries(config)) {
|
|
173
|
-
|
|
192
|
+
// Skip the boundary prefix (if present) before extracting the trigger char.
|
|
193
|
+
const src = re.source.startsWith(BOUNDARY_PREFIX)
|
|
194
|
+
? re.source.slice(BOUNDARY_PREFIX.length)
|
|
195
|
+
: re.source;
|
|
196
|
+
const m = src.match(/^[^\\([\]{}.*+?^$|]+/);
|
|
174
197
|
if (m)
|
|
175
198
|
triggers.set(m[0], name);
|
|
176
199
|
}
|
|
@@ -216,7 +239,7 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
|
|
|
216
239
|
if (WHITESPACE.test(ch))
|
|
217
240
|
break;
|
|
218
241
|
const type = triggers.get(ch);
|
|
219
|
-
if (type) {
|
|
242
|
+
if (type && isBoundaryBefore(text, i)) {
|
|
220
243
|
const raw = text.slice(i, cursor);
|
|
221
244
|
return {
|
|
222
245
|
type,
|
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 {TapperFacetConfig, TapperNode, TapperFacet} from './types'\n\nconst WHITESPACE = /\\s/\nlet nextNodeId = 0\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 id: nextNodeId++,\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n id: nextNodeId++,\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 id: nextNodeId++,\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 id: nextNodeId++,\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 id: nextNodeId++,\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 id: nextNodeId++,\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 and stable IDs from previous nodes\n if (prevNodes) {\n // Facet nodes: match by facetType + occurrence index\n const counts = new Map<string, number>()\n const prevFacetsByTypeIndex = new Map<string, TapperNode>()\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 prevFacetsByTypeIndex.set(`${node.facetType}:${idx}`, node)\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 const prev = prevFacetsByTypeIndex.get(`${node.facetType}:${idx}`)\n if (prev) {\n node.id = prev.id\n if (prev.committed) node.committed = true\n }\n }\n\n // Text nodes: match by start position\n const prevTextByStart = new Map<number, TapperNode>()\n for (const node of prevNodes) {\n if (node.type === 'text') prevTextByStart.set(node.start, node)\n }\n for (const node of nodes) {\n if (node.type === 'text') {\n const prev = prevTextByStart.get(node.start)\n if (prev) node.id = prev.id\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(config: TapperFacetConfig): Map<string, string> {\n const triggers = new Map<string, string>()\n for (const [name, re] of Object.entries(config)) {\n const m = re.source.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (\n node.type === 'facet' &&\n !node.committed &&\n node.start < cursor &&\n cursor <= node.end\n ) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex).\n // Skip if the cursor is inside or at the end of a committed facet.\n const inCommitted = nodes.some(\n n =>\n n.type === 'facet' && n.committed && n.start < cursor && cursor <= n.end,\n )\n if (!inCommitted) {\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n // Stop at whitespace — triggers don't span across words\n if (WHITESPACE.test(ch)) break\n const type = triggers.get(ch)\n if (type) {\n const raw = text.slice(i, cursor)\n return {\n type,\n raw,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n }\n\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// Regexes whose source starts with this prefix use a leading capture group\n// to gate matches on a boundary (start-of-string, whitespace, or `(`) without\n// lookbehind. The captured boundary char is stripped from the match so\n// positions and `raw` reflect the facet itself.\nconst BOUNDARY_PREFIX = '(^|\\\\s|\\\\()'\n\n// Mirror the regex's `(^|\\s|\\()` gate so trigger detection (for chars typed\n// before any regex match exists) doesn't fire mid-word — e.g. the `@` inside\n// `eric@blueskyweb.xyz` should not synthesize a mention trigger.\nfunction isBoundaryBefore(text: string, i: number) {\n if (i === 0) return true\n const prev = text[i - 1]\n return prev === '(' || WHITESPACE.test(prev)\n}\nlet nextNodeId = 0\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 // Boundary-gated regexes shift the effective start past the captured\n // boundary char (m[1]) and bump the value capture index by one.\n const hasBoundary = re.source.startsWith(BOUNDARY_PREFIX)\n for (const m of text.matchAll(re)) {\n const boundaryLen = hasBoundary ? (m[1]?.length ?? 0) : 0\n const fullMatch = m[0].slice(boundaryLen)\n allMatches.push({\n facetName: name,\n fullMatch,\n capture: (hasBoundary ? m[2] : m[1]) ?? fullMatch,\n index: m.index + boundaryLen,\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 id: nextNodeId++,\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n id: nextNodeId++,\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 id: nextNodeId++,\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 && isBoundaryBefore(text, i)) {\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 id: nextNodeId++,\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 id: nextNodeId++,\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 id: nextNodeId++,\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 and stable IDs from previous nodes\n if (prevNodes) {\n // Facet nodes: match by facetType + occurrence index\n const counts = new Map<string, number>()\n const prevFacetsByTypeIndex = new Map<string, TapperNode>()\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 prevFacetsByTypeIndex.set(`${node.facetType}:${idx}`, node)\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 const prev = prevFacetsByTypeIndex.get(`${node.facetType}:${idx}`)\n if (prev) {\n node.id = prev.id\n if (prev.committed) node.committed = true\n }\n }\n\n // Text nodes: match by start position\n const prevTextByStart = new Map<number, TapperNode>()\n for (const node of prevNodes) {\n if (node.type === 'text') prevTextByStart.set(node.start, node)\n }\n for (const node of nodes) {\n if (node.type === 'text') {\n const prev = prevTextByStart.get(node.start)\n if (prev) node.id = prev.id\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 // Skip the boundary prefix (if present) before extracting the trigger char.\n const src = re.source.startsWith(BOUNDARY_PREFIX)\n ? re.source.slice(BOUNDARY_PREFIX.length)\n : re.source\n const m = src.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (\n node.type === 'facet' &&\n !node.committed &&\n node.start < cursor &&\n cursor <= node.end\n ) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex).\n // Skip if the cursor is inside or at the end of a committed facet.\n const inCommitted = nodes.some(\n n =>\n n.type === 'facet' && n.committed && n.start < cursor && cursor <= n.end,\n )\n if (!inCommitted) {\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 && isBoundaryBefore(text, i)) {\n const raw = text.slice(i, cursor)\n return {\n type,\n raw,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n }\n\n return null\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsky.app/tapper",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A minimal rich text editor for React Native and web.",
|
|
6
6
|
"repository": "https://github.com/bluesky-social/toolbox",
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
"tapper",
|
|
15
15
|
"Tapper"
|
|
16
16
|
],
|
|
17
|
-
"dependencies": {},
|
|
18
17
|
"devDependencies": {
|
|
19
|
-
"@types/react": "~19.
|
|
20
|
-
"expo-module-scripts": "^55.0.2",
|
|
21
|
-
"vitest": "^3.1.1",
|
|
18
|
+
"@types/react": "~19.2.15",
|
|
22
19
|
"expo": "^55.0.8",
|
|
23
|
-
"
|
|
20
|
+
"expo-module-scripts": "^55.0.2",
|
|
21
|
+
"react-native": "0.82.1",
|
|
22
|
+
"vitest": "^3.1.1"
|
|
24
23
|
},
|
|
25
24
|
"peerDependencies": {
|
|
26
25
|
"expo": "*",
|
|
@@ -28,7 +27,7 @@
|
|
|
28
27
|
"react-native": "*"
|
|
29
28
|
},
|
|
30
29
|
"scripts": {
|
|
31
|
-
"build": "expo-module clean && expo-module build",
|
|
30
|
+
"build": "expo-module clean && EXPO_NONINTERACTIVE=1 expo-module build",
|
|
32
31
|
"clean": "expo-module clean",
|
|
33
32
|
"lint": "expo-module lint",
|
|
34
33
|
"typecheck": "tsc --noEmit",
|
package/src/facets.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Each regex begins with a `(^|\s|\()` capture group: a backwards-compatible
|
|
2
|
+
// alternative to lookbehind (Safari < 16.4 throws on lookbehind at parse time).
|
|
3
|
+
// The boundary char is captured into m[1]; util.ts strips it so positions and
|
|
4
|
+
// `raw` reflect the facet itself.
|
|
5
|
+
export const mention = /(^|\s|\()@([a-zA-Z0-9.-]+)\b/g
|
|
6
|
+
export const emoji = /(^|\s|\():([a-zA-Z0-9_]+):/g
|
|
3
7
|
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
|
|
8
|
+
/(^|\s|\()[##]((?!\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
9
|
export const url =
|
|
6
|
-
/((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
|
|
10
|
+
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
|
package/src/util.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import {TapperFacetConfig, TapperNode, TapperFacet} from './types'
|
|
2
2
|
|
|
3
3
|
const WHITESPACE = /\s/
|
|
4
|
+
// Regexes whose source starts with this prefix use a leading capture group
|
|
5
|
+
// to gate matches on a boundary (start-of-string, whitespace, or `(`) without
|
|
6
|
+
// lookbehind. The captured boundary char is stripped from the match so
|
|
7
|
+
// positions and `raw` reflect the facet itself.
|
|
8
|
+
const BOUNDARY_PREFIX = '(^|\\s|\\()'
|
|
9
|
+
|
|
10
|
+
// Mirror the regex's `(^|\s|\()` gate so trigger detection (for chars typed
|
|
11
|
+
// before any regex match exists) doesn't fire mid-word — e.g. the `@` inside
|
|
12
|
+
// `eric@blueskyweb.xyz` should not synthesize a mention trigger.
|
|
13
|
+
function isBoundaryBefore(text: string, i: number) {
|
|
14
|
+
if (i === 0) return true
|
|
15
|
+
const prev = text[i - 1]
|
|
16
|
+
return prev === '(' || WHITESPACE.test(prev)
|
|
17
|
+
}
|
|
4
18
|
let nextNodeId = 0
|
|
5
19
|
|
|
6
20
|
export type CompiledFacetRegexes = Map<string, RegExp>
|
|
@@ -39,12 +53,17 @@ export function parseNodesFromText(
|
|
|
39
53
|
// Reset lastIndex so stateful (global) regexes don't carry over
|
|
40
54
|
// match positions from the previous parse call.
|
|
41
55
|
re.lastIndex = 0
|
|
56
|
+
// Boundary-gated regexes shift the effective start past the captured
|
|
57
|
+
// boundary char (m[1]) and bump the value capture index by one.
|
|
58
|
+
const hasBoundary = re.source.startsWith(BOUNDARY_PREFIX)
|
|
42
59
|
for (const m of text.matchAll(re)) {
|
|
60
|
+
const boundaryLen = hasBoundary ? (m[1]?.length ?? 0) : 0
|
|
61
|
+
const fullMatch = m[0].slice(boundaryLen)
|
|
43
62
|
allMatches.push({
|
|
44
63
|
facetName: name,
|
|
45
|
-
fullMatch
|
|
46
|
-
capture: m[
|
|
47
|
-
index: m.index,
|
|
64
|
+
fullMatch,
|
|
65
|
+
capture: (hasBoundary ? m[2] : m[1]) ?? fullMatch,
|
|
66
|
+
index: m.index + boundaryLen,
|
|
48
67
|
})
|
|
49
68
|
}
|
|
50
69
|
}
|
|
@@ -108,7 +127,7 @@ export function parseNodesFromText(
|
|
|
108
127
|
const ch = text[i]
|
|
109
128
|
if (WHITESPACE.test(ch)) break
|
|
110
129
|
const facetType = triggers.get(ch)
|
|
111
|
-
if (facetType) {
|
|
130
|
+
if (facetType && isBoundaryBefore(text, i)) {
|
|
112
131
|
// Only create a trigger node if the trigger is inside a text node
|
|
113
132
|
// (i.e. the regex didn't already match it as a facet)
|
|
114
133
|
const textNodeIdx = nodes.findIndex(
|
|
@@ -200,7 +219,11 @@ export function parseNodesFromText(
|
|
|
200
219
|
export function deriveTriggers(config: TapperFacetConfig): Map<string, string> {
|
|
201
220
|
const triggers = new Map<string, string>()
|
|
202
221
|
for (const [name, re] of Object.entries(config)) {
|
|
203
|
-
|
|
222
|
+
// Skip the boundary prefix (if present) before extracting the trigger char.
|
|
223
|
+
const src = re.source.startsWith(BOUNDARY_PREFIX)
|
|
224
|
+
? re.source.slice(BOUNDARY_PREFIX.length)
|
|
225
|
+
: re.source
|
|
226
|
+
const m = src.match(/^[^\\([\]{}.*+?^$|]+/)
|
|
204
227
|
if (m) triggers.set(m[0], name)
|
|
205
228
|
}
|
|
206
229
|
return triggers
|
|
@@ -257,7 +280,7 @@ export function detectActiveFacet(
|
|
|
257
280
|
// Stop at whitespace — triggers don't span across words
|
|
258
281
|
if (WHITESPACE.test(ch)) break
|
|
259
282
|
const type = triggers.get(ch)
|
|
260
|
-
if (type) {
|
|
283
|
+
if (type && isBoundaryBefore(text, i)) {
|
|
261
284
|
const raw = text.slice(i, cursor)
|
|
262
285
|
return {
|
|
263
286
|
type,
|