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
+48 -61
View File
@@ -1,4 +1,4 @@
import {h} from 'preact' import {ComponentChild, h} from 'preact'
import logo from '../../assets/logo_full_name.svg' import logo from '../../assets/logo_full_name.svg'
import {Like as LikeIcon} from '../icons/Like' 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 {Robot as RobotIcon} from '../icons/Robot'
import {CONTENT_LABELS} from '../labels' import {CONTENT_LABELS} from '../labels'
import * as app from '../lexicons/app' import * as app from '../lexicons/app'
import {FacetRenderer} from '../util/facet-renderer'
import {niceDate} from '../util/nice-date' import {niceDate} from '../util/nice-date'
import {prettyNumber} from '../util/pretty-number' import {prettyNumber} from '../util/pretty-number'
import {getRkey} from '../util/rkey' 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 // render an empty <p> that adds an extra flex gap above the embed
if (!record?.text) return null if (!record?.text) return null
// const rt = new RichText({ const rt = new FacetRenderer({text: record.text, facets: record.facets})
// text: record.text, const richText: ComponentChild[] = []
// facets: record.facets,
// })
// const richText = [] let counter = 0
for (const segment of rt.segments()) {
// let counter = 0 if (segment.link) {
// for (const segment of rt.segments()) { richText.push(
// if ( <Link
// segment.link && key={counter}
// app.bsky.richtext.facet.link.$safeValidate(segment.link).success href={segment.link.uri}
// ) { className="text-brand hover:underline"
// richText.push( disableTracking={
// <Link !segment.link.uri.startsWith('https://bsky.app') &&
// key={counter} !segment.link.uri.startsWith('https://go.bsky.app')
// href={segment.link.uri} }>
// className="text-brand hover:underline" {segment.text}
// disableTracking={ </Link>,
// !segment.link.uri.startsWith('https://bsky.app') && )
// !segment.link.uri.startsWith('https://go.bsky.app') } else if (segment.mention) {
// }> richText.push(
// {segment.text} <Link
// </Link>, key={counter}
// ) href={`/profile/${segment.mention.did}`}
// } else if ( className="text-brand hover:underline">
// segment.mention && {segment.text}
// app.bsky.richtext.facet.mention.safeValidate(segment.mention).success </Link>,
// ) { )
// richText.push( } else if (segment.tag) {
// <Link richText.push(
// key={counter} <Link
// href={`/profile/${segment.mention.did}`} key={counter}
// className="text-brand hover:underline"> href={`/hashtag/${segment.tag.tag}`}
// {segment.text} className="text-brand hover:underline">
// </Link>, {segment.text}
// ) </Link>,
// } else if ( )
// segment.tag && } else {
// app.bsky.richtext.facet.tag.safeValidate(segment.tag).success richText.push(segment.text)
// ) { }
// richText.push( counter++
// <Link }
// key={counter}
// href={`/hashtag/${segment.tag.tag}`} return (
// className="text-brand hover:underline"> <p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
// {segment.text} {richText}
// </Link>, </p>
// ) )
// } 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>
// )
} }
+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) // (https://embed.bsky.app)
window.BSKY_DEV_EMBED_URL = 'http://localhost:5173' window.BSKY_DEV_EMBED_URL = 'http://localhost:5173'
</script> </script>
<script <script async src="http://localhost:3000/embed.js" charset="utf-8"></script>
async
src="http://localhost:3000/embed.js"
charset="utf-8"></script>
</head> </head>
<body class="p12"> <body class="p12">
<!-- Base embeds --> <!-- Base embeds -->
@@ -1048,6 +1045,34 @@
<div>Verified quoted</div> <div>Verified quoted</div>
</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> </div>
</section> </section>
</body> </body>