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/dist/index.mjs ADDED
@@ -0,0 +1,320 @@
1
+ // src/components/index.tsx
2
+ import { Text, View, StyleSheet } from "@react-pdf/renderer";
3
+
4
+ // src/utils/rtl.ts
5
+ var RTL_PATTERN = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFC]/;
6
+ var NUMERIC_ONLY_PATTERN = /^[\d\s.,₪$€%+\-*/()]+$/;
7
+ var BIDI = {
8
+ /** Right-to-Left Embedding — forces RTL rendering for the embedded text */
9
+ RLE: "\u202B",
10
+ /** Left-to-Right Embedding */
11
+ LRE: "\u202A",
12
+ /** Pop Directional Formatting — ends the current embedding */
13
+ PDF: "\u202C",
14
+ /** Right-to-Left Mark — invisible RTL directional mark */
15
+ RLM: "\u200F",
16
+ /** Left-to-Right Mark */
17
+ LRM: "\u200E",
18
+ /** Right-to-Left Override — strongly forces RTL */
19
+ RLO: "\u202E"
20
+ };
21
+ function hasRTLChars(str) {
22
+ if (str == null) return false;
23
+ return RTL_PATTERN.test(String(str));
24
+ }
25
+ function isRTLDominant(str) {
26
+ var _a, _b;
27
+ if (str == null) return false;
28
+ const s = String(str);
29
+ const rtlMatches = s.match(
30
+ /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFC]/g
31
+ );
32
+ const ltrMatches = s.match(/[a-zA-Z]/g);
33
+ const rtlCount = (_a = rtlMatches == null ? void 0 : rtlMatches.length) != null ? _a : 0;
34
+ const ltrCount = (_b = ltrMatches == null ? void 0 : ltrMatches.length) != null ? _b : 0;
35
+ return rtlCount > 0 && rtlCount >= ltrCount;
36
+ }
37
+ function wrapRTL(text) {
38
+ if (text.charCodeAt(0) === 8235) return text;
39
+ return `\u202B${text}\u202C`;
40
+ }
41
+ function wrapLTR(text) {
42
+ if (text.charCodeAt(0) === 8234) return text;
43
+ return `\u202A${text}\u202C`;
44
+ }
45
+ function smartWrap(text) {
46
+ if (text == null) return "";
47
+ const str = String(text);
48
+ if (!str) return str;
49
+ const trimmed = str.trim();
50
+ if (!trimmed) return "";
51
+ if (NUMERIC_ONLY_PATTERN.test(trimmed)) return trimmed;
52
+ return isRTLDominant(trimmed) ? wrapRTL(trimmed) : trimmed;
53
+ }
54
+ function formatCurrencyRTL(amount, symbol = "\u20AA", locale = "he-IL") {
55
+ if (!Number.isFinite(amount)) {
56
+ throw new RangeError(
57
+ `formatCurrencyRTL: expected a finite number, got ${amount}`
58
+ );
59
+ }
60
+ const abs = Math.abs(amount);
61
+ const formatted = abs.toLocaleString(locale, {
62
+ minimumFractionDigits: 0,
63
+ maximumFractionDigits: 2
64
+ });
65
+ const valueStr = amount < 0 ? `-${formatted}` : formatted;
66
+ return `\u200F${symbol}${wrapLTR(valueStr)}`;
67
+ }
68
+ function splitBidiSegments(text) {
69
+ if (!text) return [];
70
+ const segments = [];
71
+ let current = "";
72
+ let currentDir = "neutral";
73
+ for (const char of Array.from(text)) {
74
+ const charDir = RTL_PATTERN.test(char) ? "rtl" : /[a-zA-Z]/.test(char) ? "ltr" : "neutral";
75
+ if (charDir !== "neutral" && charDir !== currentDir && current) {
76
+ segments.push({ text: current, direction: currentDir });
77
+ current = char;
78
+ currentDir = charDir;
79
+ } else {
80
+ if (charDir !== "neutral") currentDir = charDir;
81
+ current += char;
82
+ }
83
+ }
84
+ if (current) segments.push({ text: current, direction: currentDir });
85
+ return segments;
86
+ }
87
+ function hasBidiMarkers(text) {
88
+ return /[\u202A-\u202E\u200E\u200F]/.test(text);
89
+ }
90
+ function stripBidiMarkers(text) {
91
+ return text.replace(/[\u202A-\u202E\u200E\u200F]/g, "");
92
+ }
93
+
94
+ // src/components/index.tsx
95
+ import { jsx, jsxs } from "react/jsx-runtime";
96
+ function RTLText({ children, style, direction = "auto", wrap = true, debug }) {
97
+ const str = String(children);
98
+ let content;
99
+ if (direction === "rtl") {
100
+ content = wrapRTL(str);
101
+ } else if (direction === "ltr") {
102
+ content = str;
103
+ } else {
104
+ content = smartWrap(str);
105
+ }
106
+ return /* @__PURE__ */ jsx(Text, { style, wrap, debug, children: content });
107
+ }
108
+ function RTLView({ children, style, debug }) {
109
+ return /* @__PURE__ */ jsx(View, { style: [styles.rtlView, style], debug, children });
110
+ }
111
+ function getRTLPageStyle(fontFamily = "Rubik") {
112
+ return {
113
+ fontFamily,
114
+ fontSize: 11,
115
+ direction: "rtl",
116
+ textAlign: "right",
117
+ paddingTop: 40,
118
+ paddingBottom: 60,
119
+ paddingHorizontal: 40
120
+ };
121
+ }
122
+ function RTLTable({
123
+ columns,
124
+ data,
125
+ fontFamily = "Rubik",
126
+ headerBg = "#1F2937",
127
+ headerColor = "#FFFFFF",
128
+ stripeBg = "#F9FAFB",
129
+ borderColor = "#E5E7EB",
130
+ fontSize = 10
131
+ }) {
132
+ const colStyles = columns.map((col) => {
133
+ var _a;
134
+ return {
135
+ flex: (_a = col.flex) != null ? _a : 1,
136
+ width: col.width,
137
+ paddingHorizontal: 8,
138
+ paddingVertical: 6,
139
+ borderRightWidth: 1,
140
+ borderRightColor: borderColor
141
+ };
142
+ });
143
+ return /* @__PURE__ */ jsxs(View, { style: { borderWidth: 1, borderColor, fontFamily }, children: [
144
+ /* @__PURE__ */ jsx(View, { style: [styles.tableRow, { backgroundColor: headerBg, flexDirection: "row-reverse" }], children: columns.map((col, i) => {
145
+ var _a;
146
+ return /* @__PURE__ */ jsx(View, { style: colStyles[i], children: /* @__PURE__ */ jsx(
147
+ Text,
148
+ {
149
+ style: {
150
+ fontSize,
151
+ fontWeight: "bold",
152
+ color: headerColor,
153
+ textAlign: (_a = col.align) != null ? _a : "right"
154
+ },
155
+ children: wrapRTL(col.header)
156
+ }
157
+ ) }, col.key);
158
+ }) }),
159
+ data.map((row, rowIndex) => /* @__PURE__ */ jsx(
160
+ View,
161
+ {
162
+ style: [
163
+ styles.tableRow,
164
+ { flexDirection: "row-reverse", backgroundColor: rowIndex % 2 === 1 ? stripeBg : "#FFFFFF" }
165
+ ],
166
+ wrap: false,
167
+ children: columns.map((col, i) => {
168
+ var _a;
169
+ const raw = row[col.key];
170
+ let cellText;
171
+ if (col.render) {
172
+ cellText = col.render(raw, row);
173
+ } else if (col.isCurrency && typeof raw === "number") {
174
+ cellText = formatCurrencyRTL(raw);
175
+ } else {
176
+ const str = String(raw != null ? raw : "");
177
+ cellText = hasRTLChars(str) ? wrapRTL(str) : str;
178
+ }
179
+ return /* @__PURE__ */ jsx(View, { style: colStyles[i], children: /* @__PURE__ */ jsx(
180
+ Text,
181
+ {
182
+ style: {
183
+ fontSize,
184
+ color: "#111827",
185
+ textAlign: (_a = col.align) != null ? _a : col.isCurrency ? "left" : "right"
186
+ },
187
+ children: cellText
188
+ }
189
+ ) }, col.key);
190
+ })
191
+ },
192
+ rowIndex
193
+ ))
194
+ ] });
195
+ }
196
+ function RTLDivider({ color = "#E5E7EB", thickness = 1, marginVertical = 8 }) {
197
+ return /* @__PURE__ */ jsx(
198
+ View,
199
+ {
200
+ style: {
201
+ borderBottomWidth: thickness,
202
+ borderBottomColor: color,
203
+ marginVertical
204
+ }
205
+ }
206
+ );
207
+ }
208
+ function RTLSummaryRow({
209
+ label,
210
+ value,
211
+ isCurrency = false,
212
+ bold = false,
213
+ fontSize = 11,
214
+ color = "#111827",
215
+ fontFamily = "Rubik"
216
+ }) {
217
+ const displayValue = isCurrency && typeof value === "number" ? formatCurrencyRTL(value) : String(value);
218
+ return /* @__PURE__ */ jsxs(View, { style: [styles.summaryRow, { fontFamily }], children: [
219
+ /* @__PURE__ */ jsx(Text, { style: { fontSize, fontWeight: bold ? "bold" : "normal", color, textAlign: "right" }, children: wrapRTL(label) }),
220
+ /* @__PURE__ */ jsx(Text, { style: { fontSize, fontWeight: bold ? "bold" : "normal", color, textAlign: "left" }, children: displayValue })
221
+ ] });
222
+ }
223
+ var styles = StyleSheet.create({
224
+ rtlView: {
225
+ flexDirection: "row-reverse",
226
+ textAlign: "right"
227
+ },
228
+ tableRow: {
229
+ flexDirection: "row-reverse",
230
+ alignItems: "center",
231
+ minHeight: 28
232
+ },
233
+ summaryRow: {
234
+ flexDirection: "row",
235
+ justifyContent: "space-between",
236
+ alignItems: "center",
237
+ paddingVertical: 4
238
+ }
239
+ });
240
+
241
+ // src/fonts/register.ts
242
+ import { Font } from "@react-pdf/renderer";
243
+ function registerRTLFont(options) {
244
+ Font.register({
245
+ family: options.family,
246
+ fonts: options.fonts
247
+ });
248
+ }
249
+ function registerRubik() {
250
+ Font.register({
251
+ family: "Rubik",
252
+ fonts: [
253
+ {
254
+ src: "https://fonts.gstatic.com/s/rubik/v28/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-B4i1UA.woff2",
255
+ fontWeight: 300
256
+ },
257
+ {
258
+ src: "https://fonts.gstatic.com/s/rubik/v28/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-NYi1UA.woff2",
259
+ fontWeight: 400
260
+ },
261
+ {
262
+ src: "https://fonts.gstatic.com/s/rubik/v28/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-B4i1UA.woff2",
263
+ fontWeight: 500
264
+ },
265
+ {
266
+ src: "https://fonts.gstatic.com/s/rubik/v28/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-0oi1UA.woff2",
267
+ fontWeight: 700
268
+ }
269
+ ]
270
+ });
271
+ }
272
+ function registerNotoSansHebrew() {
273
+ Font.register({
274
+ family: "NotoSansHebrew",
275
+ fonts: [
276
+ {
277
+ src: "https://fonts.gstatic.com/s/notosanshebrew/v38/or3HQ7v33eiDljA1IufXTtVf7V6RvEEdhQlk0LlGxCyaeNKYZC0sqk3xXGJXag.woff2",
278
+ fontWeight: 400
279
+ },
280
+ {
281
+ src: "https://fonts.gstatic.com/s/notosanshebrew/v38/or3HQ7v33eiDljA1IufXTtVf7V6RvEEdhQlk0LlGxCyaeNKYZC0sqk3xXGJXag.woff2",
282
+ fontWeight: 700
283
+ }
284
+ ]
285
+ });
286
+ }
287
+ function disableHyphenation() {
288
+ Font.registerHyphenationCallback((word) => [word]);
289
+ }
290
+ function setupHebrewPDF(fontFamily = "Rubik") {
291
+ if (fontFamily === "Rubik") {
292
+ registerRubik();
293
+ } else {
294
+ registerNotoSansHebrew();
295
+ }
296
+ disableHyphenation();
297
+ }
298
+ export {
299
+ BIDI,
300
+ RTLDivider,
301
+ RTLSummaryRow,
302
+ RTLTable,
303
+ RTLText,
304
+ RTLView,
305
+ disableHyphenation,
306
+ formatCurrencyRTL,
307
+ getRTLPageStyle,
308
+ hasBidiMarkers,
309
+ hasRTLChars,
310
+ isRTLDominant,
311
+ registerNotoSansHebrew,
312
+ registerRTLFont,
313
+ registerRubik,
314
+ setupHebrewPDF,
315
+ smartWrap,
316
+ splitBidiSegments,
317
+ stripBidiMarkers,
318
+ wrapLTR,
319
+ wrapRTL
320
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "react-pdf-rtl",
3
+ "version": "0.1.1",
4
+ "description": "RTL (Hebrew & Arabic) support utilities and components for @react-pdf/renderer",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --external react --external @react-pdf/renderer",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "npm run build",
22
+ "test": "vitest run",
23
+ "test:coverage": "vitest run --coverage"
24
+ },
25
+ "keywords": [
26
+ "react-pdf",
27
+ "rtl",
28
+ "hebrew",
29
+ "arabic",
30
+ "pdf",
31
+ "right-to-left",
32
+ "bidi",
33
+ "bidirectional",
34
+ "israel",
35
+ "he",
36
+ "ar"
37
+ ],
38
+ "author": "Ben Danziger",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/bendanziger/react-pdf-rtl"
43
+ },
44
+ "peerDependencies": {
45
+ "@react-pdf/renderer": ">=3.0.0",
46
+ "react": ">=17.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@react-pdf/renderer": "^4.3.2",
50
+ "@types/react": "^18.3.28",
51
+ "@vitest/coverage-v8": "^4.0.18",
52
+ "react": "^18.3.1",
53
+ "tsup": "^8.5.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.18"
56
+ }
57
+ }