From 90020b5e5dc06060ff23f8c372800999271d059b Mon Sep 17 00:00:00 2001 From: danialshirali16 Date: Mon, 1 Jun 2026 08:39:26 +0000 Subject: [PATCH 1/3] fix(pdf): support RTL text (Persian/Arabic) in PDF export Register Vazirmatn as a second font family for RTL content. In the backend node renderers, read the TipTap `dir` attribute on paragraph and heading nodes; when `dir === "rtl"`, apply fontFamily "Vazirmatn" and default textAlign to "right" so that Persian/Arabic characters are shaped correctly and flow right-to-left. Mirror the same Vazirmatn Font.register and a `[dir='rtl']` stylesheet rule in the frontend HTML-based export path. LTR (Inter) behaviour is completely unchanged. https://claude.ai/code/session_01BxEgURqex1hukdwboM3XF6 --- apps/live/src/lib/pdf/node-renderers.tsx | 27 ++++++++++++++----- apps/live/src/lib/pdf/plane-pdf-exporter.tsx | 19 +++++++++++++ .../core/components/editor/pdf/document.tsx | 13 +++++++++ apps/web/core/constants/editor.ts | 6 +++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/apps/live/src/lib/pdf/node-renderers.tsx b/apps/live/src/lib/pdf/node-renderers.tsx index 003d21f552a..0af078078ff 100644 --- a/apps/live/src/lib/pdf/node-renderers.tsx +++ b/apps/live/src/lib/pdf/node-renderers.tsx @@ -88,6 +88,11 @@ const getFlexAlignStyle = (textAlign: string | null | undefined): Style => { return {}; }; +const getRtlStyle = (dir: string | null | undefined): Style => { + if (dir !== "rtl") return {}; + return { fontFamily: "Vazirmatn" }; +}; + export const nodeRenderers: NodeRendererRegistry = { doc: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( {children} @@ -98,16 +103,21 @@ export const nodeRenderers: NodeRendererRegistry = { paragraph: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { const textAlign = node.attrs?.textAlign as string | null; + const dir = node.attrs?.dir as string | null | undefined; + const isRtl = dir === "rtl"; + // For RTL paragraphs with no explicit alignment, default to right-aligned + const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null); const background = node.attrs?.backgroundColor as string | undefined; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); + const alignStyle = getTextAlignStyle(effectiveTextAlign); + const flexStyle = getFlexAlignStyle(effectiveTextAlign); + const rtlStyle = getRtlStyle(dir); const resolvedBgColor = background && background !== "default" ? resolveColorForPdf(background, "background") : null; const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {}; return ( - {children} + {children} ); }, @@ -117,12 +127,17 @@ export const nodeRenderers: NodeRendererRegistry = { const styleKey = `heading${level}` as keyof typeof pdfStyles; const style = pdfStyles[styleKey] || pdfStyles.heading1; const textAlign = node.attrs?.textAlign as string | null; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); + const dir = node.attrs?.dir as string | null | undefined; + const isRtl = dir === "rtl"; + // For RTL headings with no explicit alignment, default to right-aligned + const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null); + const alignStyle = getTextAlignStyle(effectiveTextAlign); + const flexStyle = getFlexAlignStyle(effectiveTextAlign); + const rtlStyle = getRtlStyle(dir); return ( - {children} + {children} ); }, diff --git a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx index f6c6b599c90..652bb42c147 100644 --- a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx +++ b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx @@ -50,6 +50,25 @@ Font.register({ ], }); +// Resolve Vazirmatn font files from the fonts directory at the package root. +// Place the woff files at apps/live/fonts/vazirmatn/ before starting the server. +// Download from: https://github.com/rastikerdar/vazirmatn/releases +const vazirmatnFontDir = path.resolve(process.cwd(), "fonts/vazirmatn"); + +Font.register({ + family: "Vazirmatn", + fonts: [ + { + src: path.join(vazirmatnFontDir, "vazirmatn-regular.woff"), + fontWeight: 400, + }, + { + src: path.join(vazirmatnFontDir, "vazirmatn-bold.woff"), + fontWeight: 700, + }, + ], +}); + export const createPdfDocument = (doc: TipTapDocument, options: PDFExportOptions = {}) => { const { title, author, subject, pageSize = "A4", pageOrientation = "portrait", metadata, noAssets } = options; diff --git a/apps/web/core/components/editor/pdf/document.tsx b/apps/web/core/components/editor/pdf/document.tsx index 1c439bfae14..e516ae1fbc6 100644 --- a/apps/web/core/components/editor/pdf/document.tsx +++ b/apps/web/core/components/editor/pdf/document.tsx @@ -17,6 +17,11 @@ import interSemibold from "@/app/assets/fonts/inter/semibold.ttf?url"; import interThin from "@/app/assets/fonts/inter/thin.ttf?url"; import interUltraBold from "@/app/assets/fonts/inter/ultrabold.ttf?url"; import interUltraLight from "@/app/assets/fonts/inter/ultralight.ttf?url"; +// Vazirmatn — Persian/Arabic font for RTL content. +// Place font files at apps/web/app/assets/fonts/vazirmatn/ before building. +// Download from: https://github.com/rastikerdar/vazirmatn/releases +import vazirmatnBold from "@/app/assets/fonts/vazirmatn/bold.ttf?url"; +import vazirmatnRegular from "@/app/assets/fonts/vazirmatn/regular.ttf?url"; // constants import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor"; @@ -44,6 +49,14 @@ Font.register({ ], }); +Font.register({ + family: "Vazirmatn", + fonts: [ + { src: vazirmatnRegular, fontWeight: "normal" }, + { src: vazirmatnBold, fontWeight: "bold" }, + ], +}); + type Props = { content: string; pageFormat: PageProps["size"]; diff --git a/apps/web/core/constants/editor.ts b/apps/web/core/constants/editor.ts index 5cd8b929cda..de446b52ea4 100644 --- a/apps/web/core/constants/editor.ts +++ b/apps/web/core/constants/editor.ts @@ -236,6 +236,12 @@ const EDITOR_PDF_FONT_FAMILY_STYLES: Styles = { ".courier-bold": { fontFamily: "Courier-Bold", }, + // RTL content (Persian, Arabic, Hebrew, etc.) — use a font that carries + // the required Unicode shaping tables so letters connect correctly. + "[dir='rtl']": { + fontFamily: "Vazirmatn", + textAlign: "right", + }, }; const EDITOR_PDF_TYPOGRAPHY_STYLES: Styles = { From 1661248b05599dc61eb14fa1aee0f617b5a0e65b Mon Sep 17 00:00:00 2001 From: danialshirali16 Date: Mon, 1 Jun 2026 09:25:26 +0000 Subject: [PATCH 2/3] fix(pdf): resolve Vazirmatn font path relative to bundle, not CWD process.cwd() is fragile in monorepo/runtime environments. tsdown bundles everything into dist/start.js, so resolving relative to import.meta.url (one level up from dist/) gives a stable path to apps/live/fonts/vazirmatn/ regardless of where the process is started. Addresses CodeRabbit review on makeplane/plane#9187. https://claude.ai/code/session_01BxEgURqex1hukdwboM3XF6 --- apps/live/src/lib/pdf/plane-pdf-exporter.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx index 652bb42c147..5e5e63a3fe4 100644 --- a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx +++ b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx @@ -6,6 +6,7 @@ import { createRequire } from "module"; import path from "path"; +import { fileURLToPath } from "url"; import { Document, Font, Page, pdf, Text } from "@react-pdf/renderer"; import { createKeyGenerator, renderNode } from "./node-renderers"; import { pdfStyles } from "./styles"; @@ -50,10 +51,11 @@ Font.register({ ], }); -// Resolve Vazirmatn font files from the fonts directory at the package root. +// Resolve Vazirmatn font files relative to the compiled bundle (dist/start.js), +// so the path is stable regardless of where the process is started from. // Place the woff files at apps/live/fonts/vazirmatn/ before starting the server. // Download from: https://github.com/rastikerdar/vazirmatn/releases -const vazirmatnFontDir = path.resolve(process.cwd(), "fonts/vazirmatn"); +const vazirmatnFontDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../fonts/vazirmatn"); Font.register({ family: "Vazirmatn", From 2042583dc02dd76f0d49b3579c95adbe2f3aa7bf Mon Sep 17 00:00:00 2001 From: danialshirali16 Date: Mon, 1 Jun 2026 09:32:21 +0000 Subject: [PATCH 3/3] refactor(pdf): remove redundant isRtl variable; normalise Vazirmatn font weights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inline `dir === "rtl"` directly into effectiveTextAlign in paragraph and heading renderers — isRtl was only used for that one expression - Change Vazirmatn fontWeight registration from numeric (400/700) to string ("normal"/"bold") to match the weight strings mark-renderers.ts uses at render time, avoiding a potential font-lookup mismatch https://claude.ai/code/session_01BxEgURqex1hukdwboM3XF6 --- apps/live/src/lib/pdf/node-renderers.tsx | 6 ++---- apps/live/src/lib/pdf/plane-pdf-exporter.tsx | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/live/src/lib/pdf/node-renderers.tsx b/apps/live/src/lib/pdf/node-renderers.tsx index 0af078078ff..7abd063ddd1 100644 --- a/apps/live/src/lib/pdf/node-renderers.tsx +++ b/apps/live/src/lib/pdf/node-renderers.tsx @@ -104,9 +104,8 @@ export const nodeRenderers: NodeRendererRegistry = { paragraph: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { const textAlign = node.attrs?.textAlign as string | null; const dir = node.attrs?.dir as string | null | undefined; - const isRtl = dir === "rtl"; // For RTL paragraphs with no explicit alignment, default to right-aligned - const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null); + const effectiveTextAlign = textAlign ?? (dir === "rtl" ? "right" : null); const background = node.attrs?.backgroundColor as string | undefined; const alignStyle = getTextAlignStyle(effectiveTextAlign); const flexStyle = getFlexAlignStyle(effectiveTextAlign); @@ -128,9 +127,8 @@ export const nodeRenderers: NodeRendererRegistry = { const style = pdfStyles[styleKey] || pdfStyles.heading1; const textAlign = node.attrs?.textAlign as string | null; const dir = node.attrs?.dir as string | null | undefined; - const isRtl = dir === "rtl"; // For RTL headings with no explicit alignment, default to right-aligned - const effectiveTextAlign = textAlign ?? (isRtl ? "right" : null); + const effectiveTextAlign = textAlign ?? (dir === "rtl" ? "right" : null); const alignStyle = getTextAlignStyle(effectiveTextAlign); const flexStyle = getFlexAlignStyle(effectiveTextAlign); const rtlStyle = getRtlStyle(dir); diff --git a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx index 5e5e63a3fe4..743eb62ed2b 100644 --- a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx +++ b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx @@ -62,11 +62,11 @@ Font.register({ fonts: [ { src: path.join(vazirmatnFontDir, "vazirmatn-regular.woff"), - fontWeight: 400, + fontWeight: "normal", }, { src: path.join(vazirmatnFontDir, "vazirmatn-bold.woff"), - fontWeight: 700, + fontWeight: "bold", }, ], });