@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 +9 -0
- package/dist/rich-text/detection.d.ts.map +1 -1
- package/dist/rich-text/detection.js +25 -0
- package/dist/rich-text/detection.js.map +1 -1
- package/dist/rich-text/util.d.ts +1 -0
- package/dist/rich-text/util.d.ts.map +1 -1
- package/dist/rich-text/util.js +2 -1
- package/dist/rich-text/util.js.map +1 -1
- package/package.json +2 -2
- package/src/rich-text/detection.ts +29 -0
- package/src/rich-text/util.ts +3 -0
- package/tests/rich-text-detection.test.ts +68 -0
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;
|
|
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":";;;;;
|
|
1
|
+
{"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/rich-text/detection.ts"],"names":[],"mappings":";;;;;AAaA,oCA0HC;AAvID,gDAAuB;AAGvB,iCAMe;AAIf,SAAgB,YAAY,CAAC,IAAmB;IAC9C,IAAI,KAAK,CAAA;IACT,MAAM,MAAM,GAAY,EAAE,CAAA;IAC1B,CAAC;QACC,WAAW;QACX,MAAM,EAAE,GAAG,oBAAa,CAAA;QACxB,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5D,SAAQ,CAAC,wBAAwB;YACnC,CAAC;YAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3D,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,yBAAyB;gBAChC,KAAK,EAAE;oBACL,SAAS,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;oBAC5C,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;iBACjE;gBACD,QAAQ,EAAE;oBACR;wBACE,KAAK,EAAE,iCAAiC;wBACxC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,8BAA8B;qBAC9C;iBACF;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,CAAC;QACC,QAAQ;QACR,MAAM,EAAE,GAAG,gBAAS,CAAA;QACpB,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACrC,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YAClB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,MAAM,CAAA;gBACnC,IAAI,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;oBACtC,SAAQ;gBACV,CAAC;gBACD,GAAG,GAAG,WAAW,GAAG,EAAE,CAAA;YACxB,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;YACvD,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;YACrD,0BAA0B;YAC1B,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;gBACtB,KAAK,CAAC,GAAG,EAAE,CAAA;YACb,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3C,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;gBACtB,KAAK,CAAC,GAAG,EAAE,CAAA;YACb,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE;oBACL,SAAS,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC;oBAClD,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC;iBAC/C;gBACD,QAAQ,EAAE;oBACR;wBACE,KAAK,EAAE,8BAA8B;wBACrC,GAAG;qBACJ;iBACF;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,CAAC;QACC,MAAM,EAAE,GAAG,gBAAS,CAAA;QACpB,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACxB,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YAElB,IAAI,CAAC,GAAG;gBAAE,SAAQ;YAElB,0CAA0C;YAC1C,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,iCAA0B,EAAE,EAAE,CAAC,CAAA;YAExD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,EAAE;gBAAE,SAAQ;YAEjD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,CAAA;YAE1C,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE;oBACL,SAAS,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;oBAC5C,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC;iBAC5D;gBACD,QAAQ,EAAE;oBACR;wBACE,KAAK,EAAE,6BAA6B;wBACpC,GAAG,EAAE,GAAG;qBACT;iBACF;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,CAAC;QACC,WAAW;QACX,MAAM,EAAE,GAAG,oBAAa,CAAA;QACxB,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACxB,IAAI,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YAErB,IAAI,CAAC,MAAM;gBAAE,SAAQ;YAErB,yBAAyB;YACzB,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,CAAA;YAE7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,CAAA;YAE1C,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE;oBACL,SAAS,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;oBAC5C,OAAO,EAAE,IAAI,CAAC,qBAAqB,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW;iBAC5E;gBACD,QAAQ,EAAE;oBACR;wBACE,KAAK,EAAE,6BAA6B;wBACpC,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,sBAAsB;qBAC1C;iBACF;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/C,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,OAAO,CAAC,CAAC,cAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QAC9B,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACb,OAAO,KAAK,CAAA;QACd,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC","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"]}
|
package/dist/rich-text/util.d.ts
CHANGED
|
@@ -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"}
|
package/dist/rich-text/util.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
package/src/rich-text/util.ts
CHANGED
|
@@ -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[] {
|