diff --git a/packages/web/package.json b/packages/web/package.json index c40b1bdea..e4ae48489 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -82,7 +82,8 @@ "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", - "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.1.4", "@react-email/components": "^1.0.2", "@react-email/render": "^2.0.0", @@ -171,6 +172,7 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.1", "recharts": "^2.15.3", + "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanelClient.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanelClient.tsx new file mode 100644 index 000000000..ac2944134 --- /dev/null +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanelClient.tsx @@ -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("preview"); + const isMarkdown = language === 'Markdown'; + + return ( + <> + {isMarkdown && ( + <> +
+ value && setViewMode(value)} + > + + Preview + + + Code + + +
+ + )} + {isMarkdown && viewMode === "preview" ? ( + + ) : ( + + )} + + ); +} diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx new file mode 100644 index 000000000..1d217079c --- /dev/null +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureMarkDownPreviewPanel.tsx @@ -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+"/"; + return ( + +
+
+ ( +
+                                    {children}
+                                
+ ), + + 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 ( + + ); + }, + + 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 + {alt + ); + }, + + video: ({ src = "", ...props }) => { + return ( + + ); + }, + + code({ className, children, ...props }) { + const isBlock = + className?.startsWith("language-"); + + if (!isBlock) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); + }, + + table: ({ children }) => ( +
+ {children}
+
+ ), + + a: ({ children, href, ...props }) => { + // Check if link is a video URL + if ( + href && + href.match( + /^https:\/\/github\.com\/user-attachments\/assets\/.+$/, + ) + ) { + return ( + + ); + } + + return ( + + {children} + + ); + }, + }} + > + {source} +
+
+
+
+ ); +} diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/sourcePreviewPanel.tsx similarity index 92% rename from packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx rename to packages/web/src/app/[domain]/browse/[...path]/components/sourcePreviewPanel.tsx index cc5be9090..8ce9fbd55 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/sourcePreviewPanel.tsx @@ -3,16 +3,16 @@ import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { Separator } from "@/components/ui/separator"; import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"; import Image from "next/image"; -import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; +import { SourcePreviewPanelClient } from "./codePreviewPanelClient"; import { getFileSource } from '@/features/git'; -interface CodePreviewPanelProps { +interface SourcePreviewPanelProps { path: string; repoName: string; revisionName?: string; } -export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePreviewPanelProps) => { +export const SourcePreviewPanel = async ({ path, repoName, revisionName }: SourcePreviewPanelProps) => { const [fileSourceResponse, repoInfoResponse] = await Promise.all([ getFileSource({ path, @@ -74,7 +74,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre )} - }> {pathType === 'blob' ? ( - +>({ + size: "default", + variant: "default", +}) + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + + {children} + + +)) + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +}) + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName + +export { ToggleGroup, ToggleGroupItem } diff --git a/packages/web/src/lib/languageDetection.ts b/packages/web/src/lib/languageDetection.ts index e39f38de7..3a7ce2f9c 100644 --- a/packages/web/src/lib/languageDetection.ts +++ b/packages/web/src/lib/languageDetection.ts @@ -1,6 +1,23 @@ import * as linguistLanguages from 'linguist-languages'; import path from 'path'; +// Override map for extensions that are ambiguous in linguist-languages. +// These are extensions where linguist maps to multiple languages, but one +// is overwhelmingly more common in practice. +const ambiguousExtensionOverrides: Record = { + '.cs': 'C#', // Not Smalltalk + '.fs': 'F#', // Not Forth, GLSL, or Filterscript + '.html': 'HTML', // Not Ecmarkup + '.json': 'JSON', // Not OASv2-json, OASv3-json + '.md': 'Markdown', // Not GCC Machine Description + '.rs': 'Rust', // Not RenderScript (deprecated) + '.tsx': 'TSX', // Not XML + '.ts': 'TypeScript', // Not XML + '.txt': 'Text', // Not Adblock Filter List, Vim Help File + '.yaml': 'YAML', // Not MiniYAML, OASv2-yaml, OASv3-yaml + '.yml': 'YAML', +}; + const extensionToLanguage = new Map(); for (const [languageName, languageData] of Object.entries(linguistLanguages)) { @@ -31,6 +48,12 @@ export const detectLanguageFromFilename = (filename: string): string => { // Check for extension match const ext = path.extname(filename).toLowerCase(); + + // Check override map first for ambiguous extensions + if (ext && ext in ambiguousExtensionOverrides) { + return ambiguousExtensionOverrides[ext]; + } + if (ext && extensionToLanguage.has(ext)) { return extensionToLanguage.get(ext)!; } diff --git a/yarn.lock b/yarn.lock index 7b7f0e9ce..d1863ffea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4900,6 +4900,13 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10c0/88860165ee7066fa2c179f32ffcd3ee6d527d9dcdc0e8be85e9cb0e2c84834be8e3c1a976c74ba44b193f709544e12f54455d892b28e32f0708d89deda6b9f1d + languageName: node + linkType: hard + "@radix-ui/react-accordion@npm:^1.2.11": version: 1.2.11 resolution: "@radix-ui/react-accordion@npm:1.2.11" @@ -5812,6 +5819,33 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-roving-focus@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-roving-focus@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/2cd43339c36e89a3bf1db8aab34b939113dfbde56bf3a33df2d74757c78c9489b847b1962f1e2441c67e41817d120cb6177943e0f655f47bc1ff8e44fd55b1a2 + languageName: node + linkType: hard + "@radix-ui/react-roving-focus@npm:1.1.2": version: 1.1.2 resolution: "@radix-ui/react-roving-focus@npm:1.1.2" @@ -6066,13 +6100,38 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-toggle@npm:^1.1.0": - version: 1.1.2 - resolution: "@radix-ui/react-toggle@npm:1.1.2" +"@radix-ui/react-toggle-group@npm:^1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-toggle-group@npm:1.1.11" dependencies: - "@radix-ui/primitive": "npm:1.1.1" - "@radix-ui/react-primitive": "npm:2.0.2" - "@radix-ui/react-use-controllable-state": "npm:1.1.0" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-roving-focus": "npm:1.1.11" + "@radix-ui/react-toggle": "npm:1.1.10" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c8cbccda3e25754ed9f3145c67792df2d5d0ee1a910bde6dc07c4577ab508d4b939f145569d4e2af5b17dc4a5c701473380d8695248f8620cf0a372c05b8e958 + languageName: node + linkType: hard + +"@radix-ui/react-toggle@npm:1.1.10, @radix-ui/react-toggle@npm:^1.1.10": + version: 1.1.10 + resolution: "@radix-ui/react-toggle@npm:1.1.10" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -6083,7 +6142,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/2cd8dc6b64c2680f4c0662ff2424963e8cc432de3a925a549e8fd5e5e7b48da1a08434ef4ab49b6b627faea1628160f89a16f098399104ed06a00220170f72a2 + checksum: 10c0/5406cdf5dd7299ae6cfdb4865dc5fd43ca3c475ebcd4e86830bd296d734255b61f749c9bde452ebfaad126033f92dd1112ee9d95982344ffad34491238dcc9b1 languageName: node linkType: hard @@ -6145,6 +6204,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-callback-ref@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/5f6aff8592dea6a7e46589808912aba3fb3b626cf6edd2b14f01638b61dbbe49eeb9f67cd5601f4c15b2fb547b9a7e825f7c4961acd4dd70176c969ae405f8d8 + languageName: node + linkType: hard + "@radix-ui/react-use-controllable-state@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" @@ -8314,7 +8386,8 @@ __metadata: "@radix-ui/react-switch": "npm:^1.2.4" "@radix-ui/react-tabs": "npm:^1.1.2" "@radix-ui/react-toast": "npm:^1.2.2" - "@radix-ui/react-toggle": "npm:^1.1.0" + "@radix-ui/react-toggle": "npm:^1.1.10" + "@radix-ui/react-toggle-group": "npm:^1.1.11" "@radix-ui/react-tooltip": "npm:^1.1.4" "@react-email/components": "npm:^1.0.2" "@react-email/preview-server": "npm:5.1.0" @@ -8390,6 +8463,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.2.0" fast-deep-equal: "npm:^3.1.3" fuse.js: "npm:^7.0.0" + github-markdown-css: "npm:^5.9.0" google-auth-library: "npm:^10.1.0" graphql: "npm:^16.9.0" http-status-codes: "npm:^2.3.0" @@ -8424,6 +8498,7 @@ __metadata: react-markdown: "npm:^10.1.0" react-resizable-panels: "npm:^2.1.1" recharts: "npm:^2.15.3" + rehype-highlight: "npm:^7.0.2" rehype-raw: "npm:^7.0.0" rehype-sanitize: "npm:^6.0.0" remark-gfm: "npm:^4.0.1" @@ -13455,6 +13530,13 @@ __metadata: languageName: node linkType: hard +"github-markdown-css@npm:^5.9.0": + version: 5.9.0 + resolution: "github-markdown-css@npm:5.9.0" + checksum: 10c0/406f3a95bc8909d970b04e657083fbbc548c7e13c53e85c9fe46cbdc070d530b4b42b5a48363dd3775330b30fb88330df5854c62af8dd93ed5818be845c36e19 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -13767,6 +13849,15 @@ __metadata: languageName: node linkType: hard +"hast-util-is-element@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-is-element@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b + languageName: node + linkType: hard + "hast-util-parse-selector@npm:^4.0.0": version: 4.0.0 resolution: "hast-util-parse-selector@npm:4.0.0" @@ -13865,6 +13956,18 @@ __metadata: languageName: node linkType: hard +"hast-util-to-text@npm:^4.0.0": + version: 4.0.2 + resolution: "hast-util-to-text@npm:4.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + unist-util-find-after: "npm:^5.0.0" + checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946 + languageName: node + linkType: hard + "hast-util-whitespace@npm:^3.0.0": version: 3.0.0 resolution: "hast-util-whitespace@npm:3.0.0" @@ -13887,6 +13990,13 @@ __metadata: languageName: node linkType: hard +"highlight.js@npm:~11.11.0": + version: 11.11.1 + resolution: "highlight.js@npm:11.11.1" + checksum: 10c0/40f53ac19dac079891fcefd5bd8a21cf2e8931fd47da5bd1dca73b7e4375c1defed0636fc39120c639b9c44119b7d110f7f0c15aa899557a5a1c8910f3c0144c + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -15185,6 +15295,17 @@ __metadata: languageName: node linkType: hard +"lowlight@npm:^3.0.0": + version: 3.3.0 + resolution: "lowlight@npm:3.3.0" + dependencies: + "@types/hast": "npm:^3.0.0" + devlop: "npm:^1.0.0" + highlight.js: "npm:~11.11.0" + checksum: 10c0/9b796fa8443b0334ebf18bc57387c9ee31432d8c263cf2089d23e1087c653d708447284e0647bf993cb2cdc810e0b268a28f51ea27b4a624893b97bdd3f025f4 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -18179,6 +18300,19 @@ __metadata: languageName: node linkType: hard +"rehype-highlight@npm:^7.0.2": + version: 7.0.2 + resolution: "rehype-highlight@npm:7.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-to-text: "npm:^4.0.0" + lowlight: "npm:^3.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/b62effff554f9a3f2ad8688c675bc7580e6dcf20a6988f86a25e95f1adea2f4900f8a13f96ec7db6a543314aed28267e4057f8dbc71c02a76a9511a716e88da2 + languageName: node + linkType: hard + "rehype-raw@npm:^7.0.0": version: 7.0.0 resolution: "rehype-raw@npm:7.0.0" @@ -20614,6 +20748,16 @@ __metadata: languageName: node linkType: hard +"unist-util-find-after@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-find-after@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb + languageName: node + linkType: hard + "unist-util-is@npm:^6.0.0": version: 6.0.0 resolution: "unist-util-is@npm:6.0.0"