diff --git a/packages/app/src/components/file-pane-render-mode.test.ts b/packages/app/src/components/file-pane-render-mode.test.ts new file mode 100644 index 000000000..b57b81711 --- /dev/null +++ b/packages/app/src/components/file-pane-render-mode.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; + +describe("isRenderedMarkdownFile", () => { + it("detects .md files", () => { + expect(isRenderedMarkdownFile("README.md")).toBe(true); + expect(isRenderedMarkdownFile("docs/guide.MD")).toBe(true); + }); + + it("detects .markdown files", () => { + expect(isRenderedMarkdownFile("notes.markdown")).toBe(true); + expect(isRenderedMarkdownFile("docs/CHANGELOG.MARKDOWN")).toBe(true); + }); + + it("does not treat .mdx files as rendered markdown", () => { + expect(isRenderedMarkdownFile("page.mdx")).toBe(false); + }); + + it("does not treat other text files as rendered markdown", () => { + expect(isRenderedMarkdownFile("src/index.ts")).toBe(false); + expect(isRenderedMarkdownFile("README.md.txt")).toBe(false); + }); +}); diff --git a/packages/app/src/components/file-pane-render-mode.ts b/packages/app/src/components/file-pane-render-mode.ts new file mode 100644 index 000000000..d6f507f9e --- /dev/null +++ b/packages/app/src/components/file-pane-render-mode.ts @@ -0,0 +1,4 @@ +export function isRenderedMarkdownFile(filePath: string): boolean { + const normalizedPath = filePath.trim().toLowerCase(); + return normalizedPath.endsWith(".md") || normalizedPath.endsWith(".markdown"); +} diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index e11419e57..7341bf888 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; +import Markdown, { MarkdownIt } from "react-native-markdown-display"; import { ActivityIndicator, Image as RNImage, @@ -20,7 +21,9 @@ import { type HighlightStyle, } from "@getpaseo/highlight"; import { lineNumberGutterWidth } from "@/components/code-insets"; +import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; import { isWeb } from "@/constants/platform"; +import { createMarkdownStyles } from "@/styles/markdown-styles"; interface CodeLineProps { tokens: HighlightToken[]; @@ -117,6 +120,9 @@ function FilePreviewBody({ const isDark = theme.colorScheme === "dark"; const colorMap = isDark ? darkHighlightColors : lightHighlightColors; const baseColor = isDark ? "#c9d1d9" : "#24292f"; + const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]); + const markdownParser = useMemo(() => MarkdownIt({ typographer: true, linkify: true }), []); + const isMarkdownFile = preview?.kind === "text" && isRenderedMarkdownFile(filePath); const previewScrollRef = useRef(null); const scrollbar = useWebScrollViewScrollbar(previewScrollRef, { @@ -124,12 +130,12 @@ function FilePreviewBody({ }); const highlightedLines = useMemo(() => { - if (!preview || preview.kind !== "text") { + if (!preview || preview.kind !== "text" || isMarkdownFile) { return null; } return highlightCode(preview.content ?? "", filePath); - }, [preview?.kind, preview?.content, filePath]); + }, [isMarkdownFile, preview?.kind, preview?.content, filePath]); const gutterWidth = useMemo(() => { if (!highlightedLines) return 0; @@ -154,6 +160,28 @@ function FilePreviewBody({ } if (preview.kind === "text") { + if (isMarkdownFile) { + return ( + + + + {preview.content ?? ""} + + + {scrollbar.overlay} + + ); + } + const lines = highlightedLines ?? [[{ text: preview.content ?? "", style: null }]]; const codeLines = ( @@ -329,6 +357,9 @@ const styles = StyleSheet.create((theme) => ({ previewCodeScrollContent: { padding: theme.spacing[4], }, + previewMarkdownScrollContent: { + padding: theme.spacing[4], + }, previewImageScrollContent: { flexGrow: 1, padding: theme.spacing[4],