Skip to content

Commit

Permalink
syntax-highlighter (#416)
Browse files Browse the repository at this point in the history
* feat: react-ui Code Header Syntax Highlighter support

* move implementation to react-markdown
  • Loading branch information
Yonom authored Jul 7, 2024
1 parent 6f776ee commit ef25706
Show file tree
Hide file tree
Showing 28 changed files with 432 additions and 134 deletions.
7 changes: 7 additions & 0 deletions .changeset/gentle-suns-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@assistant-ui/react-markdown": patch
"@assistant-ui/react-ui": patch
"@assistant-ui/react": patch
---

feat: Code Header and Syntax Highlighter support
20 changes: 18 additions & 2 deletions apps/www/components/shadcn/Shadcn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,29 @@ import icon from "@/public/favicon/favicon.svg";
import type { TooltipContentProps } from "@radix-ui/react-tooltip";
import Image from "next/image";
import { type FC } from "react";
import { makeMarkdownText, Thread } from "@assistant-ui/react-ui";
import {
makePrismSyntaxHighlighter,
makeMarkdownText,
Thread,
} from "@assistant-ui/react-ui";
import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { ModelPicker } from "./ModelPicker";
import { useSwitchToNewThread } from "@assistant-ui/react";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";

const MarkdownText = makeMarkdownText({ remarkPlugins: [remarkGfm] });
const MarkdownText = makeMarkdownText({
remarkPlugins: [remarkGfm],
components: {
SyntaxHighlighter: makePrismSyntaxHighlighter({
style: coldarkDark,
customStyle: {
margin: 0,
backgroundColor: "black",
},
}),
},
});

type ButtonWithTooltipProps = ButtonProps & {
tooltip: string;
Expand Down
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"openai": "^4.52.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
Expand Down
14 changes: 11 additions & 3 deletions apps/www/pages/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,18 @@
} */
}

pre {
:not(.aui-md-root) > pre {
background-color: hsl(var(--background)) !important;
--tw-ring-color: hsl(var(--border)) !important
;
--tw-ring-color: hsl(var(--border)) !important;
}

pre code:not([class*="twoslash-"]) {
display: inherit;
}

pre code:not([class*="twoslash-"]) > span {
padding-left: unset;
padding-right: unset;
}

.nextra-nav-container-blur {
Expand Down
64 changes: 34 additions & 30 deletions apps/www/theme.config.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import icon from "@/public/favicon/favicon.svg";
import Image from "next/image";
import { type DocsThemeConfig, useConfig } from "nextra-theme-docs";
import React from "react";
import React, { FC, PropsWithChildren } from "react";

const Head: FC = () => {
const { frontMatter, title } = useConfig();
const description =
frontMatter["description"] ?? "React Components for AI Chat";
const hasTitle = title !== "Index";
return (
<>
<title>{hasTitle ? `${title} - assistant-ui` : "assistant-ui"}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
</>
);
};

const Main: FC<PropsWithChildren> = ({ children }) => {
const { frontMatter, normalizePagesResult } = useConfig();
return (
<>
<p className="mb-2 mt-4 text-sm font-bold text-[hsl(var(--nextra-primary-hue)_var(--nextra-primary-saturation)_45%)]">
{normalizePagesResult.activePath.at(-2)?.title}
</p>
<h1 className="text-foreground mb-2 inline-block text-2xl font-extrabold tracking-tight sm:text-3xl">
{frontMatter["title"]}
</h1>
<p>{frontMatter["description"]}</p>
{children}
</>
);
};

const config: DocsThemeConfig = {
color: {
Expand All @@ -25,35 +56,8 @@ const config: DocsThemeConfig = {
feedback: { content: null },
editLink: { component: null },
toc: { title: null, backToTop: false },
main: ({ children }) => {
const { frontMatter, normalizePagesResult } = useConfig();
return (
<>
<p className="mb-2 mt-4 text-sm font-bold text-[hsl(var(--nextra-primary-hue)_var(--nextra-primary-saturation)_45%)]">
{normalizePagesResult.activePath.at(-2)?.title}
</p>
<h1 className="text-foreground mb-2 inline-block text-2xl font-extrabold tracking-tight sm:text-3xl">
{frontMatter["title"]}
</h1>
<p>{frontMatter["description"]}</p>
{children}
</>
);
},
head: () => {
const { frontMatter, title } = useConfig();
const description =
frontMatter["description"] ?? "React Components for AI Chat";
const hasTitle = title !== "Index";
return (
<>
<title>{hasTitle ? `${title} - assistant-ui` : "assistant-ui"}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
</>
);
},
main: Main,
head: Head,
};

export default config;
2 changes: 2 additions & 0 deletions packages/react-markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"build": "tsup src/index.ts --format cjs,esm --dts --sourcemap"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-use-callback-ref": "^1.1.0",
"react-markdown": "^9.0.1",
"zod": "^3.23.8"
},
Expand Down
73 changes: 73 additions & 0 deletions packages/react-markdown/src/CodeOverride.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
ComponentPropsWithoutRef,
ComponentType,
FC,
useContext,
useMemo,
} from "react";
import { Slot } from "@radix-ui/react-slot";
import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
import { PreContext } from "./PreOverride";
import {
CodeComponent,
CodeHeaderProps,
PreComponent,
SyntaxHighlighterProps,
} from "./types";
import { DefaultSyntaxHighlighter } from "./defaultComponents";

type CodeOverrideProps = ComponentPropsWithoutRef<CodeComponent> & {
components: {
Pre: PreComponent;
Code: CodeComponent;
CodeHeader: ComponentType<CodeHeaderProps>;
SyntaxHighlighter: ComponentType<SyntaxHighlighterProps>;
};
};

const CodeBlockOverride: FC<CodeOverrideProps> = ({
components: { Pre, Code, SyntaxHighlighter, CodeHeader },
children,
...codeProps
}) => {
const preProps = useContext(PreContext);
const WrappedPre: PreComponent = useCallbackRef((props) => (
<Slot {...(preProps as any)}>
<Pre {...props} />
</Slot>
));
const WrappedCode: CodeComponent = useCallbackRef((props) => (
<Slot {...(codeProps as any)}>
<Code {...props} />
</Slot>
));

const components = useMemo(
() => ({ Pre: WrappedPre, Code: WrappedCode }),
[WrappedPre, WrappedCode],
);

const language = /language-(\w+)/.exec(codeProps.className || "")?.[1];
const code = children as string;
const SH = language ? SyntaxHighlighter : DefaultSyntaxHighlighter;

return (
<>
<CodeHeader language={language} code={code} />
<SH
components={components}
language={language ?? "unknown"}
code={code}
/>
</>
);
};

export const CodeOverride: FC<CodeOverrideProps> = ({
components,
...props
}) => {
const preProps = useContext(PreContext);
if (!preProps) return <components.Code {...(props as any)} />;
return <CodeBlockOverride components={components} {...props} />;
};
57 changes: 49 additions & 8 deletions packages/react-markdown/src/MarkdownText.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
import { INTERNAL, useContentPartText } from "@assistant-ui/react";
import type { FC } from "react";
import type { ComponentType, FC } from "react";
import ReactMarkdown, { type Options } from "react-markdown";
import { SyntaxHighlighterProps, CodeHeaderProps } from "./types";
import { PreOverride } from "./PreOverride";
import {
DefaultPre,
DefaultCode,
DefaultSyntaxHighlighter,
DefaultCodeHeader,
} from "./defaultComponents";
import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
import { CodeOverride } from "./CodeOverride";

const { useSmooth } = INTERNAL;

export type MarkdownTextPrimitiveProps = Omit<Options, "children"> & {
export type MarkdownTextPrimitiveProps = Omit<
Options,
"components" | "children"
> & {
components?: NonNullable<Options["components"]> & {
SyntaxHighlighter?: ComponentType<SyntaxHighlighterProps>;
CodeHeader?: ComponentType<CodeHeaderProps>;
};
smooth?: boolean;
};

export const MarkdownTextPrimitive: FC<MarkdownTextPrimitiveProps> = (
options,
) => {
export const MarkdownTextPrimitive: FC<MarkdownTextPrimitiveProps> = ({
smooth = true,
components: userComponents,
...rest
}) => {
const {
part: { text },
} = useContentPartText();
const smoothText = useSmooth(text, options.smooth);
return <ReactMarkdown {...options}>{smoothText}</ReactMarkdown>;
const smoothText = useSmooth(text, smooth);

const {
pre = DefaultPre,
code = DefaultCode,
SyntaxHighlighter = DefaultSyntaxHighlighter,
CodeHeader = DefaultCodeHeader,
...componentsRest
} = userComponents ?? {};
const components: typeof userComponents = {
...componentsRest,
pre: PreOverride,
code: useCallbackRef((props) => (
<CodeOverride
components={{ Pre: pre, Code: code, SyntaxHighlighter, CodeHeader }}
{...props}
/>
)),
};

return (
<ReactMarkdown components={components} {...rest}>
{smoothText}
</ReactMarkdown>
);
};
11 changes: 11 additions & 0 deletions packages/react-markdown/src/PreOverride.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext, ComponentPropsWithoutRef } from "react";
import { PreComponent } from "./types";

export const PreContext = createContext<Omit<
ComponentPropsWithoutRef<PreComponent>,
"children"
> | null>(null);

export const PreOverride: PreComponent = ({ children, ...rest }) => {
return <PreContext.Provider value={rest}>{children}</PreContext.Provider>;
};
22 changes: 22 additions & 0 deletions packages/react-markdown/src/defaultComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ComponentType } from "react";
import {
PreComponent,
CodeComponent,
SyntaxHighlighterProps,
CodeHeaderProps,
} from "./types";

export const DefaultPre: PreComponent = ({ node, ...rest }) => (
<pre {...rest} />
);
export const DefaultCode: CodeComponent = ({ node, ...rest }) => (
<code {...rest} />
);
export const DefaultSyntaxHighlighter: ComponentType<
SyntaxHighlighterProps
> = ({ components: { Pre, Code }, code }) => (
<Pre>
<Code>{code}</Code>
</Pre>
);
export const DefaultCodeHeader: ComponentType<CodeHeaderProps> = () => null;
2 changes: 2 additions & 0 deletions packages/react-markdown/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export {
MarkdownTextPrimitive,
type MarkdownTextPrimitiveProps,
} from "./MarkdownText";

export type { CodeHeaderProps, SyntaxHighlighterProps } from "./types";
22 changes: 22 additions & 0 deletions packages/react-markdown/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Options } from "react-markdown";

export type PreComponent = NonNullable<
NonNullable<Options["components"]>["pre"]
>;
export type CodeComponent = NonNullable<
NonNullable<Options["components"]>["code"]
>;

export type CodeHeaderProps = {
language: string | undefined;
code: string;
};

export type SyntaxHighlighterProps = {
components: {
Pre: PreComponent;
Code: CodeComponent;
};
language: string;
code: string;
};
5 changes: 4 additions & 1 deletion packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@
"@radix-ui/react-primitive": "^2.0.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-use-callback-ref": "^1.1.0",
"class-variance-authority": "^0.7.0",
"lucide-react": "^0.400.0",
"postcss": "^8.4.39"
"postcss": "^8.4.39",
"react-syntax-highlighter": "^15.5.0"
},
"peerDependencies": {
"@assistant-ui/react": "^0.3.3",
Expand All @@ -74,6 +76,7 @@
"devDependencies": {
"@assistant-ui/tsconfig": "workspace:*",
"@types/node": "^20.14.9",
"@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.19",
"esbuild": "^0.23.0",
"eslint-config-next": "14.2.4",
Expand Down
Loading

0 comments on commit ef25706

Please sign in to comment.