+5
.changeset/tired-suits-double.md
+5
.changeset/tired-suits-double.md
+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
+
}
+1
lib/lexicons/index.ts
+1
lib/lexicons/index.ts
···
1
1
export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js";
2
+
export * as PubLeafletBlocksBlockquote from "./types/pub/leaflet/blocks/blockquote.js";
2
3
export * as PubLeafletBlocksCode from "./types/pub/leaflet/blocks/code.js";
3
4
export * as PubLeafletBlocksHeader from "./types/pub/leaflet/blocks/header.js";
4
5
export * as PubLeafletBlocksHorizontalRule from "./types/pub/leaflet/blocks/horizontalRule.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> {}
+1
-1
lib/lexicons/types/pub/leaflet/blocks/image.ts
+1
-1
lib/lexicons/types/pub/leaflet/blocks/image.ts
+60
-48
lib/utils.ts
+60
-48
lib/utils.ts
···
4
4
import katex from "katex";
5
5
import sanitizeHTML from "sanitize-html";
6
6
import {
7
+
PubLeafletBlocksBlockquote,
7
8
PubLeafletBlocksCode,
8
9
PubLeafletBlocksHeader,
9
10
PubLeafletBlocksHorizontalRule,
···
182
183
"hr",
183
184
"div",
184
185
"span",
186
+
"blockquote",
185
187
],
186
188
selfClosing: ["img"],
187
189
});
···
244
246
}
245
247
}
246
248
247
-
function parseBlocks({
249
+
export function parseTextBlock(block: PubLeafletBlocksText.Main) {
250
+
let html = "";
251
+
const rt = new RichText({
252
+
text: block.plaintext,
253
+
facets: block.facets || [],
254
+
});
255
+
const children = [];
256
+
for (const segment of rt.segments()) {
257
+
const link = segment.facet?.find(
258
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#link",
259
+
);
260
+
const isBold = segment.facet?.find(
261
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#bold",
262
+
);
263
+
const isCode = segment.facet?.find(
264
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#code",
265
+
);
266
+
const isStrikethrough = segment.facet?.find(
267
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#strikethrough",
268
+
);
269
+
const isUnderline = segment.facet?.find(
270
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#underline",
271
+
);
272
+
const isItalic = segment.facet?.find(
273
+
(segment) => segment.$type === "pub.leaflet.richtext.facet#italic",
274
+
);
275
+
if (isCode) {
276
+
children.push(`<pre><code>${segment.text}</code></pre>`);
277
+
} else if (link) {
278
+
children.push(
279
+
`<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`,
280
+
);
281
+
} else if (isBold) {
282
+
children.push(`<b>${segment.text}</b>`);
283
+
} else if (isStrikethrough) {
284
+
children.push(`<s>${segment.text}</s>`);
285
+
} else if (isUnderline) {
286
+
children.push(
287
+
`<span style="text-decoration:underline;">${segment.text}</span>`,
288
+
);
289
+
} else if (isItalic) {
290
+
children.push(`<i>${segment.text}</i>`);
291
+
} else {
292
+
children.push(`${segment.text}`);
293
+
}
294
+
}
295
+
html += `<p>${children.join("")}</p>`;
296
+
297
+
return html.trim();
298
+
}
299
+
300
+
export function parseBlocks({
248
301
block,
249
302
did,
250
303
}: {
···
254
307
let html = "";
255
308
256
309
if (is(PubLeafletBlocksText.mainSchema, block.block)) {
257
-
const rt = new RichText({
258
-
text: block.block.plaintext,
259
-
facets: block.block.facets || [],
260
-
});
261
-
const children = [];
262
-
for (const segment of rt.segments()) {
263
-
const link = segment.facet?.find(
264
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#link",
265
-
);
266
-
const isBold = segment.facet?.find(
267
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#bold",
268
-
);
269
-
const isCode = segment.facet?.find(
270
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#code",
271
-
);
272
-
const isStrikethrough = segment.facet?.find(
273
-
(segment) =>
274
-
segment.$type === "pub.leaflet.richtext.facet#strikethrough",
275
-
);
276
-
const isUnderline = segment.facet?.find(
277
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#underline",
278
-
);
279
-
const isItalic = segment.facet?.find(
280
-
(segment) => segment.$type === "pub.leaflet.richtext.facet#italic",
281
-
);
282
-
if (isCode) {
283
-
children.push(`<pre><code>${segment.text}</code></pre>`);
284
-
} else if (link) {
285
-
children.push(
286
-
`<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`,
287
-
);
288
-
} else if (isBold) {
289
-
children.push(`<b>${segment.text}</b>`);
290
-
} else if (isStrikethrough) {
291
-
children.push(`<s>${segment.text}</s>`);
292
-
} else if (isUnderline) {
293
-
children.push(
294
-
`<span style="text-decoration:underline;">${segment.text}</span>`,
295
-
);
296
-
} else if (isItalic) {
297
-
children.push(`<i>${segment.text}</i>`);
298
-
} else {
299
-
children.push(`${segment.text}`);
300
-
}
301
-
}
302
-
html += `<p>${children.join("")}</p>`;
310
+
html += parseTextBlock(block.block);
303
311
}
304
312
305
313
if (is(PubLeafletBlocksHeader.mainSchema, block.block)) {
···
343
351
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>`;
344
352
}
345
353
354
+
if (is(PubLeafletBlocksBlockquote.mainSchema, block.block)) {
355
+
html += `<blockquote>${parseTextBlock(block.block)}</blockquote>`;
356
+
}
357
+
346
358
return html.trim();
347
359
}
348
360
349
-
function renderListItem({
361
+
export function renderListItem({
350
362
item,
351
363
did,
352
364
}: {
+1
-1
package.json
+1
-1
package.json
+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
+
});