@atcute/bluesky-richtext-builder 2.0.4 → 3.0.0
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/README.md +182 -10
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -19
- package/dist/index.js.map +1 -1
- package/lib/index.ts +22 -20
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,19 +1,191 @@
|
|
|
1
1
|
# @atcute/bluesky-richtext-builder
|
|
2
2
|
|
|
3
|
-
builder
|
|
3
|
+
fluent builder for constructing Bluesky rich text with facets.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @atcute/bluesky-richtext-builder
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Bluesky posts support rich text with mentions, links, and hashtags. these are represented as
|
|
10
|
+
"facets" - byte ranges with attached features. this package provides a builder that handles the byte
|
|
11
|
+
offset calculations for you.
|
|
12
|
+
|
|
13
|
+
## why use a builder?
|
|
14
|
+
|
|
15
|
+
Bluesky facets use byte offsets, not character positions. this matters for non-ASCII text:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// "café" is 5 bytes in UTF-8 (c=1, a=1, f=1, é=2)
|
|
19
|
+
const text = 'café ☕';
|
|
20
|
+
|
|
21
|
+
// the coffee emoji starts at byte 5, not character 5
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
the builder handles these calculations automatically, so you don't have to think about byte offsets.
|
|
25
|
+
|
|
26
|
+
## usage
|
|
27
|
+
|
|
28
|
+
### basic usage
|
|
4
29
|
|
|
5
30
|
```ts
|
|
6
31
|
import RichtextBuilder from '@atcute/bluesky-richtext-builder';
|
|
7
32
|
|
|
8
|
-
const
|
|
9
|
-
.addText(
|
|
10
|
-
.addMention(
|
|
11
|
-
.addText(
|
|
12
|
-
.addLink(
|
|
33
|
+
const rt = new RichtextBuilder()
|
|
34
|
+
.addText('hello, ')
|
|
35
|
+
.addMention('@alice', 'did:plc:abc123')
|
|
36
|
+
.addText('! check out ')
|
|
37
|
+
.addLink('my website', 'https://example.com');
|
|
38
|
+
|
|
39
|
+
console.log(rt.text);
|
|
40
|
+
// -> "hello, @alice! check out my website"
|
|
41
|
+
|
|
42
|
+
console.log(rt.facets);
|
|
43
|
+
// -> [
|
|
44
|
+
// { index: { byteStart: 7, byteEnd: 13 }, features: [{ $type: 'app.bsky.richtext.facet#mention', did: '...' }] },
|
|
45
|
+
// { index: { byteStart: 25, byteEnd: 35 }, features: [{ $type: 'app.bsky.richtext.facet#link', uri: '...' }] }
|
|
46
|
+
// ]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### creating a post
|
|
50
|
+
|
|
51
|
+
use with `@atcute/client` to create a post:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install @atcute/client @atcute/password-session @atcute/bluesky
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { Client, ok } from '@atcute/client';
|
|
59
|
+
import { PasswordSession } from '@atcute/password-session';
|
|
60
|
+
import RichtextBuilder from '@atcute/bluesky-richtext-builder';
|
|
61
|
+
|
|
62
|
+
import type {} from '@atcute/bluesky';
|
|
63
|
+
|
|
64
|
+
const session = await PasswordSession.login({
|
|
65
|
+
service: 'https://bsky.social',
|
|
66
|
+
identifier: 'you.bsky.social',
|
|
67
|
+
password: 'your-app-password',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const rpc = new Client({ handler: session });
|
|
71
|
+
|
|
72
|
+
const rt = new RichtextBuilder()
|
|
73
|
+
.addText('hello ')
|
|
74
|
+
.addMention('@bsky.app', 'did:plc:z72i7hdynmk6r22z27h6tvur')
|
|
75
|
+
.addText('! ')
|
|
76
|
+
.addTag('#atproto', 'atproto');
|
|
77
|
+
|
|
78
|
+
await ok(
|
|
79
|
+
rpc.post('com.atproto.repo.createRecord', {
|
|
80
|
+
input: {
|
|
81
|
+
repo: session.did,
|
|
82
|
+
collection: 'app.bsky.feed.post',
|
|
83
|
+
record: {
|
|
84
|
+
$type: 'app.bsky.feed.post',
|
|
85
|
+
text: rt.text,
|
|
86
|
+
facets: rt.facets,
|
|
87
|
+
createdAt: new Date().toISOString(),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### adding links
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const rt = new RichtextBuilder()
|
|
98
|
+
.addText('read the ')
|
|
99
|
+
.addLink('documentation', 'https://atproto.com/docs')
|
|
100
|
+
.addText(' for more info');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
the link text can be anything - it doesn't have to be the URL:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const rt = new RichtextBuilder().addLink('click here', 'https://example.com');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### adding mentions
|
|
110
|
+
|
|
111
|
+
mentions require both the display text and the user's DID:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const rt = new RichtextBuilder().addMention('@alice.bsky.social', 'did:plc:abc123');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
you'll typically resolve the handle to a DID first:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import {
|
|
121
|
+
CompositeHandleResolver,
|
|
122
|
+
DohJsonHandleResolver,
|
|
123
|
+
WellKnownHandleResolver,
|
|
124
|
+
} from '@atcute/identity-resolver';
|
|
125
|
+
|
|
126
|
+
const handleResolver = new CompositeHandleResolver({
|
|
127
|
+
methods: {
|
|
128
|
+
dns: new DohJsonHandleResolver({
|
|
129
|
+
dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
|
|
130
|
+
}),
|
|
131
|
+
http: new WellKnownHandleResolver(),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const handle = 'alice.bsky.social';
|
|
136
|
+
const did = await handleResolver.resolve(handle);
|
|
137
|
+
|
|
138
|
+
const rt = new RichtextBuilder().addMention(`@${handle}`, did);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### adding hashtags
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const rt = new RichtextBuilder()
|
|
145
|
+
.addText('loving ')
|
|
146
|
+
.addTag('#atproto', 'atproto')
|
|
147
|
+
.addText(' development!');
|
|
148
|
+
|
|
149
|
+
// text: "loving #atproto development!"
|
|
150
|
+
```
|
|
13
151
|
|
|
14
|
-
|
|
15
|
-
|
|
152
|
+
### custom facet features
|
|
153
|
+
|
|
154
|
+
use `addDecoratedText()` for custom facet features:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import type { FacetFeature } from '@atcute/bluesky-richtext-builder';
|
|
158
|
+
|
|
159
|
+
const feature: FacetFeature = {
|
|
160
|
+
$type: 'app.bsky.richtext.facet#link',
|
|
161
|
+
uri: 'https://example.com',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const rt = new RichtextBuilder().addDecoratedText('custom link', feature);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### getting the result
|
|
168
|
+
|
|
169
|
+
there are multiple ways to get the composed rich text:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const rt = new RichtextBuilder().addText('hello ').addTag('#world', 'world');
|
|
173
|
+
|
|
174
|
+
// via getters
|
|
175
|
+
const text = rt.text;
|
|
176
|
+
const facets = rt.facets;
|
|
177
|
+
|
|
178
|
+
// via build() method
|
|
179
|
+
const { text, facets } = rt.build();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### cloning the builder
|
|
183
|
+
|
|
184
|
+
clone a builder to create variations:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const base = new RichtextBuilder().addText('hello ');
|
|
16
188
|
|
|
17
|
-
|
|
18
|
-
|
|
189
|
+
const withMention = base.clone().addMention('@alice', 'did:plc:abc');
|
|
190
|
+
const withLink = base.clone().addLink('world', 'https://example.com');
|
|
19
191
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -23,37 +23,38 @@ declare class RichtextBuilder {
|
|
|
23
23
|
clone(): RichtextBuilder;
|
|
24
24
|
/**
|
|
25
25
|
* Add plain text to the rich text
|
|
26
|
-
* @param
|
|
26
|
+
* @param text The plain text
|
|
27
27
|
* @returns The builder instance, for chaining
|
|
28
28
|
*/
|
|
29
|
-
addText(
|
|
29
|
+
addText(text: string): this;
|
|
30
30
|
/**
|
|
31
31
|
* Add decorated text to the rich text
|
|
32
|
-
* @param
|
|
32
|
+
* @param text The text itself
|
|
33
33
|
* @param feature Feature to imbue on the text
|
|
34
34
|
* @returns The builder instance, for chaining
|
|
35
35
|
*/
|
|
36
|
-
addDecoratedText(
|
|
36
|
+
addDecoratedText(text: string, feature: FacetFeature): this;
|
|
37
37
|
/**
|
|
38
38
|
* Add link to the rich text
|
|
39
|
-
* @param
|
|
39
|
+
* @param text Text of the link
|
|
40
40
|
* @param uri Valid URL, for example: https://example.com
|
|
41
41
|
* @returns The builder instance, for chaining
|
|
42
42
|
*/
|
|
43
|
-
addLink(
|
|
43
|
+
addLink(text: string, uri: GenericUri): this;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param
|
|
45
|
+
* Mention a user in rich text
|
|
46
|
+
* @param text Text of the mention, usually in the form of `@handle`
|
|
47
47
|
* @param did Valid DID, for example: did:plc:ia76kvnndjutgedggx2ibrem
|
|
48
48
|
* @returns The builder instance, for chaining
|
|
49
49
|
*/
|
|
50
|
-
addMention(
|
|
50
|
+
addMention(text: string, did: Did): this;
|
|
51
51
|
/**
|
|
52
52
|
* Add inline hashtag to the rich text
|
|
53
|
+
* @param text Text to display
|
|
53
54
|
* @param tag The tag, without the pound prefix
|
|
54
|
-
* @returns
|
|
55
|
+
* @returns The builder instance, for chaining
|
|
55
56
|
*/
|
|
56
|
-
addTag(tag: string): this;
|
|
57
|
+
addTag(text: string, tag: string): this;
|
|
57
58
|
}
|
|
58
59
|
export default RichtextBuilder;
|
|
59
60
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGxD,KAAK,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAExD,uEAAuE;AACvE,MAAM,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC;AAC9C,gDAAgD;AAChD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;AAE1D,0BAA0B;AAC1B,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB;AAED,kDAAkD;AAClD,cAAM,eAAe;;IAMpB,8BAA8B;IAC9B,IAAI,IAAI,IAAI,MAAM,CASjB;IAED,gCAAgC;IAChC,IAAI,MAAM,IAAI,KAAK,EAAE,CASpB;IAED,sCAAsC;IACtC,KAAK,IAAI,aAAa,CAKrB;IAED,uCAAuC;IACvC,KAAK,IAAI,eAAe,CAKvB;IAED;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK1B;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAyB1D;IAED;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAE3C;IAED;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAEvC;IAED;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtC;CACD;AAED,eAAe,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { getUtf8Length } from '@atcute/uint8array';
|
|
2
2
|
/** Builder for constructing Bluesky rich texts */
|
|
3
3
|
class RichtextBuilder {
|
|
4
4
|
// Even-numbered are substrings, odd-numbered are facets
|
|
@@ -38,65 +38,67 @@ class RichtextBuilder {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Add plain text to the rich text
|
|
41
|
-
* @param
|
|
41
|
+
* @param text The plain text
|
|
42
42
|
* @returns The builder instance, for chaining
|
|
43
43
|
*/
|
|
44
|
-
addText(
|
|
44
|
+
addText(text) {
|
|
45
45
|
const segments = this.#segments;
|
|
46
|
-
segments[segments.length - 1] +=
|
|
46
|
+
segments[segments.length - 1] += text;
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Add decorated text to the rich text
|
|
51
|
-
* @param
|
|
51
|
+
* @param text The text itself
|
|
52
52
|
* @param feature Feature to imbue on the text
|
|
53
53
|
* @returns The builder instance, for chaining
|
|
54
54
|
*/
|
|
55
|
-
addDecoratedText(
|
|
55
|
+
addDecoratedText(text, feature) {
|
|
56
56
|
const segments = this.#segments;
|
|
57
57
|
const last = segments.length - 1;
|
|
58
58
|
// Calculate the starting index
|
|
59
59
|
let start = 0;
|
|
60
|
-
start +=
|
|
60
|
+
start += getUtf8Length(segments[last]);
|
|
61
61
|
if (last !== 0) {
|
|
62
62
|
start += segments[last - 1].index.byteEnd;
|
|
63
63
|
}
|
|
64
|
+
const byteLength = getUtf8Length(text);
|
|
64
65
|
const facet = {
|
|
65
66
|
index: {
|
|
66
67
|
byteStart: start,
|
|
67
|
-
byteEnd: start +
|
|
68
|
+
byteEnd: start + byteLength,
|
|
68
69
|
},
|
|
69
70
|
features: [feature],
|
|
70
71
|
};
|
|
71
|
-
segments[last] +=
|
|
72
|
+
segments[last] += text;
|
|
72
73
|
segments.push(facet, '');
|
|
73
74
|
return this;
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Add link to the rich text
|
|
77
|
-
* @param
|
|
78
|
+
* @param text Text of the link
|
|
78
79
|
* @param uri Valid URL, for example: https://example.com
|
|
79
80
|
* @returns The builder instance, for chaining
|
|
80
81
|
*/
|
|
81
|
-
addLink(
|
|
82
|
-
return this.addDecoratedText(
|
|
82
|
+
addLink(text, uri) {
|
|
83
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#link', uri: uri });
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @param
|
|
86
|
+
* Mention a user in rich text
|
|
87
|
+
* @param text Text of the mention, usually in the form of `@handle`
|
|
87
88
|
* @param did Valid DID, for example: did:plc:ia76kvnndjutgedggx2ibrem
|
|
88
89
|
* @returns The builder instance, for chaining
|
|
89
90
|
*/
|
|
90
|
-
addMention(
|
|
91
|
-
return this.addDecoratedText(
|
|
91
|
+
addMention(text, did) {
|
|
92
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#mention', did: did });
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
94
95
|
* Add inline hashtag to the rich text
|
|
96
|
+
* @param text Text to display
|
|
95
97
|
* @param tag The tag, without the pound prefix
|
|
96
|
-
* @returns
|
|
98
|
+
* @returns The builder instance, for chaining
|
|
97
99
|
*/
|
|
98
|
-
addTag(tag) {
|
|
99
|
-
return this.addDecoratedText(
|
|
100
|
+
addTag(text, tag) {
|
|
101
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#tag', tag: tag });
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
export default RichtextBuilder;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"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"}
|
package/lib/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppBskyRichtextFacet } from '@atcute/bluesky';
|
|
2
2
|
import type { Did, GenericUri } from '@atcute/lexicons';
|
|
3
|
+
import { getUtf8Length } from '@atcute/uint8array';
|
|
3
4
|
|
|
4
5
|
type UnwrapArray<T> = T extends (infer V)[] ? V : never;
|
|
5
6
|
|
|
@@ -8,8 +9,6 @@ export type Facet = AppBskyRichtextFacet.Main;
|
|
|
8
9
|
/** Feature union type from Facet['features'] */
|
|
9
10
|
export type FacetFeature = UnwrapArray<Facet['features']>;
|
|
10
11
|
|
|
11
|
-
const encoder = new TextEncoder();
|
|
12
|
-
|
|
13
12
|
/** Resulting rich text */
|
|
14
13
|
export interface BakedRichtext {
|
|
15
14
|
text: string;
|
|
@@ -65,74 +64,77 @@ class RichtextBuilder {
|
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Add plain text to the rich text
|
|
68
|
-
* @param
|
|
67
|
+
* @param text The plain text
|
|
69
68
|
* @returns The builder instance, for chaining
|
|
70
69
|
*/
|
|
71
|
-
addText(
|
|
70
|
+
addText(text: string): this {
|
|
72
71
|
const segments = this.#segments;
|
|
73
|
-
segments[segments.length - 1] +=
|
|
72
|
+
segments[segments.length - 1] += text;
|
|
74
73
|
|
|
75
74
|
return this;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* Add decorated text to the rich text
|
|
80
|
-
* @param
|
|
79
|
+
* @param text The text itself
|
|
81
80
|
* @param feature Feature to imbue on the text
|
|
82
81
|
* @returns The builder instance, for chaining
|
|
83
82
|
*/
|
|
84
|
-
addDecoratedText(
|
|
83
|
+
addDecoratedText(text: string, feature: FacetFeature): this {
|
|
85
84
|
const segments = this.#segments;
|
|
86
85
|
const last = segments.length - 1;
|
|
87
86
|
|
|
88
87
|
// Calculate the starting index
|
|
89
88
|
let start = 0;
|
|
90
89
|
|
|
91
|
-
start +=
|
|
90
|
+
start += getUtf8Length(segments[last] as string);
|
|
92
91
|
if (last !== 0) {
|
|
93
92
|
start += (segments[last - 1] as Facet).index.byteEnd;
|
|
94
93
|
}
|
|
95
94
|
|
|
95
|
+
const byteLength = getUtf8Length(text);
|
|
96
|
+
|
|
96
97
|
const facet: Facet = {
|
|
97
98
|
index: {
|
|
98
99
|
byteStart: start,
|
|
99
|
-
byteEnd: start +
|
|
100
|
+
byteEnd: start + byteLength,
|
|
100
101
|
},
|
|
101
102
|
features: [feature],
|
|
102
103
|
};
|
|
103
104
|
|
|
104
|
-
segments[last] +=
|
|
105
|
+
segments[last] += text;
|
|
105
106
|
segments.push(facet, '');
|
|
106
107
|
return this;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* Add link to the rich text
|
|
111
|
-
* @param
|
|
112
|
+
* @param text Text of the link
|
|
112
113
|
* @param uri Valid URL, for example: https://example.com
|
|
113
114
|
* @returns The builder instance, for chaining
|
|
114
115
|
*/
|
|
115
|
-
addLink(
|
|
116
|
-
return this.addDecoratedText(
|
|
116
|
+
addLink(text: string, uri: GenericUri): this {
|
|
117
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#link', uri: uri });
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
*
|
|
121
|
-
* @param
|
|
121
|
+
* Mention a user in rich text
|
|
122
|
+
* @param text Text of the mention, usually in the form of `@handle`
|
|
122
123
|
* @param did Valid DID, for example: did:plc:ia76kvnndjutgedggx2ibrem
|
|
123
124
|
* @returns The builder instance, for chaining
|
|
124
125
|
*/
|
|
125
|
-
addMention(
|
|
126
|
-
return this.addDecoratedText(
|
|
126
|
+
addMention(text: string, did: Did): this {
|
|
127
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#mention', did: did });
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Add inline hashtag to the rich text
|
|
132
|
+
* @param text Text to display
|
|
131
133
|
* @param tag The tag, without the pound prefix
|
|
132
|
-
* @returns
|
|
134
|
+
* @returns The builder instance, for chaining
|
|
133
135
|
*/
|
|
134
|
-
addTag(tag: string): this {
|
|
135
|
-
return this.addDecoratedText(
|
|
136
|
+
addTag(text: string, tag: string): this {
|
|
137
|
+
return this.addDecoratedText(text, { $type: 'app.bsky.richtext.facet#tag', tag: tag });
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"type": "module",
|
|
3
2
|
"name": "@atcute/bluesky-richtext-builder",
|
|
4
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
5
4
|
"description": "builder pattern for Bluesky's rich text facets",
|
|
6
5
|
"license": "0BSD",
|
|
7
6
|
"repository": {
|
|
@@ -14,20 +13,21 @@
|
|
|
14
13
|
"!lib/**/*.bench.ts",
|
|
15
14
|
"!lib/**/*.test.ts"
|
|
16
15
|
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
17
18
|
"exports": {
|
|
18
19
|
".": "./dist/index.js"
|
|
19
20
|
},
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"@atcute/bluesky": "^3.2.5",
|
|
23
|
-
"@atcute/lexicons": "^1.2.2"
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
24
23
|
},
|
|
25
|
-
"
|
|
26
|
-
"@
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@atcute/bluesky": "^3.2.21",
|
|
26
|
+
"@atcute/lexicons": "^1.2.9",
|
|
27
|
+
"@atcute/uint8array": "^1.1.1"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
|
-
"build": "
|
|
30
|
-
"test": "bun test --coverage",
|
|
30
|
+
"build": "tsgo --project tsconfig.build.json",
|
|
31
31
|
"prepublish": "rm -rf dist; pnpm run build"
|
|
32
32
|
}
|
|
33
33
|
}
|