diff --git a/package.json b/package.json index f3e53cd..406de28 100644 --- a/package.json +++ b/package.json @@ -26,26 +26,20 @@ "@types/luxon": "^3.7.1", "@types/mdast": "^3.0.10", "@types/react": "^18", - "@types/react-dom": "^18", "hast-util-sanitize": "^5.0.2", "html-to-text": "^9.0.5", "lodash": "^4.17.21", "rollup": "^4.52.4", "tslib": "^2.8.1", "typescript": "^5.9.3", - "react": "^18", - "react-dom": "^18" + "react": "^18" }, "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" + "react": ">=16" }, "peerDependenciesMeta": { "react": { "optional": true - }, - "react-dom": { - "optional": true } }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a9b6a3..9d213de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,15 +102,9 @@ importers: '@types/react': specifier: ^18 version: 18.3.26 - '@types/react-dom': - specifier: ^18 - version: 18.3.7(@types/react@18.3.26) react: specifier: ^18 version: 18.3.1 - react-dom: - specifier: ^18 - version: 18.3.1(react@18.3.1) rollup: specifier: ^4.52.4 version: 4.52.4 @@ -298,11 +292,6 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} - peerDependencies: - '@types/react': ^18.0.0 - '@types/react@18.3.26': resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} @@ -733,11 +722,6 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -803,9 +787,6 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -1048,10 +1029,6 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@18.3.7(@types/react@18.3.26)': - dependencies: - '@types/react': 18.3.26 - '@types/react@18.3.26': dependencies: '@types/prop-types': 15.7.15 @@ -1792,12 +1769,6 @@ snapshots: property-information@6.5.0: {} - react-dom@18.3.1(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -1953,10 +1924,6 @@ snapshots: dependencies: mri: 1.2.0 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - selderee@0.11.0: dependencies: parseley: 0.12.1 diff --git a/readme.md b/readme.md index 2ac846f..eb9cdad 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ bare-minimum version that builds. no package yet, no instructions yet, this is just the bare minimum extraction to make this build outside the cohost codebase. i strongly recommend against using this for anything serious. -things that are missing that will not be added: +things that are missing that will probably not be added: - `InfoBox` is a bare-minimum implementation that doesn't use any of our styling because i didn't want to make tailwind a dependency here @@ -26,7 +26,7 @@ things that are here that will probably be removed at some point: - extraneous IDs, types, etc. this was pulled straight out of the cohost codebase and while i removed the obviously unnecessary bits (things related to invites, for example) there might be some bonus bullshit in here. -- code related to AST caching. it's entirely unnecessary for non-production use. +- ~~code related to AST caching. it's entirely unnecessary for non-production use.~~ it's gone now # license diff --git a/src/lib/post-rendering.tsx b/src/lib/post-rendering.tsx index abd561e..786ca68 100644 --- a/src/lib/post-rendering.tsx +++ b/src/lib/post-rendering.tsx @@ -7,11 +7,10 @@ import { summaryContent, ViewBlock, } from "../types/post-blocks"; -import { PostASTMap, WirePostViewModel } from "../types/wire-models"; +import { WirePostViewModel } from "../types/wire-models"; import { compile } from "html-to-text"; import { DateTime } from "luxon"; import React, { createElement, Fragment, JSX } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; import rehypeExternalLinks from "rehype-external-links"; import rehypeRaw from "rehype-raw"; import rehypeReact from "rehype-react"; @@ -28,10 +27,8 @@ import { chooseAgeRuleset } from "./sanitize"; import { MAX_GFM_LINES, RenderingOptions } from "./shared-types"; import { cleanUpFootnotes, - compileHastAST, convertMentions, copyImgAltToTitle, - parseHastAST, } from "./unified-processors"; import _ from "lodash"; import type { Root as HASTRoot } from "hast"; @@ -75,16 +72,9 @@ const markdownRenderStack = ( } return stack - .use( - remarkRehype as Plugin< - [{ allowDangerousHtml: boolean }], - MDASTRoot, - HASTRoot - >, - { - allowDangerousHtml: true, - } - ) + .use(remarkRehype, { + allowDangerousHtml: true, + }) .use(copyImgAltToTitle) .use(() => cleanUpFootnotes) .use(rehypeRaw) @@ -101,13 +91,12 @@ const ERROR_BOX_NODE = (

); -const ERROR_BOX_HTML = renderToStaticMarkup(ERROR_BOX_NODE); -async function renderMarkdownAst( +async function renderMarkdownToReact( blocks: MarkdownViewBlock[], publishDate: Date, - options: Pick -): Promise { + options: RenderingOptions +): Promise { const src = blocks.map((block) => block.markdown.content).join("\n\n"); let lineLength = 0; @@ -128,61 +117,35 @@ async function renderMarkdownAst( ); } - return markdownRenderStack(publishDate, lineLength, options) - .use(() => convertMentions) - .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) - .use(compileHastAST) - .process(src) - .then((result) => result.value.toString()) - .catch((e) => { - // re-run the renderer with our static error box. we only get errors - // when a user has an invalid style tag that fails parsing. our error - // box is Known Good so this is not a concern for us. - return renderMarkdownAst( - [ - { - type: "markdown", - markdown: { - content: ERROR_BOX_HTML, - }, - }, - ], - publishDate, - options - ); - }); -} - -export function renderReactFromAst( - astString: string, - options: Omit -) { - let stack = unified().use(parseHastAST); - const externalRel = ["nofollow"]; if (options.externalLinksInNewTab) { externalRel.push("noopener"); } try { - return ( - stack - .use(rehypeExternalLinks, { - rel: externalRel, - target: options.externalLinksInNewTab ? "_blank" : "_self", - }) - // @ts-expect-error rehype-react types are broken - .use(rehypeReact, { - createElement, - Fragment, - components: { - Mention, - CustomEmoji, - }, - }) - .processSync(astString).result - ); + const result = await markdownRenderStack(publishDate, lineLength, options) + .use(() => convertMentions) + .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) + .use(rehypeExternalLinks, { + rel: externalRel, + target: options.externalLinksInNewTab ? "_blank" : "_self", + }) + // @ts-expect-error rehype-react types are broken + .use(rehypeReact, { + createElement, + Fragment, + components: { + Mention, + CustomEmoji, + }, + }) + .process(src); + + return result.result; } catch (e) { + // re-run the renderer with our static error box. we only get errors + // when a user has an invalid style tag that fails parsing. our error + // box is Known Good so this is not a concern for us. return ERROR_BOX_NODE; } } @@ -275,11 +238,20 @@ export function renderPostSummary( return summary; } -export async function generatePostAst( +export type PostRenderResult = { + spans: Array<{ + startIndex: number; + endIndex: number; + rendered: JSX.Element; + }>; + readMoreIndex: number | null; +}; + +export async function renderPostBlocks( viewBlocks: ViewBlock[], publishDate: Date, - options: Pick -): Promise { + options: RenderingOptions +): Promise { // identify markdown spans const spans: { startIndex: number; @@ -290,9 +262,8 @@ export async function generatePostAst( for (let i = 0; i < viewBlocks.length; i++) { const block = viewBlocks[i]; - const isMarkdownBlock = isMarkdownViewBlock(block); - if (isMarkdownBlock) { + if (isMarkdownViewBlock(block)) { if ( currentSpanStartIndex !== null && block.markdown.content === "---" && @@ -336,17 +307,16 @@ export async function generatePostAst( }); } - // render each span and return AST map + // render each span and return rendered React elements return { spans: await Promise.all( spans.map(async (span) => ({ startIndex: span.startIndex, endIndex: span.endIndex, - ast: await renderMarkdownAst( - viewBlocks.slice( - span.startIndex, - span.endIndex - ) as MarkdownViewBlock[], + rendered: await renderMarkdownToReact( + viewBlocks + .slice(span.startIndex, span.endIndex) + .filter(isMarkdownViewBlock), publishDate, options ), @@ -358,10 +328,7 @@ export async function generatePostAst( // interim rendering method for until we get the rest of the inline attachments // changes done. render a sequence of markdown spans all joined together. -export function renderReactFromSpans( - spans: PostASTMap["spans"], - options: Omit -) { +export function renderReactFromSpans(spans: PostRenderResult["spans"]) { const rendered: JSX.Element[] = []; for (let i = 0; i < spans.length; i++) { @@ -375,7 +342,7 @@ export function renderReactFromSpans( rendered.push( - {renderReactFromAst(spans[i].ast, options)} + {spans[i].rendered} ); } diff --git a/src/lib/unified-processors.ts b/src/lib/unified-processors.ts index 56af594..e560fb8 100644 --- a/src/lib/unified-processors.ts +++ b/src/lib/unified-processors.ts @@ -1,7 +1,6 @@ import type { Element, Root, Text } from "hast"; import _ from "lodash"; import { Plugin } from "unified"; -import type { Compiler, Parser } from "unified"; import { is } from "unist-util-is"; import { CONTINUE, SKIP, visit } from "unist-util-visit"; import { EXIT, visitParents } from "unist-util-visit-parents"; @@ -159,20 +158,3 @@ export const copyImgAltToTitle: Plugin<[], Root> = () => (hast: Root) => { } }); }; - -export const compileHastAST: Plugin = function () { - const compiler: Compiler = (node: Root) => { - return JSON.stringify(node); - }; - - Object.assign(this, { Compiler: compiler }); -}; - -export const parseHastAST: Plugin = function () { - const parser: Parser = (astString: string) => { - const ast = JSON.parse(astString) as Root; - return ast; - }; - - Object.assign(this, { Parser: parser }); -}; diff --git a/src/types/wire-models.ts b/src/types/wire-models.ts index 857c4a2..c90ec34 100644 --- a/src/types/wire-models.ts +++ b/src/types/wire-models.ts @@ -103,23 +103,6 @@ export const LimitedVisibilityReason = z.enum([ ]); export type LimitedVisibilityReason = z.infer; -// rationale for building the post AST this way: originally, the entire post -// shared one AST; this doesn't suffice when attachments can move around. the -// natural way to break the AST down then is per-block, but an inline HTML tag -// can span multiple blocks, so we need to build spans of contiguous markdown -// blocks as a unit or else HTML's tag auto-insertion rules kick in. -export const PostASTMap = z.object({ - spans: z.array( - z.object({ - startIndex: z.number(), - endIndex: z.number(), - ast: z.string(), - }) - ), - readMoreIndex: z.number().nullable(), -}); -export type PostASTMap = z.infer; - // double declaration required due to a typescript limitation with recursive types // see: https://github.com/colinhacks/zod#recursive-types type WirePostViewModelInternal = WirePostContentCommon & { @@ -141,7 +124,6 @@ type WirePostViewModelInternal = WirePostContentCommon & { canShare: boolean; canPublish: boolean; limitedVisibilityReason: LimitedVisibilityReason; - astMap: PostASTMap; responseToAskId: AskId | null; }; @@ -165,7 +147,6 @@ export const WirePostViewModel: z.ZodType = z.lazy( canShare: z.boolean(), canPublish: z.boolean(), limitedVisibilityReason: LimitedVisibilityReason, - astMap: PostASTMap, responseToAskId: AskId.nullable(), }) );