-
Notifications
You must be signed in to change notification settings - Fork 254
feat: add markdown preview with toggle to view raw source #863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
39ce586
eb17a4d
46b0552
4be8ab0
9447196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState } from "react"; | ||
| import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; | ||
| import { PureMarkDownPreviewPanel } from "./pureMarkDownPreviewPanel"; | ||
| import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; | ||
|
|
||
| interface SourcePreviewPanelClientProps { | ||
| path: string; | ||
| repoName: string; | ||
| revisionName: string; | ||
| source: string; | ||
| language: string; | ||
| } | ||
|
|
||
| export const SourcePreviewPanelClient = ({ | ||
| source, | ||
| language, | ||
| path, | ||
| repoName, | ||
| revisionName, | ||
| }: SourcePreviewPanelClientProps) => { | ||
| const [viewMode, setViewMode] = useState<string>("preview"); | ||
| const isMarkdown = language === 'Markdown'; | ||
|
|
||
| return ( | ||
| <> | ||
| {isMarkdown && ( | ||
| <> | ||
| <div className="p-2 border-b flex"> | ||
| <ToggleGroup | ||
| type="single" | ||
| defaultValue="preview" | ||
| value={viewMode} | ||
| onValueChange={(value) => value && setViewMode(value)} | ||
| > | ||
| <ToggleGroupItem | ||
| value="preview" | ||
| aria-label="Preview" | ||
| className="w-fit px-4" | ||
| > | ||
| Preview | ||
| </ToggleGroupItem> | ||
| <ToggleGroupItem | ||
| value="code" | ||
| aria-label="Code" | ||
| className="w-fit px-4" | ||
| > | ||
| Code | ||
| </ToggleGroupItem> | ||
| </ToggleGroup> | ||
| </div> | ||
| </> | ||
| )} | ||
| {isMarkdown && viewMode === "preview" ? ( | ||
| <PureMarkDownPreviewPanel source={source} repoName={repoName} revisionName={revisionName} /> | ||
| ) : ( | ||
| <PureCodePreviewPanel | ||
| source={source} | ||
| language={language} | ||
| repoName={repoName} | ||
| path={path} | ||
| revisionName={revisionName} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| 'use client'; | ||
|
|
||
| import { ScrollArea } from "@/components/ui/scroll-area"; | ||
| import ReactMarkdown from "react-markdown"; | ||
| import remarkGfm from "remark-gfm"; | ||
| import rehypeHighlight from "rehype-highlight"; | ||
| import "github-markdown-css/github-markdown.css"; | ||
| import "highlight.js/styles/github-dark.css"; | ||
| import rehypeRaw from "rehype-raw"; | ||
|
|
||
| interface PureMarkDownPreviewPanelProps { | ||
| source: string; | ||
| repoName: string; | ||
| revisionName: string; | ||
| } | ||
|
|
||
| export const PureMarkDownPreviewPanel = ({ | ||
| source, | ||
| repoName, | ||
| revisionName, | ||
| }: PureMarkDownPreviewPanelProps) => { | ||
| const IMAGE_BASE_URL = "https://raw.githubusercontent.com/"+repoName.split("/").slice(1).join("/")+"/"+revisionName+"/"; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return ( | ||
| <ScrollArea className="h-full overflow-auto flex-1"> | ||
| <div className="w-full flex justify-center bg-white dark:bg-background"> | ||
| <article className="markdown-body dark dark:bg-background w-full max-w-4xl px-6 py-10"> | ||
|
||
| <ReactMarkdown | ||
| remarkPlugins={[remarkGfm]} | ||
| rehypePlugins={[rehypeRaw, rehypeHighlight]} | ||
|
Comment on lines
+311
to
+313
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security:
🔒 Proposed fix: add rehype-sanitize import rehypeHighlight from "rehype-highlight";
import "github-markdown-css/github-markdown.css";
import "highlight.js/styles/github-dark.css";
import rehypeRaw from "rehype-raw";
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";Then update the plugins array — - rehypePlugins={[rehypeRaw, rehypeHighlight]}
+ rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}You may need to extend 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| components={{ | ||
| pre: ({ children }) => ( | ||
| <pre className="rounded-md overflow-x-auto"> | ||
| {children} | ||
| </pre> | ||
| ), | ||
|
|
||
| source: ({ srcSet = "", ...props }) => { | ||
| if (typeof srcSet !== "string") return null; | ||
|
|
||
| let resolvedSrcset = srcSet; | ||
|
|
||
| if ( | ||
| srcSet.startsWith(".github/") || | ||
| !srcSet.startsWith("http") | ||
| ) { | ||
| resolvedSrcset = | ||
| IMAGE_BASE_URL + | ||
| srcSet.replace(/^\.\//, ""); | ||
| } | ||
|
|
||
| return ( | ||
| <source | ||
| srcSet={resolvedSrcset} | ||
| {...props} | ||
| /> | ||
| ); | ||
| }, | ||
|
|
||
| img: ({ src = "", alt, ...props }) => { | ||
| if (typeof src !== "string") return null; | ||
|
|
||
| let resolvedSrc = src; | ||
|
|
||
| if ( | ||
| src.startsWith(".github/") || | ||
| (!src.startsWith("http://") && | ||
| !src.startsWith("https://")) | ||
| ) { | ||
| resolvedSrc = | ||
| IMAGE_BASE_URL + | ||
| src.replace(/^\.\//, ""); | ||
| } | ||
|
|
||
| return ( | ||
| // eslint-disable-next-line @next/next/no-img-element | ||
| <img | ||
| src={resolvedSrc} | ||
| alt={alt || ""} | ||
| className="max-w-full h-auto" | ||
| loading="lazy" | ||
| {...props} | ||
| /> | ||
| ); | ||
| }, | ||
|
|
||
| video: ({ src = "", ...props }) => { | ||
| return ( | ||
| <video | ||
| src={src} | ||
| controls | ||
| preload="metadata" | ||
| className="max-w-full h-auto my-4" | ||
| {...props} | ||
| > | ||
| Your browser does not support the video | ||
| tag. | ||
| </video> | ||
| ); | ||
| }, | ||
|
||
|
|
||
| code({ className, children, ...props }) { | ||
| const isBlock = | ||
| className?.startsWith("language-"); | ||
|
|
||
| if (!isBlock) { | ||
| return ( | ||
| <code | ||
| className="px-1 py-0.5 rounded" | ||
| {...props} | ||
| > | ||
| {children} | ||
| </code> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <code className={className} {...props}> | ||
| {children} | ||
| </code> | ||
| ); | ||
| }, | ||
|
|
||
| table: ({ children }) => ( | ||
| <div className="overflow-x-auto"> | ||
| <table>{children}</table> | ||
| </div> | ||
| ), | ||
|
|
||
| a: ({ children, href, ...props }) => { | ||
| // Check if link is a video URL | ||
| if ( | ||
| href && | ||
| href.match( | ||
| /^https:\/\/github\.com\/user-attachments\/assets\/.+$/, | ||
| ) | ||
| ) { | ||
| return ( | ||
| <video | ||
| src={href} | ||
| controls | ||
| preload="metadata" | ||
| className="max-w-full h-auto my-4" | ||
| > | ||
| Your browser does not support the | ||
| video tag. | ||
| </video> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <a | ||
| href={href} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="text-blue-600 hover:underline" | ||
| {...props} | ||
| > | ||
| {children} | ||
| </a> | ||
| ); | ||
| }, | ||
| }} | ||
| > | ||
| {source} | ||
| </ReactMarkdown> | ||
| </article> | ||
| </div> | ||
| </ScrollArea> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| "use client" | ||
|
|
||
| import * as React from "react" | ||
| import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" | ||
| import { type VariantProps } from "class-variance-authority" | ||
|
|
||
| import { cn } from "@/lib/utils" | ||
| import { toggleVariants } from "@/components/ui/toggle" | ||
|
|
||
| const ToggleGroupContext = React.createContext< | ||
| VariantProps<typeof toggleVariants> | ||
| >({ | ||
| size: "default", | ||
| variant: "default", | ||
| }) | ||
|
|
||
| const ToggleGroup = React.forwardRef< | ||
| React.ElementRef<typeof ToggleGroupPrimitive.Root>, | ||
| React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & | ||
| VariantProps<typeof toggleVariants> | ||
| >(({ className, variant, size, children, ...props }, ref) => ( | ||
| <ToggleGroupPrimitive.Root | ||
| ref={ref} | ||
| className={cn("flex items-center justify-center gap-1", className)} | ||
| {...props} | ||
| > | ||
| <ToggleGroupContext.Provider value={{ variant, size }}> | ||
| {children} | ||
| </ToggleGroupContext.Provider> | ||
| </ToggleGroupPrimitive.Root> | ||
| )) | ||
|
|
||
| ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName | ||
|
|
||
| const ToggleGroupItem = React.forwardRef< | ||
| React.ElementRef<typeof ToggleGroupPrimitive.Item>, | ||
| React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & | ||
| VariantProps<typeof toggleVariants> | ||
| >(({ className, children, variant, size, ...props }, ref) => { | ||
| const context = React.useContext(ToggleGroupContext) | ||
|
|
||
| return ( | ||
| <ToggleGroupPrimitive.Item | ||
| ref={ref} | ||
| className={cn( | ||
| toggleVariants({ | ||
| variant: context.variant || variant, | ||
| size: context.size || size, | ||
| }), | ||
| className | ||
| )} | ||
| {...props} | ||
| > | ||
| {children} | ||
| </ToggleGroupPrimitive.Item> | ||
| ) | ||
| }) | ||
|
|
||
| ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName | ||
|
|
||
| export { ToggleGroup, ToggleGroupItem } |

Uh oh!
There was an error while loading. Please reload this page.