tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
create static textblock component for rss
awarm.space
7 months ago
5766f221
838f8806
+205
-144
3 changed files
expand all
collapse all
unified
split
app
lish
[did]
[publication]
[rkey]
BaseTextBlock.tsx
StaticPostContent.tsx
TextBlock.tsx
+171
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
···
1
1
+
"use client";
2
2
+
import { UnicodeString } from "@atproto/api";
3
3
+
import { PubLeafletRichtextFacet } from "lexicons/api";
4
4
+
5
5
+
type Facet = PubLeafletRichtextFacet.Main;
6
6
+
export function BaseTextBlock(props: {
7
7
+
plaintext: string;
8
8
+
facets?: Facet[];
9
9
+
index: number[];
10
10
+
preview?: boolean;
11
11
+
}) {
12
12
+
let children = [];
13
13
+
let richText = new RichText({
14
14
+
text: props.plaintext,
15
15
+
facets: props.facets || [],
16
16
+
});
17
17
+
let counter = 0;
18
18
+
for (const segment of richText.segments()) {
19
19
+
let id = segment.facet?.find(PubLeafletRichtextFacet.isId);
20
20
+
let link = segment.facet?.find(PubLeafletRichtextFacet.isLink);
21
21
+
let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold);
22
22
+
let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode);
23
23
+
let isStrikethrough = segment.facet?.find(
24
24
+
PubLeafletRichtextFacet.isStrikethrough,
25
25
+
);
26
26
+
let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline);
27
27
+
let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic);
28
28
+
let isHighlighted = segment.facet?.find(
29
29
+
PubLeafletRichtextFacet.isHighlight,
30
30
+
);
31
31
+
let className = `
32
32
+
${isCode ? "inline-code" : ""}
33
33
+
${id ? "scroll-mt-12 scroll-mb-10" : ""}
34
34
+
${isBold ? "font-bold" : ""}
35
35
+
${isItalic ? "italic" : ""}
36
36
+
${isUnderline ? "underline" : ""}
37
37
+
${isStrikethrough ? "line-through decoration-tertiary" : ""}
38
38
+
${isHighlighted ? "highlight bg-highlight-1" : ""}`;
39
39
+
40
40
+
if (isCode) {
41
41
+
children.push(
42
42
+
<code key={counter} className={className} id={id?.id}>
43
43
+
{segment.text}
44
44
+
</code>,
45
45
+
);
46
46
+
} else if (link) {
47
47
+
children.push(
48
48
+
<a
49
49
+
key={counter}
50
50
+
href={link.uri}
51
51
+
className={`text-accent-contrast hover:underline ${className}`}
52
52
+
target="_blank"
53
53
+
>
54
54
+
{segment.text}
55
55
+
</a>,
56
56
+
);
57
57
+
} else {
58
58
+
children.push(
59
59
+
<span key={counter} className={className} id={id?.id}>
60
60
+
{segment.text}
61
61
+
</span>,
62
62
+
);
63
63
+
}
64
64
+
65
65
+
counter++;
66
66
+
}
67
67
+
return <>{children}</>;
68
68
+
}
69
69
+
70
70
+
type RichTextSegment = {
71
71
+
text: string;
72
72
+
facet?: Exclude<Facet["features"], { $type: string }>;
73
73
+
};
74
74
+
75
75
+
export class RichText {
76
76
+
unicodeText: UnicodeString;
77
77
+
facets?: Facet[];
78
78
+
79
79
+
constructor(props: { text: string; facets: Facet[] }) {
80
80
+
this.unicodeText = new UnicodeString(props.text);
81
81
+
this.facets = props.facets;
82
82
+
if (this.facets) {
83
83
+
this.facets = this.facets
84
84
+
.filter((facet) => facet.index.byteStart <= facet.index.byteEnd)
85
85
+
.sort((a, b) => a.index.byteStart - b.index.byteStart);
86
86
+
}
87
87
+
}
88
88
+
89
89
+
*segments(): Generator<RichTextSegment, void, void> {
90
90
+
const facets = this.facets || [];
91
91
+
if (!facets.length) {
92
92
+
yield { text: this.unicodeText.utf16 };
93
93
+
return;
94
94
+
}
95
95
+
96
96
+
let textCursor = 0;
97
97
+
let facetCursor = 0;
98
98
+
do {
99
99
+
const currFacet = facets[facetCursor];
100
100
+
if (textCursor < currFacet.index.byteStart) {
101
101
+
yield {
102
102
+
text: this.unicodeText.slice(textCursor, currFacet.index.byteStart),
103
103
+
};
104
104
+
} else if (textCursor > currFacet.index.byteStart) {
105
105
+
facetCursor++;
106
106
+
continue;
107
107
+
}
108
108
+
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
109
109
+
const subtext = this.unicodeText.slice(
110
110
+
currFacet.index.byteStart,
111
111
+
currFacet.index.byteEnd,
112
112
+
);
113
113
+
if (!subtext.trim()) {
114
114
+
// dont empty string entities
115
115
+
yield { text: subtext };
116
116
+
} else {
117
117
+
yield { text: subtext, facet: currFacet.features };
118
118
+
}
119
119
+
}
120
120
+
textCursor = currFacet.index.byteEnd;
121
121
+
facetCursor++;
122
122
+
} while (facetCursor < facets.length);
123
123
+
if (textCursor < this.unicodeText.length) {
124
124
+
yield {
125
125
+
text: this.unicodeText.slice(textCursor, this.unicodeText.length),
126
126
+
};
127
127
+
}
128
128
+
}
129
129
+
}
130
130
+
function addFacet(facets: Facet[], newFacet: Facet, length: number) {
131
131
+
if (facets.length === 0) {
132
132
+
return [newFacet];
133
133
+
}
134
134
+
135
135
+
const allFacets = [...facets, newFacet];
136
136
+
137
137
+
// Collect all boundary positions
138
138
+
const boundaries = new Set<number>();
139
139
+
boundaries.add(0);
140
140
+
boundaries.add(length);
141
141
+
142
142
+
for (const facet of allFacets) {
143
143
+
boundaries.add(facet.index.byteStart);
144
144
+
boundaries.add(facet.index.byteEnd);
145
145
+
}
146
146
+
147
147
+
const sortedBoundaries = Array.from(boundaries).sort((a, b) => a - b);
148
148
+
const result: Facet[] = [];
149
149
+
150
150
+
// Process segments between consecutive boundaries
151
151
+
for (let i = 0; i < sortedBoundaries.length - 1; i++) {
152
152
+
const start = sortedBoundaries[i];
153
153
+
const end = sortedBoundaries[i + 1];
154
154
+
155
155
+
// Find facets that are active at the start position
156
156
+
const activeFacets = allFacets.filter(
157
157
+
(facet) => facet.index.byteStart <= start && facet.index.byteEnd > start,
158
158
+
);
159
159
+
160
160
+
// Only create facet if there are active facets (features present)
161
161
+
if (activeFacets.length > 0) {
162
162
+
const features = activeFacets.flatMap((f) => f.features);
163
163
+
result.push({
164
164
+
index: { byteStart: start, byteEnd: end },
165
165
+
features,
166
166
+
});
167
167
+
}
168
168
+
}
169
169
+
170
170
+
return result;
171
171
+
}
+6
-6
app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx
···
10
10
PubLeafletPagesLinearDocument,
11
11
} from "lexicons/api";
12
12
import { blobRefToSrc } from "src/utils/blobRefToSrc";
13
13
-
import { TextBlock } from "./TextBlock";
13
13
+
import { BaseTextBlock } from "./BaseTextBlock";
14
14
import { StaticMathBlock } from "./StaticMathBlock";
15
15
import { codeToHtml } from "shiki";
16
16
···
96
96
case PubLeafletBlocksText.isMain(b.block):
97
97
return (
98
98
<p>
99
99
-
<TextBlock
99
99
+
<BaseTextBlock
100
100
facets={b.block.facets}
101
101
plaintext={b.block.plaintext}
102
102
index={[]}
···
107
107
if (b.block.level === 1)
108
108
return (
109
109
<h1>
110
110
-
<TextBlock {...b.block} index={[]} />
110
110
+
<BaseTextBlock {...b.block} index={[]} />
111
111
</h1>
112
112
);
113
113
if (b.block.level === 2)
114
114
return (
115
115
<h2>
116
116
-
<TextBlock {...b.block} index={[]} />
116
116
+
<BaseTextBlock {...b.block} index={[]} />
117
117
</h2>
118
118
);
119
119
if (b.block.level === 3)
120
120
return (
121
121
<h3>
122
122
-
<TextBlock {...b.block} index={[]} />
122
122
+
<BaseTextBlock {...b.block} index={[]} />
123
123
</h3>
124
124
);
125
125
// if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>;
126
126
// if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>;
127
127
return (
128
128
<h6>
129
129
-
<TextBlock {...b.block} index={[]} />
129
129
+
<BaseTextBlock {...b.block} index={[]} />
130
130
</h6>
131
131
);
132
132
}
+28
-138
app/lish/[did]/[publication]/[rkey]/TextBlock.tsx
···
3
3
import { PubLeafletRichtextFacet } from "lexicons/api";
4
4
import { useMemo } from "react";
5
5
import { useHighlight } from "./useHighlight";
6
6
+
import { BaseTextBlock } from "./BaseTextBlock";
6
7
7
8
type Facet = PubLeafletRichtextFacet.Main;
8
9
export function TextBlock(props: {
···
13
14
}) {
14
15
let children = [];
15
16
let highlights = useHighlight(props.index);
16
16
-
let richText = useMemo(() => {
17
17
+
let facets = useMemo(() => {
18
18
+
if (props.preview) return props.facets;
17
19
let facets = [...(props.facets || [])];
18
18
-
if (!props.preview) {
19
19
-
for (let highlight of highlights) {
20
20
-
facets = addFacet(
21
21
-
facets,
22
22
-
{
23
23
-
index: {
24
24
-
byteStart: highlight.startOffset
25
25
-
? new UnicodeString(
26
26
-
props.plaintext.slice(0, highlight.startOffset),
27
27
-
).length
28
28
-
: 0,
29
29
-
byteEnd: new UnicodeString(
30
30
-
props.plaintext.slice(0, highlight.endOffset ?? undefined),
31
31
-
).length,
32
32
-
},
33
33
-
features: [
34
34
-
{ $type: "pub.leaflet.richtext.facet#highlight" },
35
35
-
{
36
36
-
$type: "pub.leaflet.richtext.facet#id",
37
37
-
id: `${props.index.join(".")}_${highlight.startOffset || 0}`,
38
38
-
},
39
39
-
],
20
20
+
for (let highlight of highlights) {
21
21
+
facets = addFacet(
22
22
+
facets,
23
23
+
{
24
24
+
index: {
25
25
+
byteStart: highlight.startOffset
26
26
+
? new UnicodeString(
27
27
+
props.plaintext.slice(0, highlight.startOffset),
28
28
+
).length
29
29
+
: 0,
30
30
+
byteEnd: new UnicodeString(
31
31
+
props.plaintext.slice(0, highlight.endOffset ?? undefined),
32
32
+
).length,
40
33
},
41
41
-
new UnicodeString(props.plaintext).length,
42
42
-
);
43
43
-
}
44
44
-
}
45
45
-
return new RichText({ text: props.plaintext, facets });
46
46
-
}, [props.plaintext, props.facets, highlights, props.preview]);
47
47
-
let counter = 0;
48
48
-
for (const segment of richText.segments()) {
49
49
-
let id = segment.facet?.find(PubLeafletRichtextFacet.isId);
50
50
-
let link = segment.facet?.find(PubLeafletRichtextFacet.isLink);
51
51
-
let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold);
52
52
-
let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode);
53
53
-
let isStrikethrough = segment.facet?.find(
54
54
-
PubLeafletRichtextFacet.isStrikethrough,
55
55
-
);
56
56
-
let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline);
57
57
-
let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic);
58
58
-
let isHighlighted = segment.facet?.find(
59
59
-
PubLeafletRichtextFacet.isHighlight,
60
60
-
);
61
61
-
let className = `
62
62
-
${isCode ? "inline-code" : ""}
63
63
-
${id ? "scroll-mt-12 scroll-mb-10" : ""}
64
64
-
${isBold ? "font-bold" : ""}
65
65
-
${isItalic ? "italic" : ""}
66
66
-
${isUnderline ? "underline" : ""}
67
67
-
${isStrikethrough ? "line-through decoration-tertiary" : ""}
68
68
-
${isHighlighted ? "highlight bg-highlight-1" : ""}`;
69
69
-
70
70
-
if (isCode) {
71
71
-
children.push(
72
72
-
<code key={counter} className={className} id={id?.id}>
73
73
-
{segment.text}
74
74
-
</code>,
75
75
-
);
76
76
-
} else if (link) {
77
77
-
children.push(
78
78
-
<a
79
79
-
key={counter}
80
80
-
href={link.uri}
81
81
-
className={`text-accent-contrast hover:underline ${className}`}
82
82
-
target="_blank"
83
83
-
>
84
84
-
{segment.text}
85
85
-
</a>,
86
86
-
);
87
87
-
} else {
88
88
-
children.push(
89
89
-
<span key={counter} className={className} id={id?.id}>
90
90
-
{segment.text}
91
91
-
</span>,
34
34
+
features: [
35
35
+
{ $type: "pub.leaflet.richtext.facet#highlight" },
36
36
+
{
37
37
+
$type: "pub.leaflet.richtext.facet#id",
38
38
+
id: `${props.index.join(".")}_${highlight.startOffset || 0}`,
39
39
+
},
40
40
+
],
41
41
+
},
42
42
+
new UnicodeString(props.plaintext).length,
92
43
);
93
44
}
94
94
-
95
95
-
counter++;
96
96
-
}
97
97
-
return <>{children}</>;
45
45
+
return facets;
46
46
+
}, [props.plaintext, props.facets, highlights, props.preview]);
47
47
+
return <BaseTextBlock {...props} facets={facets} />;
98
48
}
99
49
100
100
-
type RichTextSegment = {
101
101
-
text: string;
102
102
-
facet?: Exclude<Facet["features"], { $type: string }>;
103
103
-
};
104
104
-
105
105
-
export class RichText {
106
106
-
unicodeText: UnicodeString;
107
107
-
facets?: Facet[];
108
108
-
109
109
-
constructor(props: { text: string; facets: Facet[] }) {
110
110
-
this.unicodeText = new UnicodeString(props.text);
111
111
-
this.facets = props.facets;
112
112
-
if (this.facets) {
113
113
-
this.facets = this.facets
114
114
-
.filter((facet) => facet.index.byteStart <= facet.index.byteEnd)
115
115
-
.sort((a, b) => a.index.byteStart - b.index.byteStart);
116
116
-
}
117
117
-
}
118
118
-
119
119
-
*segments(): Generator<RichTextSegment, void, void> {
120
120
-
const facets = this.facets || [];
121
121
-
if (!facets.length) {
122
122
-
yield { text: this.unicodeText.utf16 };
123
123
-
return;
124
124
-
}
125
125
-
126
126
-
let textCursor = 0;
127
127
-
let facetCursor = 0;
128
128
-
do {
129
129
-
const currFacet = facets[facetCursor];
130
130
-
if (textCursor < currFacet.index.byteStart) {
131
131
-
yield {
132
132
-
text: this.unicodeText.slice(textCursor, currFacet.index.byteStart),
133
133
-
};
134
134
-
} else if (textCursor > currFacet.index.byteStart) {
135
135
-
facetCursor++;
136
136
-
continue;
137
137
-
}
138
138
-
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
139
139
-
const subtext = this.unicodeText.slice(
140
140
-
currFacet.index.byteStart,
141
141
-
currFacet.index.byteEnd,
142
142
-
);
143
143
-
if (!subtext.trim()) {
144
144
-
// dont empty string entities
145
145
-
yield { text: subtext };
146
146
-
} else {
147
147
-
yield { text: subtext, facet: currFacet.features };
148
148
-
}
149
149
-
}
150
150
-
textCursor = currFacet.index.byteEnd;
151
151
-
facetCursor++;
152
152
-
} while (facetCursor < facets.length);
153
153
-
if (textCursor < this.unicodeText.length) {
154
154
-
yield {
155
155
-
text: this.unicodeText.slice(textCursor, this.unicodeText.length),
156
156
-
};
157
157
-
}
158
158
-
}
159
159
-
}
160
50
function addFacet(facets: Facet[], newFacet: Facet, length: number) {
161
51
if (facets.length === 0) {
162
52
return [newFacet];