react-pdf-rtl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # react-pdf-rtl
2
+
3
+ > RTL (Hebrew & Arabic) support for [`@react-pdf/renderer`](https://react-pdf.org/)
4
+
5
+ `@react-pdf/renderer` is a fantastic library — but Hebrew and Arabic text rendering is broken out of the box. Mixed content like `"חפירה וביסוס - 3 מ״ק"` renders reversed. Currency `₪1,500` gets scrambled. Tables need `row-reverse` everywhere. Font setup is undocumented.
6
+
7
+ This library fixes all of that.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install react-pdf-rtl
15
+ # peer dependencies
16
+ npm install @react-pdf/renderer react
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ```tsx
24
+ import { Document, Page } from "@react-pdf/renderer";
25
+ import {
26
+ setupHebrewPDF,
27
+ RTLText,
28
+ RTLTable,
29
+ RTLSummaryRow,
30
+ RTLDivider,
31
+ getRTLPageStyle,
32
+ } from "react-pdf-rtl";
33
+
34
+ // Register Rubik font + disable hyphenation — call once at module level
35
+ setupHebrewPDF();
36
+
37
+ const pageStyle = getRTLPageStyle("Rubik");
38
+
39
+ export function QuotePDF({ items, total }) {
40
+ return (
41
+ <Document>
42
+ <Page size="A4" style={pageStyle}>
43
+ <RTLText style={{ fontSize: 20, fontWeight: "bold" }}>
44
+ הצעת מחיר
45
+ </RTLText>
46
+
47
+ <RTLDivider marginVertical={12} />
48
+
49
+ <RTLTable
50
+ columns={[
51
+ { header: "תיאור עבודה", key: "description", flex: 3 },
52
+ { header: "כמות", key: "quantity", flex: 1, align: "center" },
53
+ { header: "יחידה", key: "unit", flex: 1, align: "center" },
54
+ { header: "מחיר יחידה", key: "unitPrice", flex: 1, isCurrency: true },
55
+ { header: 'סה"כ', key: "total", flex: 1, isCurrency: true },
56
+ ]}
57
+ data={items}
58
+ headerBg="#1F2937"
59
+ headerColor="#EAB308"
60
+ />
61
+
62
+ <RTLDivider marginVertical={8} />
63
+
64
+ <RTLSummaryRow label='סה"כ לפני מע"מ' value={total} isCurrency />
65
+ <RTLSummaryRow label='מע"מ 18%' value={total * 0.18} isCurrency />
66
+ <RTLSummaryRow
67
+ label="סכום סופי לתשלום"
68
+ value={total * 1.18}
69
+ isCurrency
70
+ bold
71
+ />
72
+ </Page>
73
+ </Document>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## API Reference
81
+
82
+ ### Font Setup
83
+
84
+ #### `setupHebrewPDF(fontFamily?)`
85
+ One-shot setup. Registers font + disables hyphenation. Call once at module level.
86
+ ```ts
87
+ setupHebrewPDF(); // defaults to "Rubik"
88
+ setupHebrewPDF("NotoSansHebrew");
89
+ ```
90
+
91
+ #### `registerRubik()`
92
+ Registers Rubik (weights 300, 400, 500, 700) from Google Fonts CDN.
93
+
94
+ #### `registerNotoSansHebrew()`
95
+ Registers Noto Sans Hebrew from Google Fonts CDN.
96
+
97
+ #### `registerRTLFont(options)`
98
+ Register any custom font:
99
+ ```ts
100
+ registerRTLFont({
101
+ family: "MyFont",
102
+ fonts: [
103
+ { src: "/fonts/MyFont-Regular.ttf", fontWeight: "normal" },
104
+ { src: "/fonts/MyFont-Bold.ttf", fontWeight: "bold" },
105
+ ],
106
+ });
107
+ ```
108
+
109
+ #### `disableHyphenation()`
110
+ Prevents Hebrew words from being split mid-word. Called automatically by `setupHebrewPDF`.
111
+
112
+ ---
113
+
114
+ ### Components
115
+
116
+ #### `<RTLText>`
117
+ Drop-in replacement for `<Text>` with automatic RTL detection and bidi wrapping.
118
+
119
+ ```tsx
120
+ <RTLText style={{ fontSize: 14 }}>חפירה וביסוס - 3 מ"ק</RTLText>
121
+ <RTLText direction="rtl">כותרת</RTLText>
122
+ <RTLText direction="ltr">English text</RTLText>
123
+ <RTLText direction="auto">auto-detected</RTLText> {/* default */}
124
+ ```
125
+
126
+ | Prop | Type | Default | Description |
127
+ |------|------|---------|-------------|
128
+ | `children` | `string \| number` | — | Text content |
129
+ | `style` | `Style` | — | react-pdf style |
130
+ | `direction` | `"rtl" \| "ltr" \| "auto"` | `"auto"` | Force or auto-detect direction |
131
+ | `wrap` | `boolean` | `true` | Allow text wrapping |
132
+
133
+ #### `<RTLView>`
134
+ A `<View>` pre-set with `flexDirection: "row-reverse"` and `textAlign: "right"`.
135
+
136
+ ```tsx
137
+ <RTLView style={{ gap: 8 }}>
138
+ <RTLText>תיאור</RTLText>
139
+ <Text>₪1,200</Text>
140
+ </RTLView>
141
+ ```
142
+
143
+ #### `<RTLTable>`
144
+ Full RTL-aware table. Columns rendered right-to-left. Hebrew cells auto-wrapped.
145
+
146
+ ```tsx
147
+ <RTLTable
148
+ columns={[
149
+ { header: "תיאור", key: "description", flex: 3 },
150
+ { header: "כמות", key: "qty", flex: 1, align: "center" },
151
+ { header: "מחיר", key: "price", flex: 1, isCurrency: true },
152
+ ]}
153
+ data={rows}
154
+ headerBg="#1F2937"
155
+ headerColor="#FFFFFF"
156
+ stripeBg="#F9FAFB"
157
+ />
158
+ ```
159
+
160
+ | Column Prop | Type | Description |
161
+ |------------|------|-------------|
162
+ | `header` | `string` | Column header (auto RTL-wrapped) |
163
+ | `key` | `string` | Row data key |
164
+ | `flex` | `number` | Relative width |
165
+ | `width` | `number` | Fixed width in pts |
166
+ | `align` | `"right" \| "left" \| "center"` | Cell text alignment |
167
+ | `isCurrency` | `boolean` | Formats as `₪1,500` |
168
+ | `render` | `(value, row) => string` | Custom cell renderer |
169
+
170
+ #### `<RTLSummaryRow>`
171
+ Label + value row for totals, right-aligned label, left-aligned value.
172
+
173
+ ```tsx
174
+ <RTLSummaryRow label='סה"כ' value={12500} isCurrency bold />
175
+ ```
176
+
177
+ #### `<RTLDivider>`
178
+ Simple horizontal rule.
179
+ ```tsx
180
+ <RTLDivider color="#E5E7EB" thickness={1} marginVertical={8} />
181
+ ```
182
+
183
+ #### `getRTLPageStyle(fontFamily?)`
184
+ Returns a base `Style` object for RTL pages.
185
+ ```ts
186
+ const style = getRTLPageStyle("Rubik");
187
+ // { fontFamily: "Rubik", direction: "rtl", textAlign: "right", padding: ... }
188
+ ```
189
+
190
+ ---
191
+
192
+ ### Utilities
193
+
194
+ #### `hasRTLChars(str)`
195
+ Returns `true` if string contains any Hebrew/Arabic characters.
196
+
197
+ #### `isRTLDominant(str)`
198
+ Returns `true` if the majority of alphabetic characters are RTL.
199
+
200
+ #### `wrapRTL(str)` / `wrapLTR(str)`
201
+ Manually wrap text in Unicode bidi markers (`\u202B...\u202C`).
202
+
203
+ #### `smartWrap(str)`
204
+ Auto-detect direction and wrap accordingly. Pure numbers are left unchanged.
205
+
206
+ #### `formatCurrencyRTL(amount, symbol?, locale?)`
207
+ Format a number as currency, ensuring `₪` and digits render correctly in RTL context.
208
+ ```ts
209
+ formatCurrencyRTL(1500) // ₪1,500 (correct RTL direction)
210
+ formatCurrencyRTL(1500, "$", "en-US") // $1,500
211
+ ```
212
+
213
+ #### `splitBidiSegments(str)`
214
+ Split mixed text into RTL/LTR/neutral segments:
215
+ ```ts
216
+ splitBidiSegments("שלום world 123")
217
+ // [
218
+ // { text: "שלום ", direction: "rtl" },
219
+ // { text: "world ", direction: "ltr" },
220
+ // { text: "123", direction: "neutral" },
221
+ // ]
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Why This Exists
227
+
228
+ The [`diegomura/react-pdf`](https://github.com/diegomura/react-pdf) issue tracker has RTL/Hebrew bug reports dating back to 2019 ([#732](https://github.com/diegomura/react-pdf/issues/732), [#1571](https://github.com/diegomura/react-pdf/issues/1571), [#3010](https://github.com/diegomura/react-pdf/issues/3010)) with no official fix. This library provides a battle-tested layer on top.
229
+
230
+ ---
231
+
232
+ ## License
233
+
234
+ MIT
@@ -0,0 +1,259 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ import { Style } from '@react-pdf/types';
4
+
5
+ interface RTLTextProps {
6
+ children: string | number;
7
+ style?: Style | Style[];
8
+ /** Force direction instead of auto-detecting */
9
+ direction?: "rtl" | "ltr" | "auto";
10
+ wrap?: boolean;
11
+ debug?: boolean;
12
+ }
13
+ /**
14
+ * Drop-in replacement for @react-pdf/renderer's <Text> with RTL support.
15
+ *
16
+ * Automatically:
17
+ * - Detects Hebrew/Arabic content and applies RLE bidi markers
18
+ * - Handles mixed content (Hebrew text + numbers) correctly
19
+ * - Preserves LTR content unchanged
20
+ *
21
+ * @example
22
+ * <RTLText style={{ fontSize: 14 }}>חפירה וביסוס - 3 מ"ק</RTLText>
23
+ * <RTLText>Item description in English</RTLText>
24
+ */
25
+ declare function RTLText({ children, style, direction, wrap, debug }: RTLTextProps): react_jsx_runtime.JSX.Element;
26
+ interface RTLViewProps {
27
+ children: React.ReactNode;
28
+ style?: Style | Style[];
29
+ debug?: boolean;
30
+ }
31
+ /**
32
+ * A <View> pre-configured for RTL layout.
33
+ * Sets flexDirection to "row-reverse" and textAlign to "right".
34
+ * Use as a wrapper for rows in RTL documents.
35
+ *
36
+ * @example
37
+ * <RTLView style={{ marginBottom: 8 }}>
38
+ * <RTLText>תיאור עבודה</RTLText>
39
+ * <Text>₪1,200</Text>
40
+ * </RTLView>
41
+ */
42
+ declare function RTLView({ children, style, debug }: RTLViewProps): react_jsx_runtime.JSX.Element;
43
+ /**
44
+ * Returns base styles for an RTL page layout.
45
+ * Pass your fontFamily to get consistent defaults.
46
+ *
47
+ * @example
48
+ * const pageStyle = getRTLPageStyle("Rubik");
49
+ */
50
+ declare function getRTLPageStyle(fontFamily?: string): Style;
51
+ interface RTLTableColumn {
52
+ /** Column header label */
53
+ header: string;
54
+ /** Key in the data row object */
55
+ key: string;
56
+ /** Flex width (like CSS flex) */
57
+ flex?: number;
58
+ /** Explicit width in pts */
59
+ width?: number;
60
+ /** Text alignment within the cell */
61
+ align?: "right" | "left" | "center";
62
+ /** If true, formats value as currency using formatCurrencyRTL */
63
+ isCurrency?: boolean;
64
+ /** Custom cell renderer */
65
+ render?: (value: unknown, row: Record<string, unknown>) => string;
66
+ }
67
+ interface RTLTableProps {
68
+ columns: RTLTableColumn[];
69
+ data: Record<string, unknown>[];
70
+ fontFamily?: string;
71
+ /** Header background color */
72
+ headerBg?: string;
73
+ /** Header text color */
74
+ headerColor?: string;
75
+ /** Row stripe color (alternating rows) */
76
+ stripeBg?: string;
77
+ /** Border color */
78
+ borderColor?: string;
79
+ /** Font size for table content */
80
+ fontSize?: number;
81
+ }
82
+ /**
83
+ * A fully RTL-aware table component for @react-pdf/renderer.
84
+ *
85
+ * Renders columns right-to-left, handles Hebrew text in cells,
86
+ * and supports currency formatting out of the box.
87
+ *
88
+ * @example
89
+ * <RTLTable
90
+ * columns={[
91
+ * { header: "תיאור", key: "description", flex: 3 },
92
+ * { header: "כמות", key: "quantity", flex: 1, align: "center" },
93
+ * { header: "מחיר יחידה", key: "unitPrice", flex: 1, isCurrency: true },
94
+ * { header: 'סה"כ', key: "total", flex: 1, isCurrency: true },
95
+ * ]}
96
+ * data={boqItems}
97
+ * headerBg="#1a1a1a"
98
+ * headerColor="#EAB308"
99
+ * />
100
+ */
101
+ declare function RTLTable({ columns, data, fontFamily, headerBg, headerColor, stripeBg, borderColor, fontSize, }: RTLTableProps): react_jsx_runtime.JSX.Element;
102
+ interface RTLDividerProps {
103
+ color?: string;
104
+ thickness?: number;
105
+ marginVertical?: number;
106
+ }
107
+ /**
108
+ * A simple horizontal divider for RTL PDF documents.
109
+ */
110
+ declare function RTLDivider({ color, thickness, marginVertical }: RTLDividerProps): react_jsx_runtime.JSX.Element;
111
+ interface RTLSummaryRowProps {
112
+ label: string;
113
+ value: string | number;
114
+ isCurrency?: boolean;
115
+ bold?: boolean;
116
+ fontSize?: number;
117
+ color?: string;
118
+ fontFamily?: string;
119
+ }
120
+ /**
121
+ * A label + value row for totals/summaries in RTL layout.
122
+ * Label on the right, value on the left — as expected in Hebrew documents.
123
+ *
124
+ * @example
125
+ * <RTLSummaryRow label='סה"כ לפני מע"מ' value={12500} isCurrency bold />
126
+ * <RTLSummaryRow label='מע"מ 18%' value={2250} isCurrency />
127
+ * <RTLSummaryRow label="סכום סופי" value={14750} isCurrency bold color="#EAB308" />
128
+ */
129
+ declare function RTLSummaryRow({ label, value, isCurrency, bold, fontSize, color, fontFamily, }: RTLSummaryRowProps): react_jsx_runtime.JSX.Element;
130
+
131
+ type RTLFontWeight = "normal" | "bold" | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
132
+ interface RTLFontSource {
133
+ src: string;
134
+ fontWeight?: RTLFontWeight;
135
+ fontStyle?: "normal" | "italic";
136
+ }
137
+ interface RegisterRTLFontOptions {
138
+ /** Font family name, e.g. "Rubik" */
139
+ family: string;
140
+ /** Array of font sources (different weights/styles) */
141
+ fonts: RTLFontSource[];
142
+ }
143
+ /**
144
+ * Registers a font family with @react-pdf/renderer.
145
+ * Wraps Font.register with sensible defaults for RTL fonts.
146
+ *
147
+ * @example
148
+ * registerRTLFont({
149
+ * family: "Rubik",
150
+ * fonts: [
151
+ * { src: "/fonts/Rubik-Regular.ttf", fontWeight: "normal" },
152
+ * { src: "/fonts/Rubik-Bold.ttf", fontWeight: "bold" },
153
+ * ],
154
+ * });
155
+ */
156
+ declare function registerRTLFont(options: RegisterRTLFontOptions): void;
157
+ /**
158
+ * Registers the Rubik font from Google Fonts CDN.
159
+ * Rubik is the recommended font for Hebrew PDFs — designed for Hebrew,
160
+ * supports all weights, and renders cleanly in @react-pdf/renderer.
161
+ *
162
+ * Weights registered: 300 (Light), 400 (Regular), 500 (Medium), 700 (Bold)
163
+ */
164
+ declare function registerRubik(): void;
165
+ /**
166
+ * Registers Noto Sans Hebrew from Google Fonts CDN.
167
+ * Good fallback if Rubik is not desired — extensive Hebrew glyph coverage.
168
+ */
169
+ declare function registerNotoSansHebrew(): void;
170
+ /**
171
+ * Disables font hyphenation — critical for Hebrew text.
172
+ * Hebrew words should never be hyphenated mid-word.
173
+ */
174
+ declare function disableHyphenation(): void;
175
+ /**
176
+ * One-shot setup: registers Rubik + disables hyphenation.
177
+ * This is the recommended setup for Hebrew PDFs.
178
+ *
179
+ * @example
180
+ * // At the top of your PDF component file:
181
+ * setupHebrewPDF();
182
+ */
183
+ declare function setupHebrewPDF(fontFamily?: "Rubik" | "NotoSansHebrew"): void;
184
+
185
+ /**
186
+ * Unicode bidi control characters
187
+ */
188
+ declare const BIDI: {
189
+ /** Right-to-Left Embedding — forces RTL rendering for the embedded text */
190
+ readonly RLE: "‫";
191
+ /** Left-to-Right Embedding */
192
+ readonly LRE: "‪";
193
+ /** Pop Directional Formatting — ends the current embedding */
194
+ readonly PDF: "‬";
195
+ /** Right-to-Left Mark — invisible RTL directional mark */
196
+ readonly RLM: "‏";
197
+ /** Left-to-Right Mark */
198
+ readonly LRM: "‎";
199
+ /** Right-to-Left Override — strongly forces RTL */
200
+ readonly RLO: "‮";
201
+ };
202
+ /**
203
+ * Returns true if the string contains any RTL characters (Hebrew, Arabic, etc.)
204
+ * FIX: coerces input to string — safe when called from JS without types
205
+ */
206
+ declare function hasRTLChars(str: unknown): boolean;
207
+ /**
208
+ * Returns true if the MAJORITY of the string's alphabetic content is RTL.
209
+ * Useful for mixed strings like "מחיר: 120 ₪" — still considered RTL.
210
+ * FIX: coerces input to string — safe when called from JS without types
211
+ */
212
+ declare function isRTLDominant(str: unknown): boolean;
213
+ /**
214
+ * Wraps text in RLE...PDF bidi markers so @react-pdf/renderer
215
+ * renders it right-to-left correctly, including mixed content.
216
+ *
217
+ * FIX: idempotent — guards against double-wrapping (broken bidi nesting)
218
+ */
219
+ declare function wrapRTL(text: string): string;
220
+ /**
221
+ * Wraps text in LRE...PDF for explicit LTR segments inside an RTL document.
222
+ * FIX: idempotent — guards against double-wrapping
223
+ */
224
+ declare function wrapLTR(text: string): string;
225
+ /**
226
+ * Smart wrap: auto-detects direction and wraps accordingly.
227
+ * FIX: accepts string | number | null | undefined — BoQ fields are often numbers
228
+ */
229
+ declare function smartWrap(text: string | number | null | undefined): string;
230
+ /**
231
+ * Formats a currency amount for RTL display.
232
+ * Ensures the symbol (₪) and number don't get scrambled in bidi context.
233
+ *
234
+ * FIX: throws RangeError on NaN/Infinity with clear message
235
+ * FIX: handles negative amounts correctly (discounts, returns)
236
+ */
237
+ declare function formatCurrencyRTL(amount: number, symbol?: string, locale?: string): string;
238
+ /**
239
+ * Splits mixed RTL/LTR text into segments with direction metadata.
240
+ * Reconstructs exactly: splitBidiSegments(s).map(s => s.text).join('') === s
241
+ *
242
+ * FIX: uses Array.from() to correctly handle surrogate pairs (emoji, rare Unicode)
243
+ */
244
+ type TextSegment = {
245
+ text: string;
246
+ direction: "rtl" | "ltr" | "neutral";
247
+ };
248
+ declare function splitBidiSegments(text: string): TextSegment[];
249
+ /**
250
+ * Returns true if the string already contains bidi control characters.
251
+ */
252
+ declare function hasBidiMarkers(text: string): boolean;
253
+ /**
254
+ * Strips all bidi control characters from a string.
255
+ * Use for: extracting plain text for storage, search, or logging.
256
+ */
257
+ declare function stripBidiMarkers(text: string): string;
258
+
259
+ export { BIDI, RTLDivider, type RTLDividerProps, type RTLFontSource, type RTLFontWeight, RTLSummaryRow, type RTLSummaryRowProps, RTLTable, type RTLTableColumn, type RTLTableProps, RTLText, type RTLTextProps, RTLView, type RTLViewProps, type RegisterRTLFontOptions, type TextSegment, disableHyphenation, formatCurrencyRTL, getRTLPageStyle, hasBidiMarkers, hasRTLChars, isRTLDominant, registerNotoSansHebrew, registerRTLFont, registerRubik, setupHebrewPDF, smartWrap, splitBidiSegments, stripBidiMarkers, wrapLTR, wrapRTL };