-5
.changeset/tasty-humans-swim.md
-5
.changeset/tasty-humans-swim.md
+50
CHANGELOG.md
+50
CHANGELOG.md
···
···
1
+
# leaflet-loader-astro
2
+
3
+
## 1.3.0
4
+
5
+
### Minor Changes
6
+
7
+
- eb3bc4b: Add iframe block
8
+
9
+
## 1.2.0
10
+
11
+
### Minor Changes
12
+
13
+
- f920153: Add support for blockquotes
14
+
15
+
### Patch Changes
16
+
17
+
- 8922bb1: Add JSDoc comments for available loader options for leafletStaticLoader and leafletLiveLoader
18
+
19
+
## 1.1.0
20
+
21
+
### Minor Changes
22
+
23
+
- 6d70cc6: Added support for these leaflet blocks:
24
+
25
+
- ul/li
26
+
- math
27
+
- code
28
+
- img
29
+
- hr
30
+
31
+
the only remaining block to implement is "website", though I haven't thought of a good way to output that yet. stay tuned for a further release
32
+
33
+
- 5524ce5: Added the ability to use a handle or did when specifying a repo for leafletStaticLoader and leafletLiveLoader
34
+
35
+
```ts
36
+
import { defineLiveCollection } from "astro:content";
37
+
import { leafletLiveLoader } from "leaflet-loader-astro";
38
+
39
+
const documents = defineLiveCollection({
40
+
loader: leafletLiveLoader({ repo: "dane.computer" }), // or repo: did:plc:qttsv4e7pu2jl3ilanfgc3zn, both work!
41
+
});
42
+
43
+
export const collections = { documents };
44
+
```
45
+
46
+
## 1.0.0
47
+
48
+
### Major Changes
49
+
50
+
- b4309c0: This is the initial release for `leaflet-loader-astro`
+60
-7
README.md
+60
-7
README.md
···
10
## Installation
11
12
```bash
13
-
npm install leaflet-loader-astro
14
```
15
16
## Usage
17
18
-
### Build-time loader: leafletStaticLoader (recommended)
19
20
```ts
21
// src/content.config.ts
22
import { defineCollection, z } from "astro:content";
23
-
import { leafletStaticLoader } from "leaflet-loader-astro";
24
25
const documents = defineCollection({
26
-
loader: leafletStaticLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }),
27
});
28
29
export const collections = { documents };
···
81
82
<Content />
83
```
84
85
-
### Live loader: leafletLiveLoader
86
87
```ts
88
// astro.config.mjs
···
101
```ts
102
// src/live.config.ts
103
import { defineLiveCollection, z } from "astro:content";
104
-
import { leafletLiveLoader } from "leaflet-loader-astro";
105
106
const documents = defineLiveCollection({
107
-
loader: leafletLiveLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }),
108
});
109
110
export const collections = { documents };
···
158
159
<Content />
160
```
161
162
## License
163
···
10
## Installation
11
12
```bash
13
+
npm install @nulfrost/leaflet-loader-astro
14
```
15
16
## Usage
17
18
+
<details>
19
+
<summary>Build-time loader: leafletStaticLoader **(recommended)**</summary>
20
21
```ts
22
// src/content.config.ts
23
import { defineCollection, z } from "astro:content";
24
+
import { leafletStaticLoader } from "@nulfrost/leaflet-loader-astro";
25
26
const documents = defineCollection({
27
+
loader: leafletStaticLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), // or repo: dane.is.extraordinarily.cool
28
});
29
30
export const collections = { documents };
···
82
83
<Content />
84
```
85
+
</details>
86
87
+
<details>
88
+
<summary>Live loader: leafletLiveLoader</summary>
89
90
```ts
91
// astro.config.mjs
···
104
```ts
105
// src/live.config.ts
106
import { defineLiveCollection, z } from "astro:content";
107
+
import { leafletLiveLoader } from "@nulfrost/leaflet-loader-astro";
108
109
const documents = defineLiveCollection({
110
+
loader: leafletLiveLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), // or repo: dane.is.extraordinarily.cool
111
});
112
113
export const collections = { documents };
···
161
162
<Content />
163
```
164
+
165
+
</details>
166
+
167
+
## Loader Options
168
+
169
+
### Static Loader
170
+
171
+
```ts
172
+
leafletStaticLoader()
173
+
```
174
+
175
+
`repo`: This can be either your DID (did:plc:qttsv4e7pu2jl3ilanfgc3zn) or your handle (dane.is.extraordinarily.cool)
176
+
177
+
`limit`: How many leaflet documents to return when calling `getCollection`. The default is 50 and the range is from 1 to 100.
178
+
179
+
`reverse`: Whether or not to return the leaflet documents in reverse order. By default this is false.
180
+
181
+
### Live Loader
182
+
183
+
```ts
184
+
leafletLiveLoader()
185
+
```
186
+
187
+
`repo`: This can be either your DID (did:plc:qttsv4e7pu2jl3ilanfgc3zn) or your handle (dane.is.extraordinarily.cool)
188
+
189
+
> [!NOTE]
190
+
> `getLiveCollection` supports a second argument where you can add additional filters, similar to the options you have access to for `leafletStaticLoader`
191
+
192
+
```ts
193
+
getLiveCollection()
194
+
```
195
+
196
+
`limit`: How many leaflet documents to return when calling `getCollection`. The default is 50 and the range is from 1 to 100.
197
+
198
+
`reverse`: Whether or not to return the leaflet documents in reverse order. By default this is false.
199
+
200
+
## Supported Leaflet Blocks
201
+
202
+
- [ ] Bluesky post
203
+
- [x] Iframe
204
+
- [x] Horizontal Rule
205
+
- [x] Unordered List
206
+
- [x] Math
207
+
- [x] Code
208
+
- [ ] Website
209
+
- [x] Image
210
+
- [x] Blockquote
211
+
- [x] Text
212
+
- [x] Header
213
+
- [x] List Item
214
215
## License
216
+1
-1
lex.config.js
+1
-1
lex.config.js
+22
lexicons/pub/leaflet/blocks/blockquote.json
+22
lexicons/pub/leaflet/blocks/blockquote.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "pub.leaflet.blocks.blockquote",
4
+
"defs": {
5
+
"main": {
6
+
"type": "object",
7
+
"required": ["plaintext"],
8
+
"properties": {
9
+
"plaintext": {
10
+
"type": "string"
11
+
},
12
+
"facets": {
13
+
"type": "array",
14
+
"items": {
15
+
"type": "ref",
16
+
"ref": "pub.leaflet.richtext.facet"
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
22
+
}
+11
lexicons/pub/leaflet/blocks/horizontalRule.json
+11
lexicons/pub/leaflet/blocks/horizontalRule.json
+21
lexicons/pub/leaflet/blocks/iframe.json
+21
lexicons/pub/leaflet/blocks/iframe.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "pub.leaflet.blocks.iframe",
4
+
"defs": {
5
+
"main": {
6
+
"type": "object",
7
+
"required": ["url"],
8
+
"properties": {
9
+
"url": {
10
+
"type": "string",
11
+
"format": "uri"
12
+
},
13
+
"height": {
14
+
"type": "integer",
15
+
"minimum": 16,
16
+
"maximum": 1600
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
+24
-19
lib/leaflet-live-loader.ts
+24
-19
lib/leaflet-live-loader.ts
···
1
-
import { Agent } from "@atproto/api";
2
-
import { isDid } from "@atproto/did";
3
import type { LiveLoader } from "astro/loaders";
4
import type {
5
CollectionFilter,
···
11
import {
12
getLeafletDocuments,
13
getSingleLeafletDocument,
14
leafletBlocksToHTML,
15
leafletDocumentRecordToView,
16
LiveLoaderError,
···
35
);
36
}
37
38
-
// not a valid did
39
-
if (!isDid(repo)) {
40
-
throw new LiveLoaderError("invalid did", "INVALID_DID");
41
}
42
43
return {
44
name: "leaflet-loader-astro",
45
loadCollection: async ({ filter }) => {
46
try {
47
-
const pds_url = await resolveMiniDoc(repo);
48
-
const agent = new Agent({ service: pds_url });
49
50
const { documents } = await getLeafletDocuments({
51
-
agent,
52
repo,
53
reverse: filter?.reverse,
54
cursor: filter?.cursor,
···
67
}),
68
rendered: {
69
html: leafletBlocksToHTML({
70
-
id,
71
-
uri: document.uri,
72
-
cid: document.cid,
73
-
value: document.value as unknown as LeafletDocumentRecord,
74
}),
75
},
76
};
···
95
};
96
}
97
try {
98
-
const pds_url = await resolveMiniDoc(repo);
99
-
const agent = new Agent({ service: pds_url });
100
const document = await getSingleLeafletDocument({
101
-
agent,
102
id: filter.id,
103
repo,
104
});
···
114
}),
115
rendered: {
116
html: leafletBlocksToHTML({
117
-
id: filter.id,
118
-
uri: document.uri,
119
-
cid,
120
-
value: document.value as unknown as LeafletDocumentRecord,
121
}),
122
},
123
};
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
+
import { isHandle } from "@atcute/lexicons/syntax";
3
import type { LiveLoader } from "astro/loaders";
4
import type {
5
CollectionFilter,
···
11
import {
12
getLeafletDocuments,
13
getSingleLeafletDocument,
14
+
isPlcDid,
15
leafletBlocksToHTML,
16
leafletDocumentRecordToView,
17
LiveLoaderError,
···
36
);
37
}
38
39
+
// not a valid handle, check if valid did
40
+
if (!isHandle(repo)) {
41
+
// not a valid handle or did, throw
42
+
if (!isPlcDid(repo)) {
43
+
throw new LiveLoaderError(
44
+
"invalid handle or did",
45
+
"INVALID_HANDLE_OR_DID",
46
+
);
47
+
}
48
}
49
50
return {
51
name: "leaflet-loader-astro",
52
loadCollection: async ({ filter }) => {
53
try {
54
+
const { pds, did } = await resolveMiniDoc(repo);
55
+
const handler = simpleFetchHandler({ service: pds });
56
+
const rpc = new Client({ handler });
57
58
const { documents } = await getLeafletDocuments({
59
+
rpc,
60
repo,
61
reverse: filter?.reverse,
62
cursor: filter?.cursor,
···
75
}),
76
rendered: {
77
html: leafletBlocksToHTML({
78
+
record: document.value as unknown as LeafletDocumentRecord,
79
+
did,
80
}),
81
},
82
};
···
101
};
102
}
103
try {
104
+
const { pds, did } = await resolveMiniDoc(repo);
105
+
const handler = simpleFetchHandler({ service: pds });
106
+
const rpc = new Client({ handler });
107
const document = await getSingleLeafletDocument({
108
+
rpc,
109
id: filter.id,
110
repo,
111
});
···
121
}),
122
rendered: {
123
html: leafletBlocksToHTML({
124
+
record: document.value as unknown as LeafletDocumentRecord,
125
+
did,
126
}),
127
},
128
};
+24
-17
lib/leaftlet-static-loader.ts
+24
-17
lib/leaftlet-static-loader.ts
···
1
-
import { Agent } from "@atproto/api";
2
-
import { isDid } from "@atproto/did";
3
import type { Loader, LoaderContext } from "astro/loaders";
4
import { LeafletDocumentSchema } from "schema.js";
5
import type {
···
7
StaticLeafletLoaderOptions,
8
} from "types.js";
9
import {
10
LiveLoaderError,
11
resolveMiniDoc,
12
-
getLeafletDocuments,
13
uriToRkey,
14
-
leafletDocumentRecordToView,
15
-
leafletBlocksToHTML,
16
} from "utils.js";
17
18
export function leafletStaticLoader(
19
options: StaticLeafletLoaderOptions,
20
): Loader {
21
-
const { repo, limit } = options;
22
23
if (!repo || typeof repo !== "string") {
24
throw new LiveLoaderError(
···
27
);
28
}
29
30
-
// not a valid did
31
-
if (!isDid(repo)) {
32
-
throw new LiveLoaderError("invalid did", "INVALID_DID");
33
}
34
35
return {
···
43
}: LoaderContext) => {
44
try {
45
logger.info("fetching latest leaflet documents");
46
-
const pds_url = await resolveMiniDoc(repo);
47
-
const agent = new Agent({ service: pds_url });
48
49
let cursor: string | undefined;
50
let count = 0;
···
52
fetching: do {
53
const { documents, cursor: documentsCursor } =
54
await getLeafletDocuments({
55
-
agent,
56
repo,
57
cursor,
58
-
limit: 100,
59
});
60
for (const document of documents) {
61
if (limit && count >= limit) {
···
83
digest,
84
rendered: {
85
html: leafletBlocksToHTML({
86
-
id,
87
-
uri: document.uri,
88
-
cid: document.cid,
89
-
value: document.value as unknown as LeafletDocumentRecord,
90
}),
91
},
92
});
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
+
import { isHandle } from "@atcute/lexicons/syntax";
3
import type { Loader, LoaderContext } from "astro/loaders";
4
import { LeafletDocumentSchema } from "schema.js";
5
import type {
···
7
StaticLeafletLoaderOptions,
8
} from "types.js";
9
import {
10
+
getLeafletDocuments,
11
+
isPlcDid,
12
+
leafletBlocksToHTML,
13
+
leafletDocumentRecordToView,
14
LiveLoaderError,
15
resolveMiniDoc,
16
uriToRkey,
17
} from "utils.js";
18
19
export function leafletStaticLoader(
20
options: StaticLeafletLoaderOptions,
21
): Loader {
22
+
const { repo, limit, reverse } = options;
23
24
if (!repo || typeof repo !== "string") {
25
throw new LiveLoaderError(
···
28
);
29
}
30
31
+
// not a valid handle, check if valid did
32
+
if (!isHandle(repo)) {
33
+
// not a valid handle or did, throw
34
+
if (!isPlcDid(repo)) {
35
+
throw new LiveLoaderError(
36
+
"invalid handle or did",
37
+
"INVALID_HANDLE_OR_DID",
38
+
);
39
+
}
40
}
41
42
return {
···
50
}: LoaderContext) => {
51
try {
52
logger.info("fetching latest leaflet documents");
53
+
const { pds, did } = await resolveMiniDoc(repo);
54
+
const handler = simpleFetchHandler({ service: pds });
55
+
const rpc = new Client({ handler });
56
57
let cursor: string | undefined;
58
let count = 0;
···
60
fetching: do {
61
const { documents, cursor: documentsCursor } =
62
await getLeafletDocuments({
63
+
rpc,
64
repo,
65
cursor,
66
+
reverse,
67
+
limit: 50,
68
});
69
for (const document of documents) {
70
if (limit && count >= limit) {
···
92
digest,
93
rendered: {
94
html: leafletBlocksToHTML({
95
+
record: document.value as unknown as LeafletDocumentRecord,
96
+
did,
97
}),
98
},
99
});
+3
lib/lexicons/index.ts
+3
lib/lexicons/index.ts
···
1
export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js";
2
export * as PubLeafletBlocksCode from "./types/pub/leaflet/blocks/code.js";
3
export * as PubLeafletBlocksHeader from "./types/pub/leaflet/blocks/header.js";
4
export * as PubLeafletBlocksImage from "./types/pub/leaflet/blocks/image.js";
5
export * as PubLeafletBlocksMath from "./types/pub/leaflet/blocks/math.js";
6
export * as PubLeafletBlocksText from "./types/pub/leaflet/blocks/text.js";
···
1
export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js";
2
+
export * as PubLeafletBlocksBlockquote from "./types/pub/leaflet/blocks/blockquote.js";
3
export * as PubLeafletBlocksCode from "./types/pub/leaflet/blocks/code.js";
4
export * as PubLeafletBlocksHeader from "./types/pub/leaflet/blocks/header.js";
5
+
export * as PubLeafletBlocksHorizontalRule from "./types/pub/leaflet/blocks/horizontalRule.js";
6
+
export * as PubLeafletBlocksIframe from "./types/pub/leaflet/blocks/iframe.js";
7
export * as PubLeafletBlocksImage from "./types/pub/leaflet/blocks/image.js";
8
export * as PubLeafletBlocksMath from "./types/pub/leaflet/blocks/math.js";
9
export * as PubLeafletBlocksText from "./types/pub/leaflet/blocks/text.js";
+23
lib/lexicons/types/pub/leaflet/blocks/blockquote.ts
+23
lib/lexicons/types/pub/leaflet/blocks/blockquote.ts
···
···
1
+
import type {} from "@atcute/lexicons";
2
+
import * as v from "@atcute/lexicons/validations";
3
+
import * as PubLeafletRichtextFacet from "../richtext/facet.js";
4
+
5
+
const _mainSchema = /*#__PURE__*/ v.object({
6
+
$type: /*#__PURE__*/ v.optional(
7
+
/*#__PURE__*/ v.literal("pub.leaflet.blocks.blockquote"),
8
+
),
9
+
get facets() {
10
+
return /*#__PURE__*/ v.optional(
11
+
/*#__PURE__*/ v.array(PubLeafletRichtextFacet.mainSchema),
12
+
);
13
+
},
14
+
plaintext: /*#__PURE__*/ v.string(),
15
+
});
16
+
17
+
type main$schematype = typeof _mainSchema;
18
+
19
+
export interface mainSchema extends main$schematype {}
20
+
21
+
export const mainSchema = _mainSchema as mainSchema;
22
+
23
+
export interface Main extends v.InferInput<typeof mainSchema> {}
+16
lib/lexicons/types/pub/leaflet/blocks/horizontalRule.ts
+16
lib/lexicons/types/pub/leaflet/blocks/horizontalRule.ts
···
···
1
+
import type {} from "@atcute/lexicons";
2
+
import * as v from "@atcute/lexicons/validations";
3
+
4
+
const _mainSchema = /*#__PURE__*/ v.object({
5
+
$type: /*#__PURE__*/ v.optional(
6
+
/*#__PURE__*/ v.literal("pub.leaflet.blocks.horizontalRule"),
7
+
),
8
+
});
9
+
10
+
type main$schematype = typeof _mainSchema;
11
+
12
+
export interface mainSchema extends main$schematype {}
13
+
14
+
export const mainSchema = _mainSchema as mainSchema;
15
+
16
+
export interface Main extends v.InferInput<typeof mainSchema> {}
+22
lib/lexicons/types/pub/leaflet/blocks/iframe.ts
+22
lib/lexicons/types/pub/leaflet/blocks/iframe.ts
···
···
1
+
import type {} from "@atcute/lexicons";
2
+
import * as v from "@atcute/lexicons/validations";
3
+
4
+
const _mainSchema = /*#__PURE__*/ v.object({
5
+
$type: /*#__PURE__*/ v.optional(
6
+
/*#__PURE__*/ v.literal("pub.leaflet.blocks.iframe"),
7
+
),
8
+
height: /*#__PURE__*/ v.optional(
9
+
/*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [
10
+
/*#__PURE__*/ v.integerRange(16, 1600),
11
+
]),
12
+
),
13
+
url: /*#__PURE__*/ v.genericUriString(),
14
+
});
15
+
16
+
type main$schematype = typeof _mainSchema;
17
+
18
+
export interface mainSchema extends main$schematype {}
19
+
20
+
export const mainSchema = _mainSchema as mainSchema;
21
+
22
+
export interface Main extends v.InferInput<typeof mainSchema> {}
+21
-8
lib/types.ts
+21
-8
lib/types.ts
···
1
-
import type { Agent } from "@atproto/api";
2
import type { PubLeafletRichtextFacet } from "./lexicons/index.js";
3
4
export interface LiveLeafletLoaderOptions {
5
/**
6
-
* @description Your repo is your DID (did:plc... or did:web...). You can find this information using: https://pdsls.dev
7
*/
8
repo: string;
9
}
10
11
export interface StaticLeafletLoaderOptions {
12
/**
13
-
* @description Your repo is your DID (did:plc... or did:web...). You can find this information using: https://pdsls.dev
14
*/
15
repo: string;
16
-
filter?: string;
17
/**
18
* @default 50
19
*/
20
limit?: number;
21
}
22
23
export interface LeafletDocumentRecord {
···
58
}
59
60
export interface GetLeafletDocumentsParams {
61
-
repo: string;
62
-
agent: Agent;
63
cursor?: string;
64
limit?: number;
65
reverse?: boolean;
66
}
67
68
export interface GetSingleLeafletDocumentParams {
69
-
repo: string;
70
-
agent: Agent;
71
id: string;
72
}
73
···
76
text: string;
77
facet?: Exclude<Facet["features"], { $type: string }>;
78
}
···
1
+
import type { Client } from "@atcute/client";
2
+
import type { ActorIdentifier } from "@atcute/lexicons";
3
+
import type { XRPCProcedures, XRPCQueries } from "@atcute/lexicons/ambient";
4
import type { PubLeafletRichtextFacet } from "./lexicons/index.js";
5
6
export interface LiveLeafletLoaderOptions {
7
/**
8
+
* @description Your repo is your DID (did:plc... or did:web...) or handle (username.bsky.social). You can find this information using: https://pdsls.dev
9
*/
10
repo: string;
11
}
12
13
export interface StaticLeafletLoaderOptions {
14
/**
15
+
* @description Your repo is your DID (did:plc... or did:web...) or handle (username.bsky.social). You can find this information using: https://pdsls.dev
16
*/
17
repo: string;
18
/**
19
+
* @description The number of records leaflet records to return for getCollection, the default being 50. The range can be from 1 to 100.
20
* @default 50
21
*/
22
limit?: number;
23
+
/**
24
+
* @description Whether or not the records should be returned in reverse order.
25
+
* @default undefined
26
+
*/
27
+
reverse?: boolean;
28
}
29
30
export interface LeafletDocumentRecord {
···
65
}
66
67
export interface GetLeafletDocumentsParams {
68
+
repo: ActorIdentifier;
69
+
rpc: Client<XRPCQueries, XRPCProcedures>;
70
cursor?: string;
71
limit?: number;
72
reverse?: boolean;
73
}
74
75
export interface GetSingleLeafletDocumentParams {
76
+
repo: ActorIdentifier;
77
+
rpc: Client<XRPCQueries, XRPCProcedures>;
78
id: string;
79
}
80
···
83
text: string;
84
facet?: Exclude<Facet["features"], { $type: string }>;
85
}
86
+
87
+
// yoinked from: https://github.com/mary-ext/atcute/blob/trunk/packages/lexicons/lexicons/lib/syntax/handle.ts
88
+
/**
89
+
* represents a decentralized identifier (DID).
90
+
*/
91
+
export type Did<Method extends string = string> = `did:${Method}:${string}`;
+215
-107
lib/utils.ts
+215
-107
lib/utils.ts
···
1
import { is } from "@atcute/lexicons";
2
import { AtUri, UnicodeString } from "@atproto/api";
3
import sanitizeHTML from "sanitize-html";
4
import {
5
PubLeafletBlocksHeader,
6
PubLeafletBlocksText,
7
PubLeafletPagesLinearDocument,
8
} from "./lexicons/index.js";
9
import type {
10
Facet,
11
GetLeafletDocumentsParams,
12
GetSingleLeafletDocumentParams,
···
29
export function uriToRkey(uri: string): string {
30
const u = AtUri.make(uri);
31
if (!u.rkey) {
32
-
throw new Error("Failed to get rkey from uri.");
33
}
34
return u.rkey;
35
}
···
47
}
48
const data = (await response.json()) as MiniDoc;
49
50
-
return data.pds;
51
} catch {
52
throw new Error(`failed to resolve handle: ${handleOrDid}`);
53
}
···
57
repo,
58
reverse,
59
cursor,
60
-
agent,
61
limit,
62
}: GetLeafletDocumentsParams) {
63
-
const response = await agent.com.atproto.repo.listRecords({
64
-
repo,
65
-
collection: "pub.leaflet.document",
66
-
cursor,
67
-
reverse,
68
-
limit,
69
});
70
71
-
if (response.success === false) {
72
throw new LiveLoaderError(
73
"error fetching leaflet documents",
74
"DOCUMENT_FETCH_ERROR",
···
76
}
77
78
return {
79
-
documents: response?.data?.records,
80
-
cursor: response?.data?.cursor,
81
};
82
}
83
84
export async function getSingleLeafletDocument({
85
-
agent,
86
repo,
87
id,
88
}: GetSingleLeafletDocumentParams) {
89
-
const response = await agent.com.atproto.repo.getRecord({
90
-
repo,
91
-
collection: "pub.leaflet.document",
92
-
rkey: id,
93
});
94
95
-
if (response.success === false) {
96
throw new LiveLoaderError(
97
"error fetching single document",
98
"DOCUMENT_FETCH_ERROR",
99
);
100
}
101
102
-
return response?.data;
103
}
104
105
export function leafletDocumentRecordToView({
···
122
};
123
}
124
125
-
export function leafletBlocksToHTML(record: {
126
-
id: string;
127
-
uri: string;
128
-
cid: string;
129
-
value: LeafletDocumentRecord;
130
}) {
131
let html = "";
132
-
const firstPage = record.value.pages[0];
133
let blocks: PubLeafletPagesLinearDocument.Block[] = [];
134
if (is(PubLeafletPagesLinearDocument.mainSchema, firstPage)) {
135
blocks = firstPage.blocks || [];
136
}
137
138
for (const block of blocks) {
139
-
if (is(PubLeafletBlocksText.mainSchema, block.block)) {
140
-
const rt = new RichText({
141
-
text: block.block.plaintext,
142
-
facets: block.block.facets || [],
143
-
});
144
-
const children = [];
145
-
for (const segment of rt.segments()) {
146
-
const link = segment.facet?.find(
147
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#link",
148
-
);
149
-
const isBold = segment.facet?.find(
150
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#bold",
151
-
);
152
-
const isCode = segment.facet?.find(
153
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#code",
154
-
);
155
-
const isStrikethrough = segment.facet?.find(
156
-
(segment) =>
157
-
segment.$type === "pub.leaflet.richtext.facet#strikethrough",
158
-
);
159
-
const isUnderline = segment.facet?.find(
160
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#underline",
161
-
);
162
-
const isItalic = segment.facet?.find(
163
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#italic",
164
-
);
165
-
if (isCode) {
166
-
children.push(` <code>
167
-
${segment.text}
168
-
</code>`);
169
-
} else if (link) {
170
-
children.push(
171
-
` <a
172
-
href="${link.uri}"
173
-
target="_blank"
174
-
>
175
-
${segment.text}
176
-
</a>`,
177
-
);
178
-
} else if (isBold) {
179
-
children.push(`<b>${segment.text}</b>`);
180
-
} else if (isStrikethrough) {
181
-
children.push(`<s>${segment.text}</s>`);
182
-
} else if (isUnderline) {
183
-
children.push(
184
-
`<span style="text-decoration:underline;">${segment.text}</span>`,
185
-
);
186
-
} else if (isItalic) {
187
-
children.push(`<i>${segment.text}</i>`);
188
-
} else {
189
-
children.push(
190
-
`
191
-
${segment.text}
192
-
`,
193
-
);
194
-
}
195
-
}
196
-
html += `<p>${children.join("\n")}</p>`;
197
-
}
198
-
199
-
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
200
-
if (block.block.level === 1) {
201
-
html += `<h2>${block.block.plaintext}</h2>`;
202
-
}
203
-
}
204
-
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
205
-
if (block.block.level === 2) {
206
-
html += `<h3>${block.block.plaintext}</h3>`;
207
-
}
208
-
}
209
-
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
210
-
if (block.block.level === 3) {
211
-
html += `<h4>${block.block.plaintext}</h4>`;
212
-
}
213
-
}
214
-
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
215
-
if (!block.block.level) {
216
-
html += `<h6>${block.block.plaintext}</h6>`;
217
-
}
218
-
}
219
}
220
221
-
return sanitizeHTML(html);
222
}
223
224
export class RichText {
225
unicodeText: UnicodeString;
226
facets?: Facet[];
227
-
228
constructor(props: { text: string; facets: Facet[] }) {
229
this.unicodeText = new UnicodeString(props.text);
230
this.facets = props.facets;
···
278
}
279
}
280
}
···
1
+
import type {} from "@atcute/atproto";
2
import { is } from "@atcute/lexicons";
3
import { AtUri, UnicodeString } from "@atproto/api";
4
+
import katex from "katex";
5
import sanitizeHTML from "sanitize-html";
6
import {
7
+
PubLeafletBlocksBlockquote,
8
+
PubLeafletBlocksCode,
9
PubLeafletBlocksHeader,
10
+
PubLeafletBlocksHorizontalRule,
11
+
PubLeafletBlocksIframe,
12
+
PubLeafletBlocksImage,
13
+
PubLeafletBlocksMath,
14
PubLeafletBlocksText,
15
+
PubLeafletBlocksUnorderedList,
16
PubLeafletPagesLinearDocument,
17
} from "./lexicons/index.js";
18
import type {
19
+
Did,
20
Facet,
21
GetLeafletDocumentsParams,
22
GetSingleLeafletDocumentParams,
···
39
export function uriToRkey(uri: string): string {
40
const u = AtUri.make(uri);
41
if (!u.rkey) {
42
+
throw new Error("failed to get rkey");
43
}
44
return u.rkey;
45
}
···
57
}
58
const data = (await response.json()) as MiniDoc;
59
60
+
return {
61
+
pds: data.pds,
62
+
did: data.did,
63
+
};
64
} catch {
65
throw new Error(`failed to resolve handle: ${handleOrDid}`);
66
}
···
70
repo,
71
reverse,
72
cursor,
73
+
rpc,
74
limit,
75
}: GetLeafletDocumentsParams) {
76
+
const { ok, data } = await rpc.get("com.atproto.repo.listRecords", {
77
+
params: {
78
+
collection: "pub.leaflet.document",
79
+
cursor,
80
+
reverse,
81
+
limit,
82
+
repo,
83
+
},
84
});
85
86
+
if (!ok) {
87
throw new LiveLoaderError(
88
"error fetching leaflet documents",
89
"DOCUMENT_FETCH_ERROR",
···
91
}
92
93
return {
94
+
documents: data?.records,
95
+
cursor: data?.cursor,
96
};
97
}
98
99
export async function getSingleLeafletDocument({
100
+
rpc,
101
repo,
102
id,
103
}: GetSingleLeafletDocumentParams) {
104
+
const { ok, data } = await rpc.get("com.atproto.repo.getRecord", {
105
+
params: {
106
+
collection: "pub.leaflet.document",
107
+
repo,
108
+
rkey: id,
109
+
},
110
});
111
112
+
if (!ok) {
113
throw new LiveLoaderError(
114
"error fetching single document",
115
"DOCUMENT_FETCH_ERROR",
116
);
117
}
118
119
+
return data;
120
}
121
122
export function leafletDocumentRecordToView({
···
139
};
140
}
141
142
+
export function leafletBlocksToHTML({
143
+
record,
144
+
did,
145
+
}: {
146
+
record: LeafletDocumentRecord;
147
+
did: string;
148
}) {
149
let html = "";
150
+
const firstPage = record.pages[0];
151
let blocks: PubLeafletPagesLinearDocument.Block[] = [];
152
+
153
if (is(PubLeafletPagesLinearDocument.mainSchema, firstPage)) {
154
blocks = firstPage.blocks || [];
155
}
156
157
for (const block of blocks) {
158
+
html += parseBlocks({ block, did });
159
}
160
161
+
return sanitizeHTML(html, {
162
+
allowedAttributes: {
163
+
"*": ["class", "style"],
164
+
img: ["src", "height", "width", "alt"],
165
+
a: ["href", "target", "rel"],
166
+
iframe: ["height", "allow", "loading", "src"],
167
+
},
168
+
allowedTags: [
169
+
"img",
170
+
"pre",
171
+
"code",
172
+
"p",
173
+
"a",
174
+
"b",
175
+
"s",
176
+
"ul",
177
+
"li",
178
+
"i",
179
+
"h1",
180
+
"h2",
181
+
"h3",
182
+
"h4",
183
+
"h5",
184
+
"h6",
185
+
"hr",
186
+
"div",
187
+
"span",
188
+
"blockquote",
189
+
"iframe",
190
+
],
191
+
selfClosing: ["img"],
192
+
});
193
}
194
195
export class RichText {
196
unicodeText: UnicodeString;
197
facets?: Facet[];
198
constructor(props: { text: string; facets: Facet[] }) {
199
this.unicodeText = new UnicodeString(props.text);
200
this.facets = props.facets;
···
248
}
249
}
250
}
251
+
252
+
export function parseTextBlock(block: PubLeafletBlocksText.Main) {
253
+
let html = "";
254
+
const rt = new RichText({
255
+
text: block.plaintext,
256
+
facets: block.facets || [],
257
+
});
258
+
const children = [];
259
+
for (const segment of rt.segments()) {
260
+
const link = segment.facet?.find(
261
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#link",
262
+
);
263
+
const isBold = segment.facet?.find(
264
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#bold",
265
+
);
266
+
const isCode = segment.facet?.find(
267
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#code",
268
+
);
269
+
const isStrikethrough = segment.facet?.find(
270
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#strikethrough",
271
+
);
272
+
const isUnderline = segment.facet?.find(
273
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#underline",
274
+
);
275
+
const isItalic = segment.facet?.find(
276
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#italic",
277
+
);
278
+
if (isCode) {
279
+
children.push(`<pre><code>${segment.text}</code></pre>`);
280
+
} else if (link) {
281
+
children.push(
282
+
`<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`,
283
+
);
284
+
} else if (isBold) {
285
+
children.push(`<b>${segment.text}</b>`);
286
+
} else if (isStrikethrough) {
287
+
children.push(`<s>${segment.text}</s>`);
288
+
} else if (isUnderline) {
289
+
children.push(
290
+
`<span style="text-decoration:underline;">${segment.text}</span>`,
291
+
);
292
+
} else if (isItalic) {
293
+
children.push(`<i>${segment.text}</i>`);
294
+
} else {
295
+
children.push(`${segment.text}`);
296
+
}
297
+
}
298
+
html += `<p>${children.join("")}</p>`;
299
+
300
+
return html.trim();
301
+
}
302
+
303
+
export function parseBlocks({
304
+
block,
305
+
did,
306
+
}: {
307
+
block: PubLeafletPagesLinearDocument.Block;
308
+
did: string;
309
+
}): string {
310
+
let html = "";
311
+
312
+
if (is(PubLeafletBlocksText.mainSchema, block.block)) {
313
+
html += parseTextBlock(block.block);
314
+
}
315
+
316
+
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
317
+
if (block.block.level === 1) {
318
+
html += `<h2>${block.block.plaintext}</h2>`;
319
+
}
320
+
}
321
+
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
322
+
if (block.block.level === 2) {
323
+
html += `<h3>${block.block.plaintext}</h3>`;
324
+
}
325
+
}
326
+
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
327
+
if (block.block.level === 3) {
328
+
html += `<h4>${block.block.plaintext}</h4>`;
329
+
}
330
+
}
331
+
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
332
+
if (!block.block.level) {
333
+
html += `<h6>${block.block.plaintext}</h6>`;
334
+
}
335
+
}
336
+
337
+
if (is(PubLeafletBlocksHorizontalRule.mainSchema, block.block)) {
338
+
html += `<hr />`;
339
+
}
340
+
if (is(PubLeafletBlocksUnorderedList.mainSchema, block.block)) {
341
+
html += `<ul>${block.block.children.map((child) => renderListItem({ item: child, did })).join("")}</ul>`;
342
+
}
343
+
344
+
if (is(PubLeafletBlocksMath.mainSchema, block.block)) {
345
+
html += `<div>${katex.renderToString(block.block.tex, { displayMode: true, output: "html", throwOnError: false })}</div>`;
346
+
}
347
+
348
+
if (is(PubLeafletBlocksCode.mainSchema, block.block)) {
349
+
html += `<pre><code data-language=${block.block.language}>${block.block.plaintext}</code></pre>`;
350
+
}
351
+
352
+
if (is(PubLeafletBlocksImage.mainSchema, block.block)) {
353
+
// @ts-ignore
354
+
html += `<div><img src="https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${block.block.image.ref.$link}@jpeg" height="${block.block.aspectRatio.height}" width="${block.block.aspectRatio.width}" alt="${block.block.alt}" /></div>`;
355
+
}
356
+
357
+
if (is(PubLeafletBlocksBlockquote.mainSchema, block.block)) {
358
+
html += `<blockquote>${parseTextBlock(block.block)}</blockquote>`;
359
+
}
360
+
361
+
if (is(PubLeafletBlocksIframe.mainSchema, block.block)) {
362
+
// @ts-ignore
363
+
html += `<iframe height="${block.block.height}" allow="fullscreen" loading="lazy" src="${block.block.url}"></iframe>`;
364
+
}
365
+
366
+
return html.trim();
367
+
}
368
+
369
+
export function renderListItem({
370
+
item,
371
+
did,
372
+
}: {
373
+
item: PubLeafletBlocksUnorderedList.ListItem;
374
+
did: string;
375
+
}): string {
376
+
const children: string | null = item.children?.length
377
+
? `<ul>${item.children.map((child) => renderListItem({ item: child, did }))}</ul>`
378
+
: "";
379
+
380
+
return `<li>${parseBlocks({ block: { block: item.content }, did })}${children}</li>`;
381
+
}
382
+
383
+
// yoinked from: https://github.com/mary-ext/atcute/blob/trunk/packages/lexicons/lexicons/lib/syntax/handle.ts
384
+
const PLC_DID_RE = /^did:plc:([a-z2-7]{24})$/;
385
+
386
+
export const isPlcDid = (input: string): input is Did<"plc"> => {
387
+
return input.length === 32 && PLC_DID_RE.test(input);
388
+
};
+13
-6
package.json
+13
-6
package.json
···
1
{
2
-
"name": "leaflet-loader-astro",
3
-
"version": "0.0.1",
4
"description": "A leaflet.pub astro collection loader",
5
"keywords": [
6
"astro",
···
12
"bugs": {
13
"url": "https://github.com/nulfrost/leaflet-loader-astro/issues"
14
},
15
-
"author": "Dane Miller",
16
"repository": {
17
"type": "git",
18
"url": "git+https://github.com/nulfrost/leaflet-loader-astro.git"
···
23
"lex": "lex-cli generate -c ./lex.config.js",
24
"test": "vitest --run",
25
"typecheck": "tsc",
26
"build": "rm -rf dist && tsup --format esm --dts",
27
-
"pack": "pnpm build && pnpm pack"
28
},
29
"license": "MIT",
30
"files": [
31
"dist"
32
],
33
"type": "module",
34
"main": "dist/index.js",
35
"module": "dist/index.js",
···
41
}
42
},
43
"devDependencies": {
44
"@atcute/lex-cli": "^2.1.1",
45
"@biomejs/biome": "2.1.3",
46
"@changesets/cli": "^2.29.5",
47
"@types/sanitize-html": "^2.16.0",
48
"astro": "^5.12.8",
49
"tsup": "^8.5.0",
···
51
"vitest": "^3.2.4"
52
},
53
"dependencies": {
54
"@atcute/lexicons": "^1.1.0",
55
-
"@atproto/api": "^0.16.1",
56
-
"@atproto/did": "^0.1.5",
57
"sanitize-html": "^2.17.0"
58
}
59
}
···
1
{
2
+
"name": "@nulfrost/leaflet-loader-astro",
3
+
"version": "1.3.0",
4
"description": "A leaflet.pub astro collection loader",
5
"keywords": [
6
"astro",
···
12
"bugs": {
13
"url": "https://github.com/nulfrost/leaflet-loader-astro/issues"
14
},
15
+
"author": "Dane Miller <me@dane.computer>",
16
"repository": {
17
"type": "git",
18
"url": "git+https://github.com/nulfrost/leaflet-loader-astro.git"
···
23
"lex": "lex-cli generate -c ./lex.config.js",
24
"test": "vitest --run",
25
"typecheck": "tsc",
26
+
"release": "pnpm run build && changeset publish",
27
"build": "rm -rf dist && tsup --format esm --dts",
28
+
"pack": "rm -rf *.tgz && pnpm build && pnpm pack"
29
},
30
"license": "MIT",
31
"files": [
32
"dist"
33
],
34
+
"publishConfig": {
35
+
"access": "public"
36
+
},
37
"type": "module",
38
"main": "dist/index.js",
39
"module": "dist/index.js",
···
45
}
46
},
47
"devDependencies": {
48
+
"@atcute/atproto": "^3.1.1",
49
"@atcute/lex-cli": "^2.1.1",
50
"@biomejs/biome": "2.1.3",
51
"@changesets/cli": "^2.29.5",
52
+
"@types/bun": "^1.2.19",
53
"@types/sanitize-html": "^2.16.0",
54
"astro": "^5.12.8",
55
"tsup": "^8.5.0",
···
57
"vitest": "^3.2.4"
58
},
59
"dependencies": {
60
+
"@atcute/client": "^4.0.3",
61
"@atcute/lexicons": "^1.1.0",
62
+
"@atproto/api": "^0.16.2",
63
+
"katex": "^0.16.22",
64
"sanitize-html": "^2.17.0"
65
}
66
}
+118
-48
pnpm-lock.yaml
+118
-48
pnpm-lock.yaml
···
8
9
.:
10
dependencies:
11
'@atcute/lexicons':
12
specifier: ^1.1.0
13
version: 1.1.0
14
'@atproto/api':
15
-
specifier: ^0.16.1
16
-
version: 0.16.1
17
-
'@atproto/did':
18
-
specifier: ^0.1.5
19
-
version: 0.1.5
20
sanitize-html:
21
specifier: ^2.17.0
22
version: 2.17.0
23
devDependencies:
24
'@atcute/lex-cli':
25
specifier: ^2.1.1
26
version: 2.1.1
···
30
'@changesets/cli':
31
specifier: ^2.29.5
32
version: 2.29.5
33
'@types/sanitize-html':
34
specifier: ^2.16.0
35
version: 2.16.0
36
astro:
37
specifier: ^5.12.8
38
-
version: 5.12.8(@types/node@24.2.0)(rollup@4.46.2)(typescript@5.9.2)
39
tsup:
40
specifier: ^8.5.0
41
version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)
···
44
version: 5.9.2
45
vitest:
46
specifier: ^3.2.4
47
-
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.0)
48
49
packages:
50
···
65
resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
66
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
67
68
'@atcute/lex-cli@2.1.1':
69
resolution: {integrity: sha512-QaR0sOP8Z24opGHKsSfleDbP/ahUb6HECkVaOqSwG7ORZzbLK1w0265o1BRjCVr2dT6FxlsMUa2Ge85JMA9bxg==}
70
hasBin: true
···
75
'@atcute/lexicons@1.1.0':
76
resolution: {integrity: sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==}
77
78
-
'@atproto/api@0.16.1':
79
-
resolution: {integrity: sha512-w48BlTmzKym7nZETWxgiuUX/wwRXU3xsLLKORWo/xtGnwlvpchUFnHKI3k4ttYJ2/JQE59+/4C16BaLzDyiU2w==}
80
81
'@atproto/common-web@0.4.2':
82
resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==}
83
-
84
-
'@atproto/did@0.1.5':
85
-
resolution: {integrity: sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==}
86
87
'@atproto/lexicon@0.4.12':
88
resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==}
···
672
673
'@swc/helpers@0.5.17':
674
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
675
676
'@types/chai@5.2.2':
677
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
···
703
'@types/node@12.20.55':
704
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
705
706
-
'@types/node@24.2.0':
707
-
resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==}
708
709
'@types/sanitize-html@2.16.0':
710
resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==}
···
800
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
801
engines: {node: '>=12'}
802
803
-
astro@5.12.8:
804
-
resolution: {integrity: sha512-KkJ7FR+c2SyZYlpakm48XBiuQcRsrVtdjG5LN5an0givI/tLik+ePJ4/g3qrAVhYMjJOxBA2YgFQxANPiWB+Mw==}
805
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
806
hasBin: true
807
···
845
brotli@1.3.3:
846
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
847
848
bundle-require@5.1.0:
849
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
850
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
···
931
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
932
engines: {node: '>= 6'}
933
934
common-ancestor-path@1.0.1:
935
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
936
···
967
engines: {node: '>=4'}
968
hasBin: true
969
970
debug@4.4.1:
971
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
972
engines: {node: '>=6.0'}
···
1336
1337
jsonfile@4.0.0:
1338
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
1339
1340
kleur@3.0.3:
1341
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
···
1876
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
1877
engines: {node: '>=8'}
1878
1879
-
smol-toml@1.4.1:
1880
-
resolution: {integrity: sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg==}
1881
engines: {node: '>= 18'}
1882
1883
source-map-js@1.2.1:
···
2218
yaml:
2219
optional: true
2220
2221
-
vite@7.0.6:
2222
-
resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==}
2223
engines: {node: ^20.19.0 || >=22.12.0}
2224
hasBin: true
2225
peerDependencies:
···
2398
remark-rehype: 11.1.2
2399
remark-smartypants: 3.0.2
2400
shiki: 3.9.2
2401
-
smol-toml: 1.4.1
2402
unified: 11.0.5
2403
unist-util-remove-position: 5.0.0
2404
unist-util-visit: 5.0.0
···
2423
transitivePeerDependencies:
2424
- supports-color
2425
2426
'@atcute/lex-cli@2.1.1':
2427
dependencies:
2428
'@atcute/lexicon-doc': 1.0.3
···
2439
dependencies:
2440
esm-env: 1.2.2
2441
2442
-
'@atproto/api@0.16.1':
2443
dependencies:
2444
'@atproto/common-web': 0.4.2
2445
'@atproto/lexicon': 0.4.12
···
2455
graphemer: 1.4.0
2456
multiformats: 9.9.0
2457
uint8arrays: 3.0.0
2458
-
zod: 3.25.76
2459
-
2460
-
'@atproto/did@0.1.5':
2461
-
dependencies:
2462
zod: 3.25.76
2463
2464
'@atproto/lexicon@0.4.12':
···
3001
dependencies:
3002
tslib: 2.8.1
3003
3004
'@types/chai@5.2.2':
3005
dependencies:
3006
'@types/deep-eql': 4.0.2
···
3015
3016
'@types/fontkit@2.0.8':
3017
dependencies:
3018
-
'@types/node': 24.2.0
3019
3020
'@types/hast@3.0.4':
3021
dependencies:
···
3033
3034
'@types/node@12.20.55': {}
3035
3036
-
'@types/node@24.2.0':
3037
dependencies:
3038
undici-types: 7.10.0
3039
3040
'@types/sanitize-html@2.16.0':
3041
dependencies:
3042
htmlparser2: 8.0.2
···
3053
chai: 5.2.1
3054
tinyrainbow: 2.0.0
3055
3056
-
'@vitest/mocker@3.2.4(vite@7.0.6(@types/node@24.2.0))':
3057
dependencies:
3058
'@vitest/spy': 3.2.4
3059
estree-walker: 3.0.3
3060
magic-string: 0.30.17
3061
optionalDependencies:
3062
-
vite: 7.0.6(@types/node@24.2.0)
3063
3064
'@vitest/pretty-format@3.2.4':
3065
dependencies:
···
3126
3127
assertion-error@2.0.1: {}
3128
3129
-
astro@5.12.8(@types/node@24.2.0)(rollup@4.46.2)(typescript@5.9.2):
3130
dependencies:
3131
'@astrojs/compiler': 2.12.2
3132
'@astrojs/internal-helpers': 0.7.1
···
3173
rehype: 13.0.2
3174
semver: 7.7.2
3175
shiki: 3.9.2
3176
-
smol-toml: 1.4.1
3177
tinyexec: 0.3.2
3178
tinyglobby: 0.2.14
3179
tsconfck: 3.1.6(typescript@5.9.2)
···
3182
unist-util-visit: 5.0.0
3183
unstorage: 1.16.1
3184
vfile: 6.0.3
3185
-
vite: 6.3.5(@types/node@24.2.0)
3186
-
vitefu: 1.1.1(vite@6.3.5(@types/node@24.2.0))
3187
xxhash-wasm: 1.1.0
3188
yargs-parser: 21.1.1
3189
yocto-spinner: 0.2.3
···
3268
dependencies:
3269
base64-js: 1.5.1
3270
3271
bundle-require@5.1.0(esbuild@0.25.8):
3272
dependencies:
3273
esbuild: 0.25.8
···
3335
3336
commander@4.1.1: {}
3337
3338
common-ancestor-path@1.0.1: {}
3339
3340
confbox@0.1.8: {}
···
3367
source-map-js: 1.2.1
3368
3369
cssesc@3.0.0: {}
3370
3371
debug@4.4.1:
3372
dependencies:
···
3793
optionalDependencies:
3794
graceful-fs: 4.2.11
3795
3796
kleur@3.0.3: {}
3797
3798
kleur@4.1.5: {}
···
4550
4551
slash@3.0.0: {}
4552
4553
-
smol-toml@1.4.1: {}
4554
4555
source-map-js@1.2.1: {}
4556
···
4815
'@types/unist': 3.0.3
4816
vfile-message: 4.0.3
4817
4818
-
vite-node@3.2.4(@types/node@24.2.0):
4819
dependencies:
4820
cac: 6.7.14
4821
debug: 4.4.1
4822
es-module-lexer: 1.7.0
4823
pathe: 2.0.3
4824
-
vite: 7.0.6(@types/node@24.2.0)
4825
transitivePeerDependencies:
4826
- '@types/node'
4827
- jiti
···
4836
- tsx
4837
- yaml
4838
4839
-
vite@6.3.5(@types/node@24.2.0):
4840
dependencies:
4841
esbuild: 0.25.8
4842
fdir: 6.4.6(picomatch@4.0.3)
···
4845
rollup: 4.46.2
4846
tinyglobby: 0.2.14
4847
optionalDependencies:
4848
-
'@types/node': 24.2.0
4849
fsevents: 2.3.3
4850
4851
-
vite@7.0.6(@types/node@24.2.0):
4852
dependencies:
4853
esbuild: 0.25.8
4854
fdir: 6.4.6(picomatch@4.0.3)
···
4857
rollup: 4.46.2
4858
tinyglobby: 0.2.14
4859
optionalDependencies:
4860
-
'@types/node': 24.2.0
4861
fsevents: 2.3.3
4862
4863
-
vitefu@1.1.1(vite@6.3.5(@types/node@24.2.0)):
4864
optionalDependencies:
4865
-
vite: 6.3.5(@types/node@24.2.0)
4866
4867
-
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.0):
4868
dependencies:
4869
'@types/chai': 5.2.2
4870
'@vitest/expect': 3.2.4
4871
-
'@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@24.2.0))
4872
'@vitest/pretty-format': 3.2.4
4873
'@vitest/runner': 3.2.4
4874
'@vitest/snapshot': 3.2.4
···
4886
tinyglobby: 0.2.14
4887
tinypool: 1.1.1
4888
tinyrainbow: 2.0.0
4889
-
vite: 7.0.6(@types/node@24.2.0)
4890
-
vite-node: 3.2.4(@types/node@24.2.0)
4891
why-is-node-running: 2.3.0
4892
optionalDependencies:
4893
'@types/debug': 4.1.12
4894
-
'@types/node': 24.2.0
4895
transitivePeerDependencies:
4896
- jiti
4897
- less
···
8
9
.:
10
dependencies:
11
+
'@atcute/client':
12
+
specifier: ^4.0.3
13
+
version: 4.0.3
14
'@atcute/lexicons':
15
specifier: ^1.1.0
16
version: 1.1.0
17
'@atproto/api':
18
+
specifier: ^0.16.2
19
+
version: 0.16.2
20
+
katex:
21
+
specifier: ^0.16.22
22
+
version: 0.16.22
23
sanitize-html:
24
specifier: ^2.17.0
25
version: 2.17.0
26
devDependencies:
27
+
'@atcute/atproto':
28
+
specifier: ^3.1.1
29
+
version: 3.1.1
30
'@atcute/lex-cli':
31
specifier: ^2.1.1
32
version: 2.1.1
···
36
'@changesets/cli':
37
specifier: ^2.29.5
38
version: 2.29.5
39
+
'@types/bun':
40
+
specifier: ^1.2.19
41
+
version: 1.2.19(@types/react@19.1.9)
42
'@types/sanitize-html':
43
specifier: ^2.16.0
44
version: 2.16.0
45
astro:
46
specifier: ^5.12.8
47
+
version: 5.12.9(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.9.2)
48
tsup:
49
specifier: ^8.5.0
50
version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)
···
53
version: 5.9.2
54
vitest:
55
specifier: ^3.2.4
56
+
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)
57
58
packages:
59
···
74
resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
75
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
76
77
+
'@atcute/atproto@3.1.1':
78
+
resolution: {integrity: sha512-D+RLTIPF0xLu7BPZY8KSewAPemJFh+3n3zeQ3ROsLxbTtCHbrTDMAmAFexaVRAPGcPYrwXaBUlv7yZjScJolMg==}
79
+
80
+
'@atcute/client@4.0.3':
81
+
resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==}
82
+
83
+
'@atcute/identity@1.0.3':
84
+
resolution: {integrity: sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==}
85
+
86
'@atcute/lex-cli@2.1.1':
87
resolution: {integrity: sha512-QaR0sOP8Z24opGHKsSfleDbP/ahUb6HECkVaOqSwG7ORZzbLK1w0265o1BRjCVr2dT6FxlsMUa2Ge85JMA9bxg==}
88
hasBin: true
···
93
'@atcute/lexicons@1.1.0':
94
resolution: {integrity: sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==}
95
96
+
'@atproto/api@0.16.2':
97
+
resolution: {integrity: sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==}
98
99
'@atproto/common-web@0.4.2':
100
resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==}
101
102
'@atproto/lexicon@0.4.12':
103
resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==}
···
687
688
'@swc/helpers@0.5.17':
689
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
690
+
691
+
'@types/bun@1.2.19':
692
+
resolution: {integrity: sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==}
693
694
'@types/chai@5.2.2':
695
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
···
721
'@types/node@12.20.55':
722
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
723
724
+
'@types/node@24.2.1':
725
+
resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
726
+
727
+
'@types/react@19.1.9':
728
+
resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
729
730
'@types/sanitize-html@2.16.0':
731
resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==}
···
821
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
822
engines: {node: '>=12'}
823
824
+
astro@5.12.9:
825
+
resolution: {integrity: sha512-cZ7kZ61jyE5nwSrFKSRyf5Gds+uJELqQxJFqMkcgiWQvhWZJUSShn8Uz3yc9WLyLw5Kim5P5un9SkJSGogfEZQ==}
826
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
827
hasBin: true
828
···
866
brotli@1.3.3:
867
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
868
869
+
bun-types@1.2.19:
870
+
resolution: {integrity: sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ==}
871
+
peerDependencies:
872
+
'@types/react': ^19
873
+
874
bundle-require@5.1.0:
875
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
876
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
···
957
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
958
engines: {node: '>= 6'}
959
960
+
commander@8.3.0:
961
+
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
962
+
engines: {node: '>= 12'}
963
+
964
common-ancestor-path@1.0.1:
965
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
966
···
997
engines: {node: '>=4'}
998
hasBin: true
999
1000
+
csstype@3.1.3:
1001
+
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
1002
+
1003
debug@4.4.1:
1004
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
1005
engines: {node: '>=6.0'}
···
1369
1370
jsonfile@4.0.0:
1371
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
1372
+
1373
+
katex@0.16.22:
1374
+
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
1375
+
hasBin: true
1376
1377
kleur@3.0.3:
1378
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
···
1913
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
1914
engines: {node: '>=8'}
1915
1916
+
smol-toml@1.4.2:
1917
+
resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==}
1918
engines: {node: '>= 18'}
1919
1920
source-map-js@1.2.1:
···
2255
yaml:
2256
optional: true
2257
2258
+
vite@7.1.1:
2259
+
resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==}
2260
engines: {node: ^20.19.0 || >=22.12.0}
2261
hasBin: true
2262
peerDependencies:
···
2435
remark-rehype: 11.1.2
2436
remark-smartypants: 3.0.2
2437
shiki: 3.9.2
2438
+
smol-toml: 1.4.2
2439
unified: 11.0.5
2440
unist-util-remove-position: 5.0.0
2441
unist-util-visit: 5.0.0
···
2460
transitivePeerDependencies:
2461
- supports-color
2462
2463
+
'@atcute/atproto@3.1.1':
2464
+
dependencies:
2465
+
'@atcute/lexicons': 1.1.0
2466
+
2467
+
'@atcute/client@4.0.3':
2468
+
dependencies:
2469
+
'@atcute/identity': 1.0.3
2470
+
'@atcute/lexicons': 1.1.0
2471
+
2472
+
'@atcute/identity@1.0.3':
2473
+
dependencies:
2474
+
'@atcute/lexicons': 1.1.0
2475
+
'@badrap/valita': 0.4.6
2476
+
2477
'@atcute/lex-cli@2.1.1':
2478
dependencies:
2479
'@atcute/lexicon-doc': 1.0.3
···
2490
dependencies:
2491
esm-env: 1.2.2
2492
2493
+
'@atproto/api@0.16.2':
2494
dependencies:
2495
'@atproto/common-web': 0.4.2
2496
'@atproto/lexicon': 0.4.12
···
2506
graphemer: 1.4.0
2507
multiformats: 9.9.0
2508
uint8arrays: 3.0.0
2509
zod: 3.25.76
2510
2511
'@atproto/lexicon@0.4.12':
···
3048
dependencies:
3049
tslib: 2.8.1
3050
3051
+
'@types/bun@1.2.19(@types/react@19.1.9)':
3052
+
dependencies:
3053
+
bun-types: 1.2.19(@types/react@19.1.9)
3054
+
transitivePeerDependencies:
3055
+
- '@types/react'
3056
+
3057
'@types/chai@5.2.2':
3058
dependencies:
3059
'@types/deep-eql': 4.0.2
···
3068
3069
'@types/fontkit@2.0.8':
3070
dependencies:
3071
+
'@types/node': 24.2.1
3072
3073
'@types/hast@3.0.4':
3074
dependencies:
···
3086
3087
'@types/node@12.20.55': {}
3088
3089
+
'@types/node@24.2.1':
3090
dependencies:
3091
undici-types: 7.10.0
3092
3093
+
'@types/react@19.1.9':
3094
+
dependencies:
3095
+
csstype: 3.1.3
3096
+
3097
'@types/sanitize-html@2.16.0':
3098
dependencies:
3099
htmlparser2: 8.0.2
···
3110
chai: 5.2.1
3111
tinyrainbow: 2.0.0
3112
3113
+
'@vitest/mocker@3.2.4(vite@7.1.1(@types/node@24.2.1))':
3114
dependencies:
3115
'@vitest/spy': 3.2.4
3116
estree-walker: 3.0.3
3117
magic-string: 0.30.17
3118
optionalDependencies:
3119
+
vite: 7.1.1(@types/node@24.2.1)
3120
3121
'@vitest/pretty-format@3.2.4':
3122
dependencies:
···
3183
3184
assertion-error@2.0.1: {}
3185
3186
+
astro@5.12.9(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.9.2):
3187
dependencies:
3188
'@astrojs/compiler': 2.12.2
3189
'@astrojs/internal-helpers': 0.7.1
···
3230
rehype: 13.0.2
3231
semver: 7.7.2
3232
shiki: 3.9.2
3233
+
smol-toml: 1.4.2
3234
tinyexec: 0.3.2
3235
tinyglobby: 0.2.14
3236
tsconfck: 3.1.6(typescript@5.9.2)
···
3239
unist-util-visit: 5.0.0
3240
unstorage: 1.16.1
3241
vfile: 6.0.3
3242
+
vite: 6.3.5(@types/node@24.2.1)
3243
+
vitefu: 1.1.1(vite@6.3.5(@types/node@24.2.1))
3244
xxhash-wasm: 1.1.0
3245
yargs-parser: 21.1.1
3246
yocto-spinner: 0.2.3
···
3325
dependencies:
3326
base64-js: 1.5.1
3327
3328
+
bun-types@1.2.19(@types/react@19.1.9):
3329
+
dependencies:
3330
+
'@types/node': 24.2.1
3331
+
'@types/react': 19.1.9
3332
+
3333
bundle-require@5.1.0(esbuild@0.25.8):
3334
dependencies:
3335
esbuild: 0.25.8
···
3397
3398
commander@4.1.1: {}
3399
3400
+
commander@8.3.0: {}
3401
+
3402
common-ancestor-path@1.0.1: {}
3403
3404
confbox@0.1.8: {}
···
3431
source-map-js: 1.2.1
3432
3433
cssesc@3.0.0: {}
3434
+
3435
+
csstype@3.1.3: {}
3436
3437
debug@4.4.1:
3438
dependencies:
···
3859
optionalDependencies:
3860
graceful-fs: 4.2.11
3861
3862
+
katex@0.16.22:
3863
+
dependencies:
3864
+
commander: 8.3.0
3865
+
3866
kleur@3.0.3: {}
3867
3868
kleur@4.1.5: {}
···
4620
4621
slash@3.0.0: {}
4622
4623
+
smol-toml@1.4.2: {}
4624
4625
source-map-js@1.2.1: {}
4626
···
4885
'@types/unist': 3.0.3
4886
vfile-message: 4.0.3
4887
4888
+
vite-node@3.2.4(@types/node@24.2.1):
4889
dependencies:
4890
cac: 6.7.14
4891
debug: 4.4.1
4892
es-module-lexer: 1.7.0
4893
pathe: 2.0.3
4894
+
vite: 7.1.1(@types/node@24.2.1)
4895
transitivePeerDependencies:
4896
- '@types/node'
4897
- jiti
···
4906
- tsx
4907
- yaml
4908
4909
+
vite@6.3.5(@types/node@24.2.1):
4910
dependencies:
4911
esbuild: 0.25.8
4912
fdir: 6.4.6(picomatch@4.0.3)
···
4915
rollup: 4.46.2
4916
tinyglobby: 0.2.14
4917
optionalDependencies:
4918
+
'@types/node': 24.2.1
4919
fsevents: 2.3.3
4920
4921
+
vite@7.1.1(@types/node@24.2.1):
4922
dependencies:
4923
esbuild: 0.25.8
4924
fdir: 6.4.6(picomatch@4.0.3)
···
4927
rollup: 4.46.2
4928
tinyglobby: 0.2.14
4929
optionalDependencies:
4930
+
'@types/node': 24.2.1
4931
fsevents: 2.3.3
4932
4933
+
vitefu@1.1.1(vite@6.3.5(@types/node@24.2.1)):
4934
optionalDependencies:
4935
+
vite: 6.3.5(@types/node@24.2.1)
4936
4937
+
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1):
4938
dependencies:
4939
'@types/chai': 5.2.2
4940
'@vitest/expect': 3.2.4
4941
+
'@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@24.2.1))
4942
'@vitest/pretty-format': 3.2.4
4943
'@vitest/runner': 3.2.4
4944
'@vitest/snapshot': 3.2.4
···
4956
tinyglobby: 0.2.14
4957
tinypool: 1.1.1
4958
tinyrainbow: 2.0.0
4959
+
vite: 7.1.1(@types/node@24.2.1)
4960
+
vite-node: 3.2.4(@types/node@24.2.1)
4961
why-is-node-running: 2.3.0
4962
optionalDependencies:
4963
'@types/debug': 4.1.12
4964
+
'@types/node': 24.2.1
4965
transitivePeerDependencies:
4966
- jiti
4967
- less
-5
pnpm-workspace.yaml
-5
pnpm-workspace.yaml
+189
tests/parse-blocks.test.ts
+189
tests/parse-blocks.test.ts
···
···
1
+
import { expect, test } from "vitest";
2
+
import { parseBlocks } from "../lib/utils";
3
+
4
+
test("should correctly parse an h1 block to an h2 tag", () => {
5
+
const html = parseBlocks({
6
+
block: {
7
+
$type: "pub.leaflet.pages.linearDocument#block",
8
+
block: {
9
+
$type: "pub.leaflet.blocks.header",
10
+
level: 1,
11
+
facets: [],
12
+
plaintext: "heading 1",
13
+
},
14
+
},
15
+
did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn",
16
+
});
17
+
18
+
expect(html).toMatchInlineSnapshot(`"<h2>heading 1</h2>"`);
19
+
});
20
+
21
+
test("should correctly parse an h2 block to an h3 tag", () => {
22
+
const html = parseBlocks({
23
+
block: {
24
+
$type: "pub.leaflet.pages.linearDocument#block",
25
+
block: {
26
+
$type: "pub.leaflet.blocks.header",
27
+
level: 2,
28
+
facets: [],
29
+
plaintext: "heading 2",
30
+
},
31
+
},
32
+
did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn",
33
+
});
34
+
35
+
expect(html).toMatchInlineSnapshot(`"<h3>heading 2</h3>"`);
36
+
});
37
+
38
+
test("should correctly parse an h3 block to an h4 tag", () => {
39
+
const html = parseBlocks({
40
+
block: {
41
+
$type: "pub.leaflet.pages.linearDocument#block",
42
+
block: {
43
+
$type: "pub.leaflet.blocks.header",
44
+
level: 3,
45
+
facets: [],
46
+
plaintext: "heading 3",
47
+
},
48
+
},
49
+
did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn",
50
+
});
51
+
52
+
expect(html).toMatchInlineSnapshot(`"<h4>heading 3</h4>"`);
53
+
});
54
+
55
+
test("should correctly parse a block with no level to an h6 tag", () => {
56
+
const html = parseBlocks({
57
+
block: {
58
+
$type: "pub.leaflet.pages.linearDocument#block",
59
+
block: {
60
+
$type: "pub.leaflet.blocks.header",
61
+
facets: [],
62
+
plaintext: "heading 6",
63
+
},
64
+
},
65
+
did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn",
66
+
});
67
+
68
+
expect(html).toMatchInlineSnapshot(`"<h6>heading 6</h6>"`);
69
+
});
70
+
71
+
test("should correctly parse an unordered list block", () => {
72
+
const html = parseBlocks({
73
+
block: {
74
+
$type: "pub.leaflet.pages.linearDocument#block",
75
+
block: {
76
+
$type: "pub.leaflet.blocks.unorderedList",
77
+
children: [
78
+
{
79
+
$type: "pub.leaflet.blocks.unorderedList#listItem",
80
+
content: {
81
+
$type: "pub.leaflet.blocks.text",
82
+
facets: [
83
+
{
84
+
index: {
85
+
byteEnd: 18,
86
+
byteStart: 0,
87
+
},
88
+
features: [
89
+
{
90
+
uri: "https://pdsls.dev/",
91
+
$type: "pub.leaflet.richtext.facet#link",
92
+
},
93
+
],
94
+
},
95
+
{
96
+
index: {
97
+
byteEnd: 28,
98
+
byteStart: 22,
99
+
},
100
+
features: [
101
+
{
102
+
uri: "https://bsky.app/profile/juli.ee",
103
+
$type: "pub.leaflet.richtext.facet#link",
104
+
},
105
+
],
106
+
},
107
+
],
108
+
plaintext: "https://pdsls.dev/ by Juliet",
109
+
},
110
+
children: [],
111
+
},
112
+
{
113
+
$type: "pub.leaflet.blocks.unorderedList#listItem",
114
+
content: {
115
+
$type: "pub.leaflet.blocks.text",
116
+
facets: [
117
+
{
118
+
index: {
119
+
byteEnd: 34,
120
+
byteStart: 0,
121
+
},
122
+
features: [
123
+
{
124
+
uri: "https://github.com/mary-ext/atcute",
125
+
$type: "pub.leaflet.richtext.facet#link",
126
+
},
127
+
],
128
+
},
129
+
{
130
+
index: {
131
+
byteEnd: 42,
132
+
byteStart: 38,
133
+
},
134
+
features: [
135
+
{
136
+
uri: "https://bsky.app/profile/mary.my.id",
137
+
$type: "pub.leaflet.richtext.facet#link",
138
+
},
139
+
],
140
+
},
141
+
],
142
+
plaintext: "https://github.com/mary-ext/atcute by mary",
143
+
},
144
+
children: [],
145
+
},
146
+
{
147
+
$type: "pub.leaflet.blocks.unorderedList#listItem",
148
+
content: {
149
+
$type: "pub.leaflet.blocks.text",
150
+
facets: [
151
+
{
152
+
index: {
153
+
byteEnd: 27,
154
+
byteStart: 0,
155
+
},
156
+
features: [
157
+
{
158
+
uri: "https://www.microcosm.blue/",
159
+
$type: "pub.leaflet.richtext.facet#link",
160
+
},
161
+
],
162
+
},
163
+
{
164
+
index: {
165
+
byteEnd: 35,
166
+
byteStart: 31,
167
+
},
168
+
features: [
169
+
{
170
+
uri: "https://bsky.app/profile/bad-example.com",
171
+
$type: "pub.leaflet.richtext.facet#link",
172
+
},
173
+
],
174
+
},
175
+
],
176
+
plaintext: "https://www.microcosm.blue/ by phil",
177
+
},
178
+
children: [],
179
+
},
180
+
],
181
+
},
182
+
},
183
+
did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn",
184
+
});
185
+
186
+
expect(html).toMatchInlineSnapshot(
187
+
`"<ul><li><p><a href="https://pdsls.dev/" target="_blank" rel="noopener noreferrer">https://pdsls.dev/</a> by <a href="https://bsky.app/profile/juli.ee" target="_blank" rel="noopener noreferrer">Juliet</a></p></li><li><p><a href="https://github.com/mary-ext/atcute" target="_blank" rel="noopener noreferrer">https://github.com/mary-ext/atcute</a> by <a href="https://bsky.app/profile/mary.my.id" target="_blank" rel="noopener noreferrer">mary</a></p></li><li><p><a href="https://www.microcosm.blue/" target="_blank" rel="noopener noreferrer">https://www.microcosm.blue/</a> by <a href="https://bsky.app/profile/bad-example.com" target="_blank" rel="noopener noreferrer">phil</a></p></li></ul>"`,
188
+
);
189
+
});
+133
tests/parse-text-blocks.test.ts
+133
tests/parse-text-blocks.test.ts
···
···
1
+
import { expect, test } from "vitest";
2
+
import { parseTextBlock } from "../lib/utils";
3
+
4
+
test("should correctly parse a text block without facets", () => {
5
+
const html = parseTextBlock({
6
+
$type: "pub.leaflet.blocks.text",
7
+
facets: [],
8
+
plaintext: "just plaintext no facets",
9
+
});
10
+
11
+
expect(html).toMatchInlineSnapshot(`"<p>just plaintext no facets</p>"`);
12
+
});
13
+
14
+
test("should correctly parse a text block with bolded text", () => {
15
+
const html = parseTextBlock({
16
+
$type: "pub.leaflet.blocks.text",
17
+
facets: [
18
+
{
19
+
index: {
20
+
byteEnd: 11,
21
+
byteStart: 0,
22
+
},
23
+
features: [
24
+
{
25
+
$type: "pub.leaflet.richtext.facet#bold",
26
+
},
27
+
],
28
+
},
29
+
],
30
+
plaintext: "bolded text with some plaintext",
31
+
});
32
+
33
+
expect(html).toMatchInlineSnapshot(
34
+
`"<p><b>bolded text</b> with some plaintext</p>"`,
35
+
);
36
+
});
37
+
38
+
test("should correctly parse a text block with an inline link", () => {
39
+
const html = parseTextBlock({
40
+
$type: "pub.leaflet.blocks.text",
41
+
facets: [
42
+
{
43
+
index: {
44
+
byteEnd: 27,
45
+
byteStart: 0,
46
+
},
47
+
features: [
48
+
{
49
+
uri: "https://blacksky.community/",
50
+
$type: "pub.leaflet.richtext.facet#link",
51
+
},
52
+
],
53
+
},
54
+
],
55
+
plaintext: "https://blacksky.community/",
56
+
});
57
+
58
+
expect(html).toMatchInlineSnapshot(
59
+
`"<p><a href="https://blacksky.community/" target="_blank" rel="noopener noreferrer">https://blacksky.community/</a></p>"`,
60
+
);
61
+
});
62
+
63
+
test("should correctly parse a text block with strikethrough text", () => {
64
+
const html = parseTextBlock({
65
+
$type: "pub.leaflet.blocks.text",
66
+
facets: [
67
+
{
68
+
index: {
69
+
byteEnd: 13,
70
+
byteStart: 0,
71
+
},
72
+
features: [
73
+
{
74
+
$type: "pub.leaflet.richtext.facet#strikethrough",
75
+
},
76
+
],
77
+
},
78
+
],
79
+
plaintext: "strikethrough text with some plaintext",
80
+
});
81
+
82
+
expect(html).toMatchInlineSnapshot(
83
+
`"<p><s>strikethrough</s> text with some plaintext</p>"`,
84
+
);
85
+
});
86
+
87
+
test("should correctly parse a text block with underlined text", () => {
88
+
const html = parseTextBlock({
89
+
$type: "pub.leaflet.blocks.text",
90
+
facets: [
91
+
{
92
+
index: {
93
+
byteEnd: 10,
94
+
byteStart: 0,
95
+
},
96
+
features: [
97
+
{
98
+
$type: "pub.leaflet.richtext.facet#underline",
99
+
},
100
+
],
101
+
},
102
+
],
103
+
plaintext: "underlined text with some plaintext",
104
+
});
105
+
106
+
expect(html).toMatchInlineSnapshot(
107
+
`"<p><span style="text-decoration:underline;">underlined</span> text with some plaintext</p>"`,
108
+
);
109
+
});
110
+
111
+
test("should correctly parse a text block with italicized text", () => {
112
+
const html = parseTextBlock({
113
+
$type: "pub.leaflet.blocks.text",
114
+
facets: [
115
+
{
116
+
index: {
117
+
byteEnd: 10,
118
+
byteStart: 0,
119
+
},
120
+
features: [
121
+
{
122
+
$type: "pub.leaflet.richtext.facet#italic",
123
+
},
124
+
],
125
+
},
126
+
],
127
+
plaintext: "italicized text with some plaintext",
128
+
});
129
+
130
+
expect(html).toMatchInlineSnapshot(
131
+
`"<p><i>italicized</i> text with some plaintext</p>"`,
132
+
);
133
+
});