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 +234 -0
- package/dist/index.d.mts +259 -0
- package/dist/index.d.ts +259 -0
- package/dist/index.js +367 -0
- package/dist/index.mjs +320 -0
- package/package.json +57 -0
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
|
+
}
|