@bsky.app/tapper 0.6.0 → 0.6.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @bsky.app/tapper
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#55](https://github.com/bluesky-social/toolbox/pull/55) [`509c0a7`](https://github.com/bluesky-social/toolbox/commit/509c0a705fe9104631dff523780ccbd47cec453d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix URL facets not highlighting on native. When an app bundles tapper, Babel's named-capture-group transform strips the `(?<domain>)` name from the `url` facet's regex source but can't rewrite the `m.groups.domain` access in its validate function, so the domain read back `undefined` and every bare domain was rejected. Validate now reads the domain by capture index instead. Parsing also switched from `matchAll` to an `exec` loop (Hermes drops named groups under `matchAll` too), and non-global facet patterns are now compiled with the `g` flag automatically instead of throwing.
8
+
9
+ - [#53](https://github.com/bluesky-social/toolbox/pull/53) [`bc83ab8`](https://github.com/bluesky-social/toolbox/commit/bc83ab8a5e761302eff0b75bb17518ce9b5aa638) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Update npmignore to prune some files
10
+
3
11
  ## 0.6.0
4
12
 
5
13
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,qBAAqB,EAAC,MAAM,SAAS,CAAA;AAM7C,eAAO,MAAM,OAAO,QAAkC,CAAA;AACtD,eAAO,MAAM,KAAK,QAAgC,CAAA;AAClD,eAAO,MAAM,GAAG,QAC4K,CAAA;AAW5L,eAAO,MAAM,GAAG,EAAE,qBASjB,CAAA"}
1
+ {"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,qBAAqB,EAAC,MAAM,SAAS,CAAA;AAM7C,eAAO,MAAM,OAAO,QAAkC,CAAA;AACtD,eAAO,MAAM,KAAK,QAAgC,CAAA;AAClD,eAAO,MAAM,GAAG,QAC4K,CAAA;AAiB5L,eAAO,MAAM,GAAG,EAAE,qBASjB,CAAA"}
package/build/facets.js CHANGED
@@ -14,12 +14,18 @@ const TLD_SET = new Set(TLDs);
14
14
  // editor but never become facets. `https?://` matches (m[3]) skip validation,
15
15
  // same as RichText. The TLD check is deliberately case-sensitive: RichText's
16
16
  // isValidDomain is too (`BSKY.APP` never facets), so don't lowercase here.
17
+ //
18
+ // Capture indices are load-bearing: validate reads the domain from m[5]. We
19
+ // can't use the (?<domain>) name at runtime — bundlers (Babel's
20
+ // transform-named-capturing-groups-regex) strip the name from the regex source
21
+ // when an app bundles us, so m.groups.domain reads back undefined. If you
22
+ // reorder or add capture groups in the pattern, update the m[5] index below.
17
23
  export const url = {
18
24
  pattern: /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim,
19
25
  validate: m => {
20
26
  if (m[3])
21
27
  return true;
22
- const domain = m.groups?.domain;
28
+ const domain = m[5];
23
29
  if (!domain)
24
30
  return false;
25
31
  return TLD_SET.has(domain.slice(domain.lastIndexOf('.') + 1));
@@ -1 +1 @@
1
- {"version":3,"file":"facets.js","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAIvB,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;AAE5L,iEAAiE;AACjE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;AAE7B,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,MAAM,CAAC,MAAM,GAAG,GAA0B;IACxC,OAAO,EACL,iFAAiF;IACnF,QAAQ,EAAE,CAAC,CAAC,EAAE;QACZ,IAAI,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;QACrB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAA;QAC/B,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAA;QACzB,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;CACF,CAAA","sourcesContent":["import TLDs from 'tlds'\n\nimport {TapperFacetDefinition} from './types'\n\n// 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\n\n// Built once at module load and shared by every Tapper instance.\nconst TLD_SET = new Set(TLDs)\n\n// Bare-domain matches must end in a real TLD, mirroring the validation\n// RichText's detectFacets applies after its identical URL_REGEX matches.\n// Without this, dotted abbreviations like `P.S.` or `e.g.` highlight in the\n// editor but never become facets. `https?://` matches (m[3]) skip validation,\n// same as RichText. The TLD check is deliberately case-sensitive: RichText's\n// isValidDomain is too (`BSKY.APP` never facets), so don't lowercase here.\nexport const url: TapperFacetDefinition = {\n pattern:\n /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim,\n validate: m => {\n if (m[3]) return true\n const domain = m.groups?.domain\n if (!domain) return false\n return TLD_SET.has(domain.slice(domain.lastIndexOf('.') + 1))\n },\n}\n"]}
1
+ {"version":3,"file":"facets.js","sourceRoot":"","sources":["../src/facets.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAIvB,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;AAE5L,iEAAiE;AACjE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;AAE7B,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,gEAAgE;AAChE,+EAA+E;AAC/E,0EAA0E;AAC1E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,GAAG,GAA0B;IACxC,OAAO,EACL,iFAAiF;IACnF,QAAQ,EAAE,CAAC,CAAC,EAAE;QACZ,IAAI,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;QACrB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACnB,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAA;QACzB,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;CACF,CAAA","sourcesContent":["import TLDs from 'tlds'\n\nimport {TapperFacetDefinition} from './types'\n\n// 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\n\n// Built once at module load and shared by every Tapper instance.\nconst TLD_SET = new Set(TLDs)\n\n// Bare-domain matches must end in a real TLD, mirroring the validation\n// RichText's detectFacets applies after its identical URL_REGEX matches.\n// Without this, dotted abbreviations like `P.S.` or `e.g.` highlight in the\n// editor but never become facets. `https?://` matches (m[3]) skip validation,\n// same as RichText. The TLD check is deliberately case-sensitive: RichText's\n// isValidDomain is too (`BSKY.APP` never facets), so don't lowercase here.\n//\n// Capture indices are load-bearing: validate reads the domain from m[5]. We\n// can't use the (?<domain>) name at runtime — bundlers (Babel's\n// transform-named-capturing-groups-regex) strip the name from the regex source\n// when an app bundles us, so m.groups.domain reads back undefined. If you\n// reorder or add capture groups in the pattern, update the m[5] index below.\nexport const url: TapperFacetDefinition = {\n pattern:\n /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim,\n validate: m => {\n if (m[3]) return true\n const domain = m[5]\n if (!domain) return false\n return TLD_SET.has(domain.slice(domain.lastIndexOf('.') + 1))\n },\n}\n"]}
@@ -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;AAmBlE,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE7D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAUtB;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,CA8Kd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7E;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"}
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,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE7D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAatB;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,CAsLd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7E;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
@@ -25,8 +25,11 @@ export function compileFacetRegexes(config) {
25
25
  const compiled = new Map();
26
26
  for (const [name, def] of Object.entries(config)) {
27
27
  const re = def instanceof RegExp ? def : def.pattern;
28
+ // Force the global flag: the exec loop in parseNodesFromText would spin
29
+ // forever on a non-global regex (matchAll used to throw on these).
30
+ const flags = re.flags.includes('g') ? re.flags : re.flags + 'g';
28
31
  compiled.set(name, {
29
- regex: new RegExp(re.source, re.flags),
32
+ regex: new RegExp(re.source, flags),
30
33
  validate: def instanceof RegExp ? undefined : def.validate,
31
34
  });
32
35
  }
@@ -41,7 +44,16 @@ export function parseNodesFromText(text, regexes, prevNodes, cursor, triggers) {
41
44
  // Boundary-gated regexes shift the effective start past the captured
42
45
  // boundary char (m[1]) and bump the value capture index by one.
43
46
  const hasBoundary = re.source.startsWith(BOUNDARY_PREFIX);
44
- for (const m of text.matchAll(re)) {
47
+ // exec loop instead of matchAll: Hermes's matchAll drops named capture
48
+ // groups (m.groups is undefined), which breaks validate functions that
49
+ // read them. exec populates groups correctly on all engines, and resets
50
+ // lastIndex to 0 itself when it exhausts the matches.
51
+ let m;
52
+ while ((m = re.exec(text)) !== null) {
53
+ // Guard against zero-length matches pinning lastIndex in place
54
+ // (matchAll did this advance implicitly).
55
+ if (m[0] === '')
56
+ re.lastIndex++;
45
57
  if (validate && !validate(m))
46
58
  continue;
47
59
  const boundaryLen = hasBoundary ? (m[1]?.length ?? 0) : 0;
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/\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\ntype CompiledFacet = {\n regex: RegExp\n validate?: (match: RegExpMatchArray) => boolean\n}\n\nexport type CompiledFacetRegexes = Map<string, CompiledFacet>\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. Config values may be a bare RegExp or a\n * {pattern, validate} definition.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfig,\n): CompiledFacetRegexes {\n const compiled = new Map<string, CompiledFacet>()\n for (const [name, def] of Object.entries(config)) {\n const re = def instanceof RegExp ? def : def.pattern\n compiled.set(name, {\n regex: new RegExp(re.source, re.flags),\n validate: def instanceof RegExp ? undefined : def.validate,\n })\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, {regex: re, validate}] 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 if (validate && !validate(m)) continue\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, def] of Object.entries(config)) {\n const re = def instanceof RegExp ? def : def.pattern\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"]}
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\ntype CompiledFacet = {\n regex: RegExp\n validate?: (match: RegExpMatchArray) => boolean\n}\n\nexport type CompiledFacetRegexes = Map<string, CompiledFacet>\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. Config values may be a bare RegExp or a\n * {pattern, validate} definition.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfig,\n): CompiledFacetRegexes {\n const compiled = new Map<string, CompiledFacet>()\n for (const [name, def] of Object.entries(config)) {\n const re = def instanceof RegExp ? def : def.pattern\n // Force the global flag: the exec loop in parseNodesFromText would spin\n // forever on a non-global regex (matchAll used to throw on these).\n const flags = re.flags.includes('g') ? re.flags : re.flags + 'g'\n compiled.set(name, {\n regex: new RegExp(re.source, flags),\n validate: def instanceof RegExp ? undefined : def.validate,\n })\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, {regex: re, validate}] 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 // exec loop instead of matchAll: Hermes's matchAll drops named capture\n // groups (m.groups is undefined), which breaks validate functions that\n // read them. exec populates groups correctly on all engines, and resets\n // lastIndex to 0 itself when it exhausts the matches.\n let m: RegExpExecArray | null\n while ((m = re.exec(text)) !== null) {\n // Guard against zero-length matches pinning lastIndex in place\n // (matchAll did this advance implicitly).\n if (m[0] === '') re.lastIndex++\n if (validate && !validate(m)) continue\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, def] of Object.entries(config)) {\n const re = def instanceof RegExp ? def : def.pattern\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.6.0",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "description": "A minimal rich text editor for React Native and web.",
6
6
  "repository": {
package/src/facets.ts CHANGED
@@ -20,12 +20,18 @@ const TLD_SET = new Set(TLDs)
20
20
  // editor but never become facets. `https?://` matches (m[3]) skip validation,
21
21
  // same as RichText. The TLD check is deliberately case-sensitive: RichText's
22
22
  // isValidDomain is too (`BSKY.APP` never facets), so don't lowercase here.
23
+ //
24
+ // Capture indices are load-bearing: validate reads the domain from m[5]. We
25
+ // can't use the (?<domain>) name at runtime — bundlers (Babel's
26
+ // transform-named-capturing-groups-regex) strip the name from the regex source
27
+ // when an app bundles us, so m.groups.domain reads back undefined. If you
28
+ // reorder or add capture groups in the pattern, update the m[5] index below.
23
29
  export const url: TapperFacetDefinition = {
24
30
  pattern:
25
31
  /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim,
26
32
  validate: m => {
27
33
  if (m[3]) return true
28
- const domain = m.groups?.domain
34
+ const domain = m[5]
29
35
  if (!domain) return false
30
36
  return TLD_SET.has(domain.slice(domain.lastIndexOf('.') + 1))
31
37
  },
package/src/util.ts CHANGED
@@ -37,8 +37,11 @@ export function compileFacetRegexes(
37
37
  const compiled = new Map<string, CompiledFacet>()
38
38
  for (const [name, def] of Object.entries(config)) {
39
39
  const re = def instanceof RegExp ? def : def.pattern
40
+ // Force the global flag: the exec loop in parseNodesFromText would spin
41
+ // forever on a non-global regex (matchAll used to throw on these).
42
+ const flags = re.flags.includes('g') ? re.flags : re.flags + 'g'
40
43
  compiled.set(name, {
41
- regex: new RegExp(re.source, re.flags),
44
+ regex: new RegExp(re.source, flags),
42
45
  validate: def instanceof RegExp ? undefined : def.validate,
43
46
  })
44
47
  }
@@ -66,7 +69,15 @@ export function parseNodesFromText(
66
69
  // Boundary-gated regexes shift the effective start past the captured
67
70
  // boundary char (m[1]) and bump the value capture index by one.
68
71
  const hasBoundary = re.source.startsWith(BOUNDARY_PREFIX)
69
- for (const m of text.matchAll(re)) {
72
+ // exec loop instead of matchAll: Hermes's matchAll drops named capture
73
+ // groups (m.groups is undefined), which breaks validate functions that
74
+ // read them. exec populates groups correctly on all engines, and resets
75
+ // lastIndex to 0 itself when it exhausts the matches.
76
+ let m: RegExpExecArray | null
77
+ while ((m = re.exec(text)) !== null) {
78
+ // Guard against zero-length matches pinning lastIndex in place
79
+ // (matchAll did this advance implicitly).
80
+ if (m[0] === '') re.lastIndex++
70
81
  if (validate && !validate(m)) continue
71
82
  const boundaryLen = hasBoundary ? (m[1]?.length ?? 0) : 0
72
83
  const fullMatch = m[0].slice(boundaryLen)
package/eslint.config.js DELETED
@@ -1,4 +0,0 @@
1
- // @generated by expo-module-scripts
2
- const {defineConfig} = require('eslint/config')
3
- const baseConfig = require('expo-module-scripts/eslint.config.base')
4
- module.exports = defineConfig([baseConfig])
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- // @generated by expo-module-scripts
2
- {
3
- "extends": "expo-module-scripts/tsconfig.base",
4
- "compilerOptions": {
5
- "rootDir": "./src",
6
- "outDir": "./build"
7
- },
8
- "include": ["./src"],
9
- "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
10
- }
package/vitest.config.ts DELETED
@@ -1,13 +0,0 @@
1
- import {defineConfig} from 'vitest/config'
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ['src/__tests__/**/*.test.ts'],
6
- alias: {
7
- 'react-native': new URL(
8
- 'src/__tests__/__mocks__/react-native.ts',
9
- import.meta.url,
10
- ).pathname,
11
- },
12
- },
13
- })