@bsky.app/tapper 0.3.0 → 0.3.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,11 @@
1
1
  # @bsky.app/tapper
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`27b4748`](https://github.com/bluesky-social/toolbox/commit/27b4748ea3c9b2ed45b7979f0431b6eed2b908d3) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Misc fixes
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
@@ -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;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,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,CAsCpB"}
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,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
@@ -172,7 +172,10 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
172
172
  range: { start: node.start, end: node.end },
173
173
  };
174
174
  }
175
- if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
175
+ if (node.type === 'facet' &&
176
+ !node.committed &&
177
+ node.start < cursor &&
178
+ cursor <= node.end) {
176
179
  return {
177
180
  type: node.facetType,
178
181
  raw: node.raw,
@@ -181,21 +184,25 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
181
184
  };
182
185
  }
183
186
  }
184
- // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)
185
- for (let i = cursor - 1; i >= 0; i--) {
186
- const ch = text[i];
187
- // Stop at whitespace — triggers don't span across words
188
- if (WHITESPACE.test(ch))
189
- break;
190
- const type = triggers.get(ch);
191
- if (type) {
192
- const raw = text.slice(i, cursor);
193
- return {
194
- type,
195
- raw,
196
- value: text.slice(i + 1, cursor),
197
- range: { start: i, end: cursor },
198
- };
187
+ // Scan backward from cursor for a trigger char (partial facet not yet matched by regex).
188
+ // Skip if the cursor is inside or at the end of a committed facet.
189
+ const inCommitted = nodes.some(n => n.type === 'facet' && n.committed && n.start < cursor && cursor <= n.end);
190
+ if (!inCommitted) {
191
+ for (let i = cursor - 1; i >= 0; i--) {
192
+ const ch = text[i];
193
+ // Stop at whitespace — triggers don't span across words
194
+ if (WHITESPACE.test(ch))
195
+ break;
196
+ const type = triggers.get(ch);
197
+ if (type) {
198
+ const raw = text.slice(i, cursor);
199
+ return {
200
+ type,
201
+ raw,
202
+ value: text.slice(i + 1, cursor),
203
+ range: { start: i, end: cursor },
204
+ };
205
+ }
199
206
  }
200
207
  }
201
208
  return null;
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\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 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 (node.type === 'facet' && 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 }\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 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 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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsky.app/tapper",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
package/src/util.ts CHANGED
@@ -210,7 +210,12 @@ export function detectActiveFacet(
210
210
  range: {start: node.start, end: node.end},
211
211
  }
212
212
  }
213
- if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
213
+ if (
214
+ node.type === 'facet' &&
215
+ !node.committed &&
216
+ node.start < cursor &&
217
+ cursor <= node.end
218
+ ) {
214
219
  return {
215
220
  type: node.facetType,
216
221
  raw: node.raw,
@@ -220,19 +225,26 @@ export function detectActiveFacet(
220
225
  }
221
226
  }
222
227
 
223
- // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)
224
- for (let i = cursor - 1; i >= 0; i--) {
225
- const ch = text[i]
226
- // Stop at whitespace — triggers don't span across words
227
- if (WHITESPACE.test(ch)) break
228
- const type = triggers.get(ch)
229
- if (type) {
230
- const raw = text.slice(i, cursor)
231
- return {
232
- type,
233
- raw,
234
- value: text.slice(i + 1, cursor),
235
- range: {start: i, end: cursor},
228
+ // Scan backward from cursor for a trigger char (partial facet not yet matched by regex).
229
+ // Skip if the cursor is inside or at the end of a committed facet.
230
+ const inCommitted = nodes.some(
231
+ n =>
232
+ n.type === 'facet' && n.committed && n.start < cursor && cursor <= n.end,
233
+ )
234
+ if (!inCommitted) {
235
+ for (let i = cursor - 1; i >= 0; i--) {
236
+ const ch = text[i]
237
+ // Stop at whitespace — triggers don't span across words
238
+ if (WHITESPACE.test(ch)) break
239
+ const type = triggers.get(ch)
240
+ if (type) {
241
+ const raw = text.slice(i, cursor)
242
+ return {
243
+ type,
244
+ raw,
245
+ value: text.slice(i + 1, cursor),
246
+ range: {start: i, end: cursor},
247
+ }
236
248
  }
237
249
  }
238
250
  }