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),
+ };
+ }
+ }
+}