add a custom fact renderer

This commit is contained in:
Samuel Newman
2026-05-13 15:47:56 +03:00
parent a8fae7c8fe
commit 334dc018bf
3 changed files with 163 additions and 64 deletions
+47 -60
View File
@@ -1,4 +1,4 @@
import {h} from 'preact'
import {ComponentChild, h} from 'preact'
import logo from '../../assets/logo_full_name.svg'
import {Like as LikeIcon} from '../icons/Like'
@@ -7,6 +7,7 @@ import {Repost as RepostIcon} from '../icons/Repost'
import {Robot as RobotIcon} from '../icons/Robot'
import {CONTENT_LABELS} from '../labels'
import * as app from '../lexicons/app'
import {FacetRenderer} from '../util/facet-renderer'
import {niceDate} from '../util/nice-date'
import {prettyNumber} from '../util/pretty-number'
import {getRkey} from '../util/rkey'
@@ -146,65 +147,51 @@ function PostContent({record}: {record: app.bsky.feed.post.Main | null}) {
// render an empty <p> that adds an extra flex gap above the embed
if (!record?.text) return null
// const rt = new RichText({
// text: record.text,
// facets: record.facets,
// })
const rt = new FacetRenderer({text: record.text, facets: record.facets})
const richText: ComponentChild[] = []
// const richText = []
let counter = 0
for (const segment of rt.segments()) {
if (segment.link) {
richText.push(
<Link
key={counter}
href={segment.link.uri}
className="text-brand hover:underline"
disableTracking={
!segment.link.uri.startsWith('https://bsky.app') &&
!segment.link.uri.startsWith('https://go.bsky.app')
}>
{segment.text}
</Link>,
)
} else if (segment.mention) {
richText.push(
<Link
key={counter}
href={`/profile/${segment.mention.did}`}
className="text-brand hover:underline">
{segment.text}
</Link>,
)
} else if (segment.tag) {
richText.push(
<Link
key={counter}
href={`/hashtag/${segment.tag.tag}`}
className="text-brand hover:underline">
{segment.text}
</Link>,
)
} else {
richText.push(segment.text)
}
counter++
}
// let counter = 0
// for (const segment of rt.segments()) {
// if (
// segment.link &&
// app.bsky.richtext.facet.link.$safeValidate(segment.link).success
// ) {
// richText.push(
// <Link
// key={counter}
// href={segment.link.uri}
// className="text-brand hover:underline"
// disableTracking={
// !segment.link.uri.startsWith('https://bsky.app') &&
// !segment.link.uri.startsWith('https://go.bsky.app')
// }>
// {segment.text}
// </Link>,
// )
// } else if (
// segment.mention &&
// app.bsky.richtext.facet.mention.safeValidate(segment.mention).success
// ) {
// richText.push(
// <Link
// key={counter}
// href={`/profile/${segment.mention.did}`}
// className="text-brand hover:underline">
// {segment.text}
// </Link>,
// )
// } else if (
// segment.tag &&
// app.bsky.richtext.facet.tag.safeValidate(segment.tag).success
// ) {
// richText.push(
// <Link
// key={counter}
// href={`/hashtag/${segment.tag.tag}`}
// className="text-brand hover:underline">
// {segment.text}
// </Link>,
// )
// } else {
// richText.push(segment.text)
// }
// counter++
// }
// return (
// <p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
// {richText}
// </p>
// )
return (
<p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
{richText}
</p>
)
}
+87
View File
@@ -0,0 +1,87 @@
import * as facetDefs from '../lexicons/app/bsky/richtext/facet.defs'
type Facet = facetDefs.Main
type Link = facetDefs.Link
type Mention = facetDefs.Mention
type Tag = facetDefs.Tag
const encoder = new TextEncoder()
const decoder = new TextDecoder()
export class FacetSegment {
constructor(
public text: string,
public facet?: Facet,
) {}
get link(): Link | undefined {
return this.facet?.features.find(
f => f.$type === 'app.bsky.richtext.facet#link',
) as Link | undefined
}
get mention(): Mention | undefined {
return this.facet?.features.find(
f => f.$type === 'app.bsky.richtext.facet#mention',
) as Mention | undefined
}
get tag(): Tag | undefined {
return this.facet?.features.find(
f => f.$type === 'app.bsky.richtext.facet#tag',
) as Tag | undefined
}
}
export class FacetRenderer {
private utf8: Uint8Array
private facets: Facet[]
constructor({text, facets}: {text: string; facets?: Facet[]}) {
this.utf8 = encoder.encode(text)
this.facets = (facets ?? [])
.filter(f => f.index.byteStart <= f.index.byteEnd)
.sort((a, b) => a.index.byteStart - b.index.byteStart)
}
private slice(start: number, end: number): string {
return decoder.decode(this.utf8.slice(start, end))
}
*segments(): Generator<FacetSegment, void, void> {
if (!this.facets.length) {
yield new FacetSegment(decoder.decode(this.utf8))
return
}
let textCursor = 0
let facetCursor = 0
do {
const currFacet = this.facets[facetCursor]
if (textCursor < currFacet.index.byteStart) {
yield new FacetSegment(
this.slice(textCursor, currFacet.index.byteStart),
)
} else if (textCursor > currFacet.index.byteStart) {
facetCursor++
continue
}
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
const subtext = this.slice(
currFacet.index.byteStart,
currFacet.index.byteEnd,
)
if (!subtext.trim()) {
yield new FacetSegment(subtext)
} else {
yield new FacetSegment(subtext, currFacet)
}
}
textCursor = currFacet.index.byteEnd
facetCursor++
} while (facetCursor < this.facets.length)
if (textCursor < this.utf8.byteLength) {
yield new FacetSegment(this.slice(textCursor, this.utf8.byteLength))
}
}
}
+29 -4
View File
@@ -35,10 +35,7 @@
// (https://embed.bsky.app)
window.BSKY_DEV_EMBED_URL = 'http://localhost:5173'
</script>
<script
async
src="http://localhost:3000/embed.js"
charset="utf-8"></script>
<script async src="http://localhost:3000/embed.js" charset="utf-8"></script>
</head>
<body class="p12">
<!-- Base embeds -->
@@ -1048,6 +1045,34 @@
<div>Verified quoted</div>
</div>
<div class="item">
<blockquote
class="bluesky-embed"
data-bluesky-uri="at://did:plc:mbsyz5d4psc7m5ri57ikievz/app.bsky.feed.post/3mljg3kmf4226"
data-bluesky-cid="bafyreidho2pt4sdq3bwgnb62pbnuvucjsxvgk3jq2t3iqzblbdb2pmjwpe"
data-bluesky-embed-color-mode="system">
<p lang="en">
#tangled is everything I wanted from a modern #git forge: -
#atproto based (this is HUGE!) - European - Decentralized -
Federated - Social with #vouching - Built with #golang and using a
#permissive #MIT license What not to like there?<br /><br /><a
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz/post/3mljg3kmf4226?ref_src=embed"
>[image or embed]</a
>
</p>
&mdash; perpetual screaming 🇺🇦 (<a
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz?ref_src=embed"
>@bazub.zubko.cc</a
>)
<a
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz/post/3mljg3kmf4226?ref_src=embed"
>10 May 2026 at 21:43</a
>
</blockquote>
<div>Lots of hashtags</div>
</div>
</div>
</section>
</body>