Skip to content

Commit 2a415f3

Browse files
authored
Merge pull request #3 from mertguvencli/feat/katex-math-support
feat: add KaTeX math/LaTeX rendering support
2 parents 54561ea + 354dbec commit 2a415f3

9 files changed

Lines changed: 341 additions & 56 deletions

File tree

app/globals.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,60 @@
131131
}
132132
}
133133

134+
/* Use tabular (fixed-width) numbers on ordered lists so markers align
135+
consistently — html2canvas renders proportional figures at slightly
136+
different widths, causing misaligned numbering in PDF exports. */
137+
.prose ol {
138+
font-variant-numeric: tabular-nums;
139+
}
140+
.prose ol > li::marker {
141+
font-variant-numeric: tabular-nums;
142+
}
143+
144+
/* Remove smart quotes that Tailwind Typography adds to blockquotes —
145+
they show up as extra quotation marks in PDF exports. */
146+
.prose blockquote p::before,
147+
.prose blockquote p::after {
148+
content: none !important;
149+
}
150+
151+
/* Remove backtick pseudo-elements that Tailwind Typography adds to
152+
inline code — html2canvas cannot resolve utility-class overrides
153+
(before:content-none) reliably, causing text to vanish in PDF exports. */
154+
.prose :where(code):not(:where(pre *))::before,
155+
.prose :where(code):not(:where(pre *))::after {
156+
content: none !important;
157+
}
158+
159+
/* Ensure HTML align attributes work (html2canvas needs explicit CSS) */
160+
[align="center"] {
161+
text-align: center !important;
162+
}
163+
[align="right"] {
164+
text-align: right !important;
165+
}
166+
167+
/* Style <details>/<summary> properly since html2canvas can't render
168+
the native widget. */
169+
.prose details {
170+
border: 1px solid var(--border, #e4e4e7);
171+
border-radius: 0.5rem;
172+
padding: 0.75rem 1rem;
173+
margin-bottom: 1.25rem;
174+
}
175+
.prose details summary {
176+
font-weight: 600;
177+
cursor: pointer;
178+
list-style: none;
179+
margin-bottom: 0;
180+
}
181+
.prose details summary::-webkit-details-marker {
182+
display: none;
183+
}
184+
.prose details[open] summary {
185+
margin-bottom: 0.5rem;
186+
}
187+
134188
/* When exporting to PDF, force code blocks to wrap long lines instead of
135189
scrolling horizontally — otherwise html2canvas captures only the visible
136190
width and overflowing code gets clipped. */
@@ -142,4 +196,9 @@
142196
.pdf-exporting pre code {
143197
white-space: pre-wrap !important;
144198
word-break: break-word !important;
199+
}
200+
201+
/* Force details open during PDF export */
202+
.pdf-exporting details {
203+
open: true;
145204
}

app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono, Inter } from "next/font/google";
3+
import "katex/dist/katex.min.css";
34
import "./globals.css";
45
import { cn } from "@/lib/utils";
56

app/page.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useRef, useState, useCallback } from "react";
3+
import { useRef, useState, useCallback, useEffect } from "react";
44
import { MarkdownEditor } from "@/components/markdown-editor";
55
import { MarkdownPreview } from "@/components/markdown-preview";
66
import { usePdfExport } from "@/lib/use-pdf-export";
@@ -14,7 +14,7 @@ import {
1414
SelectValue,
1515
} from "@/components/ui/select";
1616
import { Separator } from "@/components/ui/separator";
17-
import { Loader2, DownloadIcon } from "lucide-react";
17+
import { Loader2, DownloadIcon, EyeIcon, XIcon, Maximize2Icon, Minimize2Icon } from "lucide-react";
1818

1919
type PageSize = "a4" | "letter" | "legal";
2020

@@ -28,11 +28,21 @@ export default function Home() {
2828
const [markdown, setMarkdown] = useState(defaultMarkdown);
2929
const [pageSize, setPageSize] = useState<PageSize>("a4");
3030
const [editorWidth, setEditorWidth] = useState(35);
31+
const [previewOnly, setPreviewOnly] = useState(false);
3132
const dragging = useRef(false);
3233
const previewRef = useRef<HTMLDivElement>(null);
3334
const containerRef = useRef<HTMLDivElement>(null);
3435
const { exportPdf, exporting } = usePdfExport(previewRef, pageSize);
3536

37+
useEffect(() => {
38+
if (!previewOnly) return;
39+
const handleKeyDown = (e: KeyboardEvent) => {
40+
if (e.key === "Escape") setPreviewOnly(false);
41+
};
42+
document.addEventListener("keydown", handleKeyDown);
43+
return () => document.removeEventListener("keydown", handleKeyDown);
44+
}, [previewOnly]);
45+
3646
const clampWidth = (pct: number) => Math.min(80, Math.max(20, pct));
3747

3848
const handleMouseDown = useCallback((e: React.MouseEvent) => {
@@ -98,6 +108,17 @@ export default function Home() {
98108
</div>
99109

100110
<div className="flex items-center gap-2">
111+
<Button
112+
variant="outline"
113+
size="icon"
114+
onClick={() => setPreviewOnly(true)}
115+
title="Preview only"
116+
>
117+
<Maximize2Icon className="size-4" />
118+
</Button>
119+
120+
<Separator orientation="vertical" className="h-6" />
121+
101122
<Select
102123
value={pageSize}
103124
onValueChange={(v) => setPageSize(v as PageSize)}
@@ -179,6 +200,28 @@ export default function Home() {
179200
</div>
180201
</div>
181202

203+
{/* Preview-only fullscreen modal */}
204+
{previewOnly && (
205+
<div className="fixed inset-0 z-50 flex flex-col bg-white">
206+
<div className="flex shrink-0 justify-end p-4">
207+
<Button
208+
variant="outline"
209+
size="icon"
210+
onClick={() => setPreviewOnly(false)}
211+
>
212+
<Minimize2Icon className="size-4" />
213+
</Button>
214+
</div>
215+
<div className="flex-1 overflow-auto px-8 pb-8">
216+
<MarkdownPreview
217+
fluid
218+
content={markdown}
219+
pageSize={pageSize}
220+
/>
221+
</div>
222+
</div>
223+
)}
224+
182225
{/* SEO content — visible to crawlers, visually hidden from app UI */}
183226
<footer className="sr-only" aria-label="About MarkBloom">
184227
<h2>What is MarkBloom?</h2>

components/markdown-preview.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import { forwardRef } from "react";
44
import ReactMarkdown from "react-markdown";
5+
import rehypeKatex from "rehype-katex";
56
import rehypeRaw from "rehype-raw";
67
import remarkGfm from "remark-gfm";
8+
import remarkMath from "remark-math";
79
import { cn } from "@/lib/utils";
810
import { markdownComponents } from "@/lib/markdown-components";
911

1012
interface MarkdownPreviewProps {
1113
content: string;
1214
className?: string;
1315
pageSize: "a4" | "letter" | "legal";
16+
fluid?: boolean;
1417
}
1518

1619
const pageSizes = {
@@ -20,26 +23,27 @@ const pageSizes = {
2023
};
2124

2225
export const MarkdownPreview = forwardRef<HTMLDivElement, MarkdownPreviewProps>(
23-
function MarkdownPreview({ content, className, pageSize }, ref) {
26+
function MarkdownPreview({ content, className, pageSize, fluid }, ref) {
2427
const size = pageSizes[pageSize];
2528

2629
return (
2730
<div
2831
ref={ref}
2932
className={cn(
30-
"bg-white text-zinc-900 shadow-lg mx-auto",
33+
"bg-white text-zinc-900 mx-auto",
34+
!fluid && "shadow-lg",
3135
className
3236
)}
33-
style={{
37+
style={fluid ? { maxWidth: "860px", width: "100%" } : {
3438
width: size.width,
3539
minHeight: size.minHeight,
36-
padding: "40px 50px",
40+
padding: !fluid && "40px 50px",
3741
}}
3842
>
39-
<article className="prose prose-zinc max-w-none">
43+
<article className="prose prose-zinc max-w-none" style={{ color: "#3f3f46" }}>
4044
<ReactMarkdown
41-
remarkPlugins={[remarkGfm]}
42-
rehypePlugins={[rehypeRaw]}
45+
remarkPlugins={[remarkGfm, remarkMath]}
46+
rehypePlugins={[rehypeRaw, rehypeKatex]}
4347
components={markdownComponents}
4448
>
4549
{content}

0 commit comments

Comments
 (0)