From e9a96c969212d3496360a98f6259e825f13bd3a0 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Fri, 26 Apr 2024 00:18:42 -0400 Subject: [PATCH] feat: render optimized images and mdx parser fixes (#710) --- packages/ui/app/package.json | 1 + packages/ui/app/src/mdx/base-components.tsx | 37 ++++++++++--- packages/ui/app/src/mdx/mdx.ts | 5 ++ packages/ui/app/src/mdx/plugins/makeToc.ts | 6 +-- .../src/mdx/plugins/rehypeFernComponents.ts | 6 +-- .../app/src/mdx/plugins/rehypeSanitizeJSX.ts | 54 +++++++++++++++++-- packages/ui/app/src/mdx/plugins/utils.ts | 2 +- pnpm-lock.yaml | 3 ++ 8 files changed, 97 insertions(+), 17 deletions(-) diff --git a/packages/ui/app/package.json b/packages/ui/app/package.json index 17dad65e5d..3fbf36cc1e 100644 --- a/packages/ui/app/package.json +++ b/packages/ui/app/package.json @@ -117,6 +117,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@types/estree": "^1.0.5", + "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.4", "@types/jsonpath": "^0.2.4", "@types/lodash-es": "^4.17.12", diff --git a/packages/ui/app/src/mdx/base-components.tsx b/packages/ui/app/src/mdx/base-components.tsx index a15b397cd1..f4c765ad2b 100644 --- a/packages/ui/app/src/mdx/base-components.tsx +++ b/packages/ui/app/src/mdx/base-components.tsx @@ -1,5 +1,6 @@ -import { useMounted } from "@fern-ui/react-commons"; +import { DocsV1Read } from "@fern-api/fdr-sdk"; import cn from "clsx"; +import { toNumber } from "lodash-es"; import { AnchorHTMLAttributes, Children, @@ -9,11 +10,14 @@ import { FC, isValidElement, ReactElement, + useMemo, } from "react"; import Zoom from "react-medium-image-zoom"; import { AbsolutelyPositionedAnchor } from "../commons/AbsolutelyPositionedAnchor"; import { FernCard } from "../components/FernCard"; +import { FernImage } from "../components/FernImage"; import { FernLink } from "../components/FernLink"; +import { useDocsContext } from "../contexts/docs-context/useDocsContext"; import { useNavigationContext } from "../contexts/navigation-context"; import { onlyText } from "../util/onlyText"; import "./base-components.scss"; @@ -130,14 +134,33 @@ function isImgElement(element: ReactElement): element is ReactElement return element.type === Img; } -export const Img: FC = ({ className, src, alt, disableZoom, ...rest }) => { - const mounted = useMounted(); - if (!mounted || disableZoom) { - return {alt}; - } +export const Img: FC = ({ className, src, height, width, disableZoom, ...rest }) => { + const { files } = useDocsContext(); + // const mounted = useMounted(); + // if (!mounted || disableZoom) { + // return {alt}; + // } + const fernImageSrc = useMemo((): DocsV1Read.File_ | undefined => { + if (src == null) { + return undefined; + } + + if (src.startsWith("file:")) { + const fileId = src.slice(5); + return files[fileId]; + } + + return { type: "url", url: src }; + }, [files, src]); return ( - {alt} + {/* {alt} */} + ); }; diff --git a/packages/ui/app/src/mdx/mdx.ts b/packages/ui/app/src/mdx/mdx.ts index b589f2b322..bc97d8a294 100644 --- a/packages/ui/app/src/mdx/mdx.ts +++ b/packages/ui/app/src/mdx/mdx.ts @@ -1,3 +1,4 @@ +import { DocsV1Read } from "@fern-api/fdr-sdk"; import type { MDXRemoteSerializeResult } from "next-mdx-remote"; import { serialize } from "next-mdx-remote/serialize"; import rehypeKatex from "rehype-katex"; @@ -154,10 +155,12 @@ function withDefaultMdxOptions({ export async function maybeSerializeMdxContent( content: string, mdxOptions?: FernSerializeMdxOptions, + files?: Record, ): Promise; export async function maybeSerializeMdxContent( content: string | undefined, mdxOptions?: FernSerializeMdxOptions, + files?: Record, ): Promise; export async function maybeSerializeMdxContent( content: string | undefined, @@ -199,10 +202,12 @@ export async function maybeSerializeMdxContent( export async function serializeMdxWithFrontmatter( content: string, mdxOptions?: FernSerializeMdxOptions, + files?: Record, ): Promise; export async function serializeMdxWithFrontmatter( content: string | undefined, mdxOptions?: FernSerializeMdxOptions, + files?: Record, ): Promise; export async function serializeMdxWithFrontmatter( content: string | undefined, diff --git a/packages/ui/app/src/mdx/plugins/makeToc.ts b/packages/ui/app/src/mdx/plugins/makeToc.ts index b277acd41f..935bf3774c 100644 --- a/packages/ui/app/src/mdx/plugins/makeToc.ts +++ b/packages/ui/app/src/mdx/plugins/makeToc.ts @@ -22,7 +22,7 @@ export function makeToc(tree: Root): ElementContent { if ( getBooleanValue( node.attributes.find((attr) => isMdxJsxAttribute(attr) && attr.name === "toc")?.value, - ) === false + ) !== true ) { return SKIP; } @@ -64,7 +64,7 @@ export function makeToc(tree: Root): ElementContent { const itemsAttr = attributes.find((attr) => attr.name === "tabs"); const tocAttr = attributes.find((attr) => attr.name === "toc"); const parentSkipToc = - tocAttr != null && typeof tocAttr.value === "object" && tocAttr.value?.value === "false"; + tocAttr == null || (typeof tocAttr.value === "object" && tocAttr.value?.value !== "true"); if (itemsAttr?.value == null || typeof itemsAttr.value === "string") { return; } @@ -91,7 +91,7 @@ export function makeToc(tree: Root): ElementContent { const itemsAttr = attributes.find((attr) => attr.name === "items"); const tocAttr = attributes.find((attr) => attr.name === "toc"); const parentSkipToc = - tocAttr != null && typeof tocAttr.value === "object" && tocAttr.value?.value === "false"; + tocAttr == null || (typeof tocAttr.value === "object" && tocAttr.value?.value !== "true"); if (itemsAttr?.value == null || typeof itemsAttr.value === "string") { return; diff --git a/packages/ui/app/src/mdx/plugins/rehypeFernComponents.ts b/packages/ui/app/src/mdx/plugins/rehypeFernComponents.ts index 2d4c0c338c..d9f11a4af6 100644 --- a/packages/ui/app/src/mdx/plugins/rehypeFernComponents.ts +++ b/packages/ui/app/src/mdx/plugins/rehypeFernComponents.ts @@ -12,15 +12,15 @@ export function rehypeFernComponents(): (tree: Root) => void { } if (isMdxJsxFlowElement(node) && node.name != null) { - if (node.name === "Tabs" && node.children.length > 0) { + if (node.name === "Tabs") { transformTabs(node, index, parent); } - if (node.name === "TabGroup" && node.children.length > 0) { + if (node.name === "TabGroup") { transformTabs(node, index, parent); } - if (node.name === "AccordionGroup" && node.children.length > 0) { + if (node.name === "AccordionGroup") { transformAccordionGroup(node, index, parent); } } diff --git a/packages/ui/app/src/mdx/plugins/rehypeSanitizeJSX.ts b/packages/ui/app/src/mdx/plugins/rehypeSanitizeJSX.ts index 8ab0b44fa0..3b8b18f090 100644 --- a/packages/ui/app/src/mdx/plugins/rehypeSanitizeJSX.ts +++ b/packages/ui/app/src/mdx/plugins/rehypeSanitizeJSX.ts @@ -1,6 +1,7 @@ -import type { Expression } from "estree"; +import type { JSXFragment } from "estree-jsx"; import { SKIP as ESTREE_SKIP, visit as visitEstree } from "estree-util-visit"; import type { ElementContent, Root } from "hast"; +import { MdxJsxAttribute, MdxJsxExpressionAttribute } from "mdast-util-mdx-jsx"; import { SKIP, visit } from "unist-util-visit"; import { parseStringStyle } from "../../util/parseStringStyle"; import { INTRINSIC_JSX_TAGS } from "../common/intrinsict-elements"; @@ -31,7 +32,7 @@ export function rehypeSanitizeJSX({ showErrors = false }: { showErrors?: boolean attr.value?.type === "mdxJsxAttributeValueExpression" && attr.value.data?.estree != null ) { - visitEstree(attr.value.data.estree, (esnode, _key, _i, ancestors) => { + visitEstree(attr.value.data.estree, (esnode, _key, i, ancestors) => { if (ancestors.length === 0) { return undefined; } @@ -45,6 +46,9 @@ export function rehypeSanitizeJSX({ showErrors = false }: { showErrors?: boolean ancestor.expression = jsxFragment(); return ESTREE_SKIP; } + if (ancestor.type === "JSXFragment" && i != null) { + ancestor.children[i] = jsxFragment(); + } } return undefined; }); @@ -52,6 +56,13 @@ export function rehypeSanitizeJSX({ showErrors = false }: { showErrors?: boolean return attr; }); + + // const temporaryRoot: Root = { + // type: "root", + // children: node.children, + // }; + // rehypeSanitizeJSX({ showErrors })(temporaryRoot); + // node.children = temporaryRoot.children as ElementContent[]; } } return; @@ -92,9 +103,46 @@ export function rehypeSanitizeJSX({ showErrors = false }: { showErrors?: boolean }); } }); + + // convert img to img element + visit(tree, (node, index, parent) => { + if (index == null) { + return; + } + if (isMdxJsxFlowElement(node)) { + if (node.name === "img") { + const properties = toProperties(node.attributes); + if (properties != null) { + parent?.children.splice(index, 1, { + type: "element", + tagName: "img", + properties, + children: node.children, + }); + } + } + } + }); }; } +function toProperties(attributes: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]): Record | undefined { + const properties: Record = {}; + for (const attr of attributes) { + if (attr.type === "mdxJsxAttribute" && attr.value != null) { + if (typeof attr.value === "string") { + properties[attr.name] = attr.value; + } else { + // todo: handle literal expressions + return undefined; + } + } else if (attr.type === "mdxJsxExpressionAttribute") { + return undefined; + } + } + return properties; +} + function mdxErrorBoundary(nodeName: string): ElementContent { return { type: "mdxJsxFlowElement", @@ -104,7 +152,7 @@ function mdxErrorBoundary(nodeName: string): ElementContent { }; } -function jsxFragment(): Expression { +function jsxFragment(): JSXFragment { return { type: "JSXFragment", openingFragment: { diff --git a/packages/ui/app/src/mdx/plugins/utils.ts b/packages/ui/app/src/mdx/plugins/utils.ts index df4b87051d..e43f1b2e65 100644 --- a/packages/ui/app/src/mdx/plugins/utils.ts +++ b/packages/ui/app/src/mdx/plugins/utils.ts @@ -9,7 +9,7 @@ import { unknownToString } from "../../util/unknownToString"; import { valueToEstree } from "./to-estree"; export function isMdxJsxFlowElement(node: Node): node is MdxJsxFlowElementHast { - return node.type === "mdxJsxFlowElement"; + return node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement"; } export function isMdxJsxAttribute( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1162a8de6d..6fc8002964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -966,6 +966,9 @@ importers: '@types/estree': specifier: ^1.0.5 version: 1.0.5 + '@types/estree-jsx': + specifier: ^1.0.5 + version: 1.0.5 '@types/hast': specifier: ^3.0.4 version: 3.0.4