diff --git a/app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx b/app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx index 8b90b8a4..bf8b0cb2 100644 --- a/app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx @@ -1,203 +1,26 @@ -import { UnicodeString } from "@atproto/api"; -import { PubLeafletRichtextFacet } from "lexicons/api"; -import { didToBlueskyUrl } from "src/utils/mentionUtils"; -import { AtMentionLink } from "components/AtMentionLink"; import { ProfilePopover } from "components/ProfilePopover"; - -type Facet = PubLeafletRichtextFacet.Main; -export function BaseTextBlock(props: { - plaintext: string; - facets?: Facet[]; - index: number[]; - preview?: boolean; -}) { - let children = []; - let richText = new RichText({ - text: props.plaintext, - facets: props.facets || [], - }); - let counter = 0; - for (const segment of richText.segments()) { - let id = segment.facet?.find(PubLeafletRichtextFacet.isId); - let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); - let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); - let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); - let isStrikethrough = segment.facet?.find( - PubLeafletRichtextFacet.isStrikethrough, - ); - let isDidMention = segment.facet?.find( - PubLeafletRichtextFacet.isDidMention, - ); - let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); - let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); - let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); - let isHighlighted = segment.facet?.find( - PubLeafletRichtextFacet.isHighlight, - ); - let className = ` - ${isCode ? "inline-code" : ""} - ${id ? "scroll-mt-12 scroll-mb-10" : ""} - ${isBold ? "font-bold" : ""} - ${isItalic ? "italic" : ""} - ${isUnderline ? "underline" : ""} - ${isStrikethrough ? "line-through decoration-tertiary" : ""} - ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); - - // Split text by newlines and insert
tags - const textParts = segment.text.split("\n"); - const renderedText = textParts.flatMap((part, i) => - i < textParts.length - 1 - ? [part,
] - : [part], - ); - - if (isCode) { - children.push( - - {renderedText} - , - ); - } else if (isDidMention) { - children.push( - {renderedText}} - />, - ); - } else if (isAtMention) { - children.push( - - {renderedText} - , - ); - } else if (link) { - children.push( - - {renderedText} - , - ); - } else { - children.push( - - {renderedText} - , - ); - } - - counter++; - } - return <>{children}; +import { TextBlockCore, TextBlockCoreProps, RichText } from "./TextBlockCore"; +import { ReactNode } from "react"; + +// Re-export RichText for backwards compatibility +export { RichText }; + +function DidMentionWithPopover(props: { did: string; children: ReactNode }) { + return ( + + ); } -type RichTextSegment = { - text: string; - facet?: Exclude; -}; - -export class RichText { - unicodeText: UnicodeString; - facets?: Facet[]; - - constructor(props: { text: string; facets: Facet[] }) { - this.unicodeText = new UnicodeString(props.text); - this.facets = props.facets; - if (this.facets) { - this.facets = this.facets - .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) - .sort((a, b) => a.index.byteStart - b.index.byteStart); - } - } - - *segments(): Generator { - const facets = this.facets || []; - if (!facets.length) { - yield { text: this.unicodeText.utf16 }; - return; - } - - let textCursor = 0; - let facetCursor = 0; - do { - const currFacet = facets[facetCursor]; - if (textCursor < currFacet.index.byteStart) { - yield { - text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), - }; - } else if (textCursor > currFacet.index.byteStart) { - facetCursor++; - continue; - } - if (currFacet.index.byteStart < currFacet.index.byteEnd) { - const subtext = this.unicodeText.slice( - currFacet.index.byteStart, - currFacet.index.byteEnd, - ); - if (!subtext.trim()) { - // dont empty string entities - yield { text: subtext }; - } else { - yield { text: subtext, facet: currFacet.features }; - } - } - textCursor = currFacet.index.byteEnd; - facetCursor++; - } while (facetCursor < facets.length); - if (textCursor < this.unicodeText.length) { - yield { - text: this.unicodeText.slice(textCursor, this.unicodeText.length), - }; - } - } -} -function addFacet(facets: Facet[], newFacet: Facet, length: number) { - if (facets.length === 0) { - return [newFacet]; - } - - const allFacets = [...facets, newFacet]; - - // Collect all boundary positions - const boundaries = new Set(); - boundaries.add(0); - boundaries.add(length); - - for (const facet of allFacets) { - boundaries.add(facet.index.byteStart); - boundaries.add(facet.index.byteEnd); - } - - const sortedBoundaries = Array.from(boundaries).sort((a, b) => a - b); - const result: Facet[] = []; - - // Process segments between consecutive boundaries - for (let i = 0; i < sortedBoundaries.length - 1; i++) { - const start = sortedBoundaries[i]; - const end = sortedBoundaries[i + 1]; - - // Find facets that are active at the start position - const activeFacets = allFacets.filter( - (facet) => facet.index.byteStart <= start && facet.index.byteEnd > start, - ); - - // Only create facet if there are active facets (features present) - if (activeFacets.length > 0) { - const features = activeFacets.flatMap((f) => f.features); - result.push({ - index: { byteStart: start, byteEnd: end }, - features, - }); - } - } - - return result; +export function BaseTextBlock(props: Omit) { + return ( + + ); } diff --git a/app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx index 7721d9b0..009ac855 100644 --- a/app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx @@ -12,10 +12,14 @@ import { PubLeafletPagesLinearDocument, } from "lexicons/api"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; -import { BaseTextBlock } from "./BaseTextBlock"; +import { TextBlockCore, TextBlockCoreProps } from "./TextBlockCore"; import { StaticMathBlock } from "./StaticMathBlock"; import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; +function StaticBaseTextBlock(props: Omit) { + return ; +} + export function StaticPostContent({ blocks, did, @@ -47,7 +51,7 @@ let Block = async ({ case PubLeafletBlocksBlockquote.isMain(b.block): { return (
- - - + ); if (b.block.level === 2) return (

- +

); if (b.block.level === 3) return (

- +

); // if (b.block.level === 4) return

{b.block.plaintext}

; // if (b.block.level === 5) return
{b.block.plaintext}
; return (
- +
); } diff --git a/app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx b/app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx new file mode 100644 index 00000000..373d9604 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx @@ -0,0 +1,181 @@ +import { UnicodeString } from "@atproto/api"; +import { PubLeafletRichtextFacet } from "lexicons/api"; +import { AtMentionLink } from "components/AtMentionLink"; +import { ReactNode } from "react"; + +type Facet = PubLeafletRichtextFacet.Main; + +export type FacetRenderers = { + DidMention?: (props: { did: string; children: ReactNode }) => ReactNode; +}; + +export type TextBlockCoreProps = { + plaintext: string; + facets?: Facet[]; + index: number[]; + preview?: boolean; + renderers?: FacetRenderers; +}; + +export function TextBlockCore(props: TextBlockCoreProps) { + let children = []; + let richText = new RichText({ + text: props.plaintext, + facets: props.facets || [], + }); + let counter = 0; + for (const segment of richText.segments()) { + let id = segment.facet?.find(PubLeafletRichtextFacet.isId); + let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); + let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); + let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); + let isStrikethrough = segment.facet?.find( + PubLeafletRichtextFacet.isStrikethrough, + ); + let isDidMention = segment.facet?.find( + PubLeafletRichtextFacet.isDidMention, + ); + let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); + let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); + let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); + let isHighlighted = segment.facet?.find( + PubLeafletRichtextFacet.isHighlight, + ); + let className = ` + ${isCode ? "inline-code" : ""} + ${id ? "scroll-mt-12 scroll-mb-10" : ""} + ${isBold ? "font-bold" : ""} + ${isItalic ? "italic" : ""} + ${isUnderline ? "underline" : ""} + ${isStrikethrough ? "line-through decoration-tertiary" : ""} + ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); + + // Split text by newlines and insert
tags + const textParts = segment.text.split("\n"); + const renderedText = textParts.flatMap((part, i) => + i < textParts.length - 1 + ? [part,
] + : [part], + ); + + if (isCode) { + children.push( + + {renderedText} + , + ); + } else if (isDidMention) { + const DidMentionRenderer = props.renderers?.DidMention; + if (DidMentionRenderer) { + children.push( + + {renderedText} + , + ); + } else { + // Default: render as a simple link + children.push( + + {renderedText} + , + ); + } + } else if (isAtMention) { + children.push( + + {renderedText} + , + ); + } else if (link) { + children.push( + + {renderedText} + , + ); + } else { + children.push( + + {renderedText} + , + ); + } + + counter++; + } + return <>{children}; +} + +type RichTextSegment = { + text: string; + facet?: Exclude; +}; + +export class RichText { + unicodeText: UnicodeString; + facets?: Facet[]; + + constructor(props: { text: string; facets: Facet[] }) { + this.unicodeText = new UnicodeString(props.text); + this.facets = props.facets; + if (this.facets) { + this.facets = this.facets + .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) + .sort((a, b) => a.index.byteStart - b.index.byteStart); + } + } + + *segments(): Generator { + const facets = this.facets || []; + if (!facets.length) { + yield { text: this.unicodeText.utf16 }; + return; + } + + let textCursor = 0; + let facetCursor = 0; + do { + const currFacet = facets[facetCursor]; + if (textCursor < currFacet.index.byteStart) { + yield { + text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), + }; + } else if (textCursor > currFacet.index.byteStart) { + facetCursor++; + continue; + } + if (currFacet.index.byteStart < currFacet.index.byteEnd) { + const subtext = this.unicodeText.slice( + currFacet.index.byteStart, + currFacet.index.byteEnd, + ); + if (!subtext.trim()) { + // dont empty string entities + yield { text: subtext }; + } else { + yield { text: subtext, facet: currFacet.features }; + } + } + textCursor = currFacet.index.byteEnd; + facetCursor++; + } while (facetCursor < facets.length); + if (textCursor < this.unicodeText.length) { + yield { + text: this.unicodeText.slice(textCursor, this.unicodeText.length), + }; + } + } +}