@atproto/api 0.18.13 → 0.18.14

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,14 @@
1
1
  # @atproto/api
2
2
 
3
+ ## 0.18.14
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4539](https://github.com/bluesky-social/atproto/pull/4539) [`3ffebd0`](https://github.com/bluesky-social/atproto/commit/3ffebd0bf25776308e06e4b083dc2d0e156d9ac0) Thanks [@mozzius](https://github.com/mozzius)! - Add $cashtag support to the Rich Text facet detection
8
+
9
+ - Updated dependencies []:
10
+ - @atproto/common-web@0.4.12
11
+
3
12
  ## 0.18.13
4
13
 
5
14
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"detection.d.ts","sourceRoot":"","sources":["../../src/rich-text/detection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAQzC,MAAM,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAA;AAE7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,KAAK,EAAE,GAAG,SAAS,CA8FrE"}
1
+ {"version":3,"file":"detection.d.ts","sourceRoot":"","sources":["../../src/rich-text/detection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AASzC,MAAM,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAA;AAE7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,KAAK,EAAE,GAAG,SAAS,CA0HrE"}
@@ -95,6 +95,31 @@ function detectFacets(text) {
95
95
  });
96
96
  }
97
97
  }
98
+ {
99
+ // cashtags
100
+ const re = util_1.CASHTAG_REGEX;
101
+ while ((match = re.exec(text.utf16))) {
102
+ const leading = match[1];
103
+ let ticker = match[2];
104
+ if (!ticker)
105
+ continue;
106
+ // Normalize to uppercase
107
+ ticker = ticker.toUpperCase();
108
+ const index = match.index + leading.length;
109
+ facets.push({
110
+ index: {
111
+ byteStart: text.utf16IndexToUtf8Index(index),
112
+ byteEnd: text.utf16IndexToUtf8Index(index + 1 + ticker.length), // +1 for $
113
+ },
114
+ features: [
115
+ {
116
+ $type: 'app.bsky.richtext.facet#tag',
117
+ tag: '$' + ticker, // Store with $ prefix
118
+ },
119
+ ],
120
+ });
121
+ }
122
+ }
98
123
  return facets.length > 0 ? facets : undefined;
99
124
  }
100
125
  function isValidDomain(str) {
@@ -1 +1 @@
1
- {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/rich-text/detection.ts"],"names":[],"mappings":"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","sourcesContent":["import TLDs from 'tlds'\nimport { AppBskyRichtextFacet } from '../client'\nimport { UnicodeString } from './unicode'\nimport {\n MENTION_REGEX,\n TAG_REGEX,\n TRAILING_PUNCTUATION_REGEX,\n URL_REGEX,\n} from './util'\n\nexport type Facet = AppBskyRichtextFacet.Main\n\nexport function detectFacets(text: UnicodeString): Facet[] | undefined {\n let match\n const facets: Facet[] = []\n {\n // mentions\n const re = MENTION_REGEX\n while ((match = re.exec(text.utf16))) {\n if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) {\n continue // probably not a handle\n }\n\n const start = text.utf16.indexOf(match[3], match.index) - 1\n facets.push({\n $type: 'app.bsky.richtext.facet',\n index: {\n byteStart: text.utf16IndexToUtf8Index(start),\n byteEnd: text.utf16IndexToUtf8Index(start + match[3].length + 1),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#mention',\n did: match[3], // must be resolved afterwards\n },\n ],\n })\n }\n }\n {\n // links\n const re = URL_REGEX\n while ((match = re.exec(text.utf16))) {\n let uri = match[2]\n if (!uri.startsWith('http')) {\n const domain = match.groups?.domain\n if (!domain || !isValidDomain(domain)) {\n continue\n }\n uri = `https://${uri}`\n }\n const start = text.utf16.indexOf(match[2], match.index)\n const index = { start, end: start + match[2].length }\n // strip ending puncuation\n if (/[.,;:!?]$/.test(uri)) {\n uri = uri.slice(0, -1)\n index.end--\n }\n if (/[)]$/.test(uri) && !uri.includes('(')) {\n uri = uri.slice(0, -1)\n index.end--\n }\n facets.push({\n index: {\n byteStart: text.utf16IndexToUtf8Index(index.start),\n byteEnd: text.utf16IndexToUtf8Index(index.end),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#link',\n uri,\n },\n ],\n })\n }\n }\n {\n const re = TAG_REGEX\n while ((match = re.exec(text.utf16))) {\n const leading = match[1]\n let tag = match[2]\n\n if (!tag) continue\n\n // strip ending punctuation and any spaces\n tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '')\n\n if (tag.length === 0 || tag.length > 64) continue\n\n const index = match.index + leading.length\n\n facets.push({\n index: {\n byteStart: text.utf16IndexToUtf8Index(index),\n byteEnd: text.utf16IndexToUtf8Index(index + 1 + tag.length),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#tag',\n tag: tag,\n },\n ],\n })\n }\n }\n return facets.length > 0 ? facets : undefined\n}\n\nfunction isValidDomain(str: string): boolean {\n return !!TLDs.find((tld) => {\n const i = str.lastIndexOf(tld)\n if (i === -1) {\n return false\n }\n return str.charAt(i - 1) === '.' && i === str.length - tld.length\n })\n}\n"]}
1
+ {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/rich-text/detection.ts"],"names":[],"mappings":"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","sourcesContent":["import TLDs from 'tlds'\nimport { AppBskyRichtextFacet } from '../client'\nimport { UnicodeString } from './unicode'\nimport {\n CASHTAG_REGEX,\n MENTION_REGEX,\n TAG_REGEX,\n TRAILING_PUNCTUATION_REGEX,\n URL_REGEX,\n} from './util'\n\nexport type Facet = AppBskyRichtextFacet.Main\n\nexport function detectFacets(text: UnicodeString): Facet[] | undefined {\n let match\n const facets: Facet[] = []\n {\n // mentions\n const re = MENTION_REGEX\n while ((match = re.exec(text.utf16))) {\n if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) {\n continue // probably not a handle\n }\n\n const start = text.utf16.indexOf(match[3], match.index) - 1\n facets.push({\n $type: 'app.bsky.richtext.facet',\n index: {\n byteStart: text.utf16IndexToUtf8Index(start),\n byteEnd: text.utf16IndexToUtf8Index(start + match[3].length + 1),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#mention',\n did: match[3], // must be resolved afterwards\n },\n ],\n })\n }\n }\n {\n // links\n const re = URL_REGEX\n while ((match = re.exec(text.utf16))) {\n let uri = match[2]\n if (!uri.startsWith('http')) {\n const domain = match.groups?.domain\n if (!domain || !isValidDomain(domain)) {\n continue\n }\n uri = `https://${uri}`\n }\n const start = text.utf16.indexOf(match[2], match.index)\n const index = { start, end: start + match[2].length }\n // strip ending puncuation\n if (/[.,;:!?]$/.test(uri)) {\n uri = uri.slice(0, -1)\n index.end--\n }\n if (/[)]$/.test(uri) && !uri.includes('(')) {\n uri = uri.slice(0, -1)\n index.end--\n }\n facets.push({\n index: {\n byteStart: text.utf16IndexToUtf8Index(index.start),\n byteEnd: text.utf16IndexToUtf8Index(index.end),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#link',\n uri,\n },\n ],\n })\n }\n }\n {\n const re = TAG_REGEX\n while ((match = re.exec(text.utf16))) {\n const leading = match[1]\n let tag = match[2]\n\n if (!tag) continue\n\n // strip ending punctuation and any spaces\n tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '')\n\n if (tag.length === 0 || tag.length > 64) continue\n\n const index = match.index + leading.length\n\n facets.push({\n index: {\n byteStart: text.utf16IndexToUtf8Index(index),\n byteEnd: text.utf16IndexToUtf8Index(index + 1 + tag.length),\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#tag',\n tag: tag,\n },\n ],\n })\n }\n }\n {\n // cashtags\n const re = CASHTAG_REGEX\n while ((match = re.exec(text.utf16))) {\n const leading = match[1]\n let ticker = match[2]\n\n if (!ticker) continue\n\n // Normalize to uppercase\n ticker = ticker.toUpperCase()\n\n const index = match.index + leading.length\n\n facets.push({\n index: {\n byteStart: text.utf16IndexToUtf8Index(index),\n byteEnd: text.utf16IndexToUtf8Index(index + 1 + ticker.length), // +1 for $\n },\n features: [\n {\n $type: 'app.bsky.richtext.facet#tag',\n tag: '$' + ticker, // Store with $ prefix\n },\n ],\n })\n }\n }\n return facets.length > 0 ? facets : undefined\n}\n\nfunction isValidDomain(str: string): boolean {\n return !!TLDs.find((tld) => {\n const i = str.lastIndexOf(tld)\n if (i === -1) {\n return false\n }\n return str.charAt(i - 1) === '.' && i === str.length - tld.length\n })\n}\n"]}
@@ -6,4 +6,5 @@ export declare const TRAILING_PUNCTUATION_REGEX: RegExp;
6
6
  * `\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` zero-width spaces (likely incomplete)
7
7
  */
8
8
  export declare const TAG_REGEX: RegExp;
9
+ export declare const CASHTAG_REGEX: RegExp;
9
10
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/rich-text/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,QAAsC,CAAA;AAChE,eAAO,MAAM,SAAS,QAC6D,CAAA;AACnF,eAAO,MAAM,0BAA0B,QAAc,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,SAAS,QAE8J,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/rich-text/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,QAAsC,CAAA;AAChE,eAAO,MAAM,SAAS,QAC6D,CAAA;AACnF,eAAO,MAAM,0BAA0B,QAAc,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,SAAS,QAE8J,CAAA;AAEpL,eAAO,MAAM,aAAa,QAC2C,CAAA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TAG_REGEX = exports.TRAILING_PUNCTUATION_REGEX = exports.URL_REGEX = exports.MENTION_REGEX = void 0;
3
+ exports.CASHTAG_REGEX = exports.TAG_REGEX = exports.TRAILING_PUNCTUATION_REGEX = exports.URL_REGEX = exports.MENTION_REGEX = void 0;
4
4
  exports.MENTION_REGEX = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g;
5
5
  exports.URL_REGEX = /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim;
6
6
  exports.TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu;
@@ -11,4 +11,5 @@ exports.TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu;
11
11
  exports.TAG_REGEX =
12
12
  // eslint-disable-next-line no-misleading-character-class
13
13
  /(^|\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]*)?/gu;
14
+ exports.CASHTAG_REGEX = /(^|\s|\()\$([A-Za-z][A-Za-z0-9]{0,4})(?=\s|$|[.,;:!?)"'\u2019])/gu;
14
15
  //# sourceMappingURL=util.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/rich-text/util.ts"],"names":[],"mappings":";;;AAAa,QAAA,aAAa,GAAG,mCAAmC,CAAA;AACnD,QAAA,SAAS,GACpB,iFAAiF,CAAA;AACtE,QAAA,0BAA0B,GAAG,WAAW,CAAA;AAErD;;;GAGG;AACU,QAAA,SAAS;AACpB,yDAAyD;AACzD,kLAAkL,CAAA","sourcesContent":["export const MENTION_REGEX = /(^|\\s|\\()(@)([a-zA-Z0-9.-]+)(\\b)/g\nexport const URL_REGEX =\n /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim\nexport const TRAILING_PUNCTUATION_REGEX = /\\p{P}+$/gu\n\n/**\n * `\\ufe0f` emoji modifier\n * `\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2` zero-width spaces (likely incomplete)\n */\nexport const TAG_REGEX =\n // eslint-disable-next-line no-misleading-character-class\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]*)?/gu\n"]}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/rich-text/util.ts"],"names":[],"mappings":";;;AAAa,QAAA,aAAa,GAAG,mCAAmC,CAAA;AACnD,QAAA,SAAS,GACpB,iFAAiF,CAAA;AACtE,QAAA,0BAA0B,GAAG,WAAW,CAAA;AAErD;;;GAGG;AACU,QAAA,SAAS;AACpB,yDAAyD;AACzD,kLAAkL,CAAA;AAEvK,QAAA,aAAa,GACxB,mEAAmE,CAAA","sourcesContent":["export const MENTION_REGEX = /(^|\\s|\\()(@)([a-zA-Z0-9.-]+)(\\b)/g\nexport const URL_REGEX =\n /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim\nexport const TRAILING_PUNCTUATION_REGEX = /\\p{P}+$/gu\n\n/**\n * `\\ufe0f` emoji modifier\n * `\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2` zero-width spaces (likely incomplete)\n */\nexport const TAG_REGEX =\n // eslint-disable-next-line no-misleading-character-class\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]*)?/gu\n\nexport const CASHTAG_REGEX =\n /(^|\\s|\\()\\$([A-Za-z][A-Za-z0-9]{0,4})(?=\\s|$|[.,;:!?)\"'\\u2019])/gu\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/api",
3
- "version": "0.18.13",
3
+ "version": "0.18.14",
4
4
  "license": "MIT",
5
5
  "description": "Client library for atproto and Bluesky",
6
6
  "keywords": [
@@ -21,7 +21,7 @@
21
21
  "multiformats": "^9.9.0",
22
22
  "tlds": "^1.234.0",
23
23
  "zod": "^3.23.8",
24
- "@atproto/common-web": "^0.4.11",
24
+ "@atproto/common-web": "^0.4.12",
25
25
  "@atproto/lexicon": "^0.6.0",
26
26
  "@atproto/syntax": "^0.4.2",
27
27
  "@atproto/xrpc": "^0.7.7"
@@ -2,6 +2,7 @@ import TLDs from 'tlds'
2
2
  import { AppBskyRichtextFacet } from '../client'
3
3
  import { UnicodeString } from './unicode'
4
4
  import {
5
+ CASHTAG_REGEX,
5
6
  MENTION_REGEX,
6
7
  TAG_REGEX,
7
8
  TRAILING_PUNCTUATION_REGEX,
@@ -103,6 +104,34 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined {
103
104
  })
104
105
  }
105
106
  }
107
+ {
108
+ // cashtags
109
+ const re = CASHTAG_REGEX
110
+ while ((match = re.exec(text.utf16))) {
111
+ const leading = match[1]
112
+ let ticker = match[2]
113
+
114
+ if (!ticker) continue
115
+
116
+ // Normalize to uppercase
117
+ ticker = ticker.toUpperCase()
118
+
119
+ const index = match.index + leading.length
120
+
121
+ facets.push({
122
+ index: {
123
+ byteStart: text.utf16IndexToUtf8Index(index),
124
+ byteEnd: text.utf16IndexToUtf8Index(index + 1 + ticker.length), // +1 for $
125
+ },
126
+ features: [
127
+ {
128
+ $type: 'app.bsky.richtext.facet#tag',
129
+ tag: '$' + ticker, // Store with $ prefix
130
+ },
131
+ ],
132
+ })
133
+ }
134
+ }
106
135
  return facets.length > 0 ? facets : undefined
107
136
  }
108
137
 
@@ -10,3 +10,6 @@ export const TRAILING_PUNCTUATION_REGEX = /\p{P}+$/gu
10
10
  export const TAG_REGEX =
11
11
  // eslint-disable-next-line no-misleading-character-class
12
12
  /(^|\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]*)?/gu
13
+
14
+ export const CASHTAG_REGEX =
15
+ /(^|\s|\()\$([A-Za-z][A-Za-z0-9]{0,4})(?=\s|$|[.,;:!?)"'\u2019])/gu
@@ -371,6 +371,74 @@ describe('detectFacets', () => {
371
371
  expect(detectedIndices).toEqual(indices)
372
372
  })
373
373
  })
374
+
375
+ describe('correctly detects cashtags inline', () => {
376
+ const inputs: [
377
+ string,
378
+ string[],
379
+ { byteStart: number; byteEnd: number }[],
380
+ ][] = [
381
+ ['$AAPL', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]],
382
+ ['$aapl', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // normalized to uppercase
383
+ ['$A', ['$A'], [{ byteStart: 0, byteEnd: 2 }]],
384
+ ['$a', ['$A'], [{ byteStart: 0, byteEnd: 2 }]], // single char normalized
385
+ [
386
+ '$BTC $ETH',
387
+ ['$BTC', '$ETH'],
388
+ [
389
+ { byteStart: 0, byteEnd: 4 },
390
+ { byteStart: 5, byteEnd: 9 },
391
+ ],
392
+ ],
393
+ ['$100', [], []], // starts with digit - not a cashtag
394
+ ['$GOOGL', ['$GOOGL'], [{ byteStart: 0, byteEnd: 6 }]], // 5 chars - max length
395
+ ['$TOOLONG', [], []], // >5 chars
396
+ ['check $LEGO now', ['$LEGO'], [{ byteStart: 6, byteEnd: 11 }]],
397
+ ['($GOOG)', ['$GOOG'], [{ byteStart: 1, byteEnd: 6 }]],
398
+ ['$AAPL.', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // trailing punctuation
399
+ [
400
+ '$AAPL, $MSFT!',
401
+ ['$AAPL', '$MSFT'],
402
+ [
403
+ { byteStart: 0, byteEnd: 5 },
404
+ { byteStart: 7, byteEnd: 12 },
405
+ ],
406
+ ],
407
+ ['no$SPACE', [], []], // must have leading space or start
408
+ ['$', [], []], // just dollar sign
409
+ ['$ AAPL', [], []], // space after $
410
+ ['$123ABC', [], []], // starts with digit
411
+ ['$ABC12', ['$ABC12'], [{ byteStart: 0, byteEnd: 6 }]], // digits after letters OK (5 chars)
412
+ ['$ABC123', [], []], // 6 chars - too long
413
+ ]
414
+
415
+ it.each(inputs)('%s', (input, tags, indices) => {
416
+ const rt = new RichText({ text: input })
417
+ rt.detectFacetsWithoutResolution()
418
+
419
+ const detectedTags: string[] = []
420
+ const detectedIndices: { byteStart: number; byteEnd: number }[] = []
421
+
422
+ for (const { facet } of rt.segments()) {
423
+ if (!facet) continue
424
+ for (const feature of facet.features) {
425
+ if (isTag(feature) && feature.tag.startsWith('$')) {
426
+ detectedTags.push(feature.tag)
427
+ }
428
+ }
429
+ if (
430
+ facet.features.some(
431
+ (f) => isTag(f) && (f as any).tag?.startsWith('$'),
432
+ )
433
+ ) {
434
+ detectedIndices.push(facet.index)
435
+ }
436
+ }
437
+
438
+ expect(detectedTags).toEqual(tags)
439
+ expect(detectedIndices).toEqual(indices)
440
+ })
441
+ })
374
442
  })
375
443
 
376
444
  function segmentToOutput(segment: RichTextSegment): string[] {