@digicole/pdfmake-rtl 2.1.0 → 2.1.2
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/CHANGELOG.md +118 -83
- package/README.md +11 -10
- package/build/pdfmake.js +71 -42
- package/build/pdfmake.js.map +1 -1
- package/build/pdfmake.min.js +2 -2
- package/build/pdfmake.min.js.map +1 -1
- package/build/vfs_fonts.js +11 -11
- package/js/3rd-party/svg-to-pdfkit/source.js +3823 -0
- package/js/3rd-party/svg-to-pdfkit.js +7 -0
- package/js/DocMeasure.js +713 -0
- package/js/DocPreprocessor.js +275 -0
- package/js/DocumentContext.js +310 -0
- package/js/ElementWriter.js +687 -0
- package/js/LayoutBuilder.js +1240 -0
- package/js/Line.js +113 -0
- package/js/OutputDocument.js +64 -0
- package/js/OutputDocumentServer.js +29 -0
- package/js/PDFDocument.js +144 -0
- package/js/PageElementWriter.js +161 -0
- package/js/PageSize.js +74 -0
- package/js/Printer.js +351 -0
- package/js/Renderer.js +417 -0
- package/js/SVGMeasure.js +92 -0
- package/js/StyleContextStack.js +191 -0
- package/js/TableProcessor.js +575 -0
- package/js/TextBreaker.js +166 -0
- package/js/TextDecorator.js +152 -0
- package/js/TextInlines.js +244 -0
- package/js/URLResolver.js +43 -0
- package/js/base.js +59 -0
- package/js/browser-extensions/OutputDocumentBrowser.js +82 -0
- package/js/browser-extensions/fonts/Cairo.js +38 -0
- package/js/browser-extensions/fonts/Roboto.js +38 -0
- package/js/browser-extensions/index.js +59 -0
- package/js/browser-extensions/pdfMake.js +3 -0
- package/js/browser-extensions/standard-fonts/Courier.js +38 -0
- package/js/browser-extensions/standard-fonts/Helvetica.js +38 -0
- package/js/browser-extensions/standard-fonts/Symbol.js +23 -0
- package/js/browser-extensions/standard-fonts/Times.js +38 -0
- package/js/browser-extensions/standard-fonts/ZapfDingbats.js +23 -0
- package/js/browser-extensions/virtual-fs-cjs.js +3 -0
- package/js/columnCalculator.js +148 -0
- package/js/helpers/node.js +123 -0
- package/js/helpers/tools.js +46 -0
- package/js/helpers/variableType.js +59 -0
- package/js/index.js +15 -0
- package/js/qrEnc.js +721 -0
- package/js/rtlUtils.js +519 -0
- package/js/standardPageSizes.js +56 -0
- package/js/tableLayouts.js +98 -0
- package/js/virtual-fs.js +60 -0
- package/package.json +1 -1
- package/src/{docMeasure.js → DocMeasure.js} +8 -8
- package/src/{elementWriter.js → ElementWriter.js} +3 -3
- package/src/{layoutBuilder.js → LayoutBuilder.js} +1406 -1393
- package/src/{tableProcessor.js → TableProcessor.js} +633 -620
- package/src/rtlUtils.js +503 -500
- /package/src/{docPreprocessor.js → DocPreprocessor.js} +0 -0
- /package/src/{documentContext.js → DocumentContext.js} +0 -0
- /package/src/{line.js → Line.js} +0 -0
- /package/src/{pageElementWriter.js → PageElementWriter.js} +0 -0
- /package/src/{printer.js → Printer.js} +0 -0
- /package/src/{svgMeasure.js → SVGMeasure.js} +0 -0
- /package/src/{styleContextStack.js → StyleContextStack.js} +0 -0
- /package/src/{textDecorator.js → TextDecorator.js} +0 -0
package/src/rtlUtils.js
CHANGED
|
@@ -1,500 +1,503 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RTL (Right-to-Left) utilities for handling Arabic, Persian (Farsi), and Urdu languages
|
|
3
|
-
* Production-ready module for pdfmake RTL support
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Unicode ranges for Arabic script (includes Persian and Urdu characters)
|
|
7
|
-
const ARABIC_RANGE = [
|
|
8
|
-
[0x0600, 0x06FF], // Arabic block
|
|
9
|
-
[0x0750, 0x077F], // Arabic Supplement
|
|
10
|
-
[0x08A0, 0x08FF], // Arabic Extended-A
|
|
11
|
-
[0xFB50, 0xFDFF], // Arabic Presentation Forms-A
|
|
12
|
-
[0xFE70, 0xFEFF] // Arabic Presentation Forms-B
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
// Unicode ranges for Persian (Farsi) specific characters
|
|
16
|
-
const PERSIAN_RANGE = [
|
|
17
|
-
[0x06A9, 0x06AF], // Persian Kaf, Gaf
|
|
18
|
-
[0x06C0, 0x06C3], // Persian Heh,
|
|
19
|
-
[0x06CC, 0x06CE], // Persian Yeh variants
|
|
20
|
-
[0x06D0, 0x06D5], // Persian Yeh Barree, Arabic-Indic digits
|
|
21
|
-
[0x200C, 0x200D] // Zero Width Non-Joiner, Zero Width Joiner (used in Persian)
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
// Unicode ranges for Urdu specific characters
|
|
25
|
-
const URDU_RANGE = [
|
|
26
|
-
[0x0679, 0x0679], // Urdu Tteh
|
|
27
|
-
[0x067E, 0x067E], // Urdu Peh
|
|
28
|
-
[0x0686, 0x0686], // Urdu Tcheh
|
|
29
|
-
[0x0688, 0x0688], // Urdu Ddal
|
|
30
|
-
[0x0691, 0x0691], // Urdu Rreh
|
|
31
|
-
[0x0698, 0x0698], // Urdu Jeh
|
|
32
|
-
[0x06A9, 0x06A9], // Urdu Keheh
|
|
33
|
-
[0x06AF, 0x06AF], // Urdu Gaf
|
|
34
|
-
[0x06BA, 0x06BA], // Urdu Noon Ghunna
|
|
35
|
-
[0x06BE, 0x06BE], // Urdu Heh Doachashmee
|
|
36
|
-
[0x06C1, 0x06C1], // Urdu Heh Goal
|
|
37
|
-
[0x06D2, 0x06D2], // Urdu Yeh Barree
|
|
38
|
-
[0x06D3, 0x06D3] // Urdu Yeh Barree with Hamza
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
// Strong RTL characters (Arabic, Persian, Urdu)
|
|
42
|
-
const RTL_CHARS = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u200C-\u200D]/;
|
|
43
|
-
|
|
44
|
-
// Strong LTR characters (Latin, etc.)
|
|
45
|
-
const LTR_CHARS = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if a character is in Arabic script (includes Persian and Urdu)
|
|
49
|
-
* @param {string} char - Single character to check
|
|
50
|
-
* @returns {boolean} - True if character is Arabic/Persian/Urdu
|
|
51
|
-
*/
|
|
52
|
-
function isArabicChar(char) {
|
|
53
|
-
const code = char.charCodeAt(0);
|
|
54
|
-
return ARABIC_RANGE.some(range => code >= range[0] && code <= range[1]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check if a character is in Persian (Farsi) script
|
|
59
|
-
* @param {string} char - Single character to check
|
|
60
|
-
* @returns {boolean} - True if character is Persian
|
|
61
|
-
*/
|
|
62
|
-
function isPersianChar(char) {
|
|
63
|
-
const code = char.charCodeAt(0);
|
|
64
|
-
return PERSIAN_RANGE.some(range => code >= range[0] && code <= range[1]) || isArabicChar(char);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Check if a character is in Urdu script
|
|
69
|
-
* @param {string} char - Single character to check
|
|
70
|
-
* @returns {boolean} - True if character is Urdu
|
|
71
|
-
*/
|
|
72
|
-
function isUrduChar(char) {
|
|
73
|
-
const code = char.charCodeAt(0);
|
|
74
|
-
return URDU_RANGE.some(range => code >= range[0] && code <= range[1]) || isArabicChar(char);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Check if a character requires RTL rendering
|
|
79
|
-
* @param {string} char - Single character to check
|
|
80
|
-
* @returns {boolean} - True if character requires RTL
|
|
81
|
-
*/
|
|
82
|
-
function isRTLChar(char) {
|
|
83
|
-
return RTL_CHARS.test(char);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if a character is strongly LTR
|
|
88
|
-
* @param {string} char - Single character to check
|
|
89
|
-
* @returns {boolean} - True if character is strongly LTR
|
|
90
|
-
*/
|
|
91
|
-
function isLTRChar(char) {
|
|
92
|
-
return LTR_CHARS.test(char);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Check if text contains any RTL characters
|
|
97
|
-
* @param {string} text - Text to check
|
|
98
|
-
* @returns {boolean} - True if text contains RTL characters
|
|
99
|
-
*/
|
|
100
|
-
function containsRTL(text) {
|
|
101
|
-
if (!text || typeof text !== 'string') {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
return RTL_CHARS.test(text);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Determine the predominant text direction of a string
|
|
109
|
-
* @param {string} text - Text to analyze
|
|
110
|
-
* @returns {string} - 'rtl', 'ltr', or 'neutral'
|
|
111
|
-
*/
|
|
112
|
-
function getTextDirection(text) {
|
|
113
|
-
if (!text || typeof text !== 'string') {
|
|
114
|
-
return 'neutral';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let rtlCount = 0;
|
|
118
|
-
let ltrCount = 0;
|
|
119
|
-
|
|
120
|
-
for (let i = 0; i < text.length; i++) {
|
|
121
|
-
const char = text.charAt(i);
|
|
122
|
-
if (isRTLChar(char)) {
|
|
123
|
-
rtlCount++;
|
|
124
|
-
} else if (isLTRChar(char)) {
|
|
125
|
-
ltrCount++;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// If we have any strong directional characters
|
|
130
|
-
if (rtlCount > 0 || ltrCount > 0) {
|
|
131
|
-
if (rtlCount > ltrCount) {
|
|
132
|
-
return 'rtl';
|
|
133
|
-
} else if (ltrCount > rtlCount) {
|
|
134
|
-
return 'ltr';
|
|
135
|
-
} else {
|
|
136
|
-
// Equal counts - slight preference for RTL if both exist
|
|
137
|
-
return rtlCount > 0 ? 'rtl' : 'ltr';
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return 'neutral';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Fix bracket directionality for RTL text
|
|
146
|
-
* Only mirrors brackets that are in RTL context (adjacent to RTL characters)
|
|
147
|
-
* Brackets next to numbers or LTR text are preserved (e.g. "1)" stays as "1)")
|
|
148
|
-
* @param {string} text - Text to process
|
|
149
|
-
* @returns {string} - Text with contextually mirrored brackets
|
|
150
|
-
*/
|
|
151
|
-
function fixArabicTextUsingReplace(text) {
|
|
152
|
-
if (!text || typeof text !== 'string') {
|
|
153
|
-
return text;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Remove leading dot if present
|
|
157
|
-
if (text.startsWith('.')) {
|
|
158
|
-
text = text.slice(1);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const DIGIT_OR_LTR = /[0-9A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
|
|
162
|
-
const mirrorMap = { '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<' };
|
|
163
|
-
const openBrackets = { '(': ')', '[': ']', '{': '}', '<': '>' };
|
|
164
|
-
|
|
165
|
-
// --- Pre-pass: find matched bracket pairs ---
|
|
166
|
-
// Any bracket that is part of a balanced open/close pair within the same
|
|
167
|
-
// text should NOT be mirrored, because the PDF engine will display them
|
|
168
|
-
// in visual order and mirroring would invert them.
|
|
169
|
-
let pairedIndices = new Set();
|
|
170
|
-
let stack = [];
|
|
171
|
-
for (let i = 0; i < text.length; i++) {
|
|
172
|
-
let ch = text[i];
|
|
173
|
-
if (openBrackets[ch] !== undefined) {
|
|
174
|
-
stack.push({ ch: ch, idx: i });
|
|
175
|
-
} else if (ch === ')' || ch === ']' || ch === '}' || ch === '>') {
|
|
176
|
-
// Find matching opening bracket on stack
|
|
177
|
-
for (let s = stack.length - 1; s >= 0; s--) {
|
|
178
|
-
if (openBrackets[stack[s].ch] === ch) {
|
|
179
|
-
pairedIndices.add(stack[s].idx);
|
|
180
|
-
pairedIndices.add(i);
|
|
181
|
-
stack.splice(s, 1);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
let result = '';
|
|
189
|
-
for (let i = 0; i < text.length; i++) {
|
|
190
|
-
const ch = text[i];
|
|
191
|
-
if (mirrorMap[ch] !== undefined) {
|
|
192
|
-
// If this bracket is part of a balanced pair, do NOT mirror it
|
|
193
|
-
if (pairedIndices.has(i)) {
|
|
194
|
-
result += ch;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Find the previous non-space character
|
|
199
|
-
let prevChar = null;
|
|
200
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
201
|
-
if (text[j] !== ' ') {
|
|
202
|
-
prevChar = text[j];
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Find the next non-space character
|
|
207
|
-
let nextChar = null;
|
|
208
|
-
for (let j = i + 1; j < text.length; j++) {
|
|
209
|
-
if (text[j] !== ' ') {
|
|
210
|
-
nextChar = text[j];
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// If previous char is a digit or LTR letter, bracket belongs to it — don't mirror
|
|
216
|
-
// e.g. "1)" or "a)" — the bracket is part of numbering
|
|
217
|
-
if (prevChar && DIGIT_OR_LTR.test(prevChar)) {
|
|
218
|
-
result += ch;
|
|
219
|
-
}
|
|
220
|
-
// If next char is a digit or LTR letter and no RTL before, don't mirror
|
|
221
|
-
// e.g. "(Hello"
|
|
222
|
-
else if (!prevChar && nextChar && DIGIT_OR_LTR.test(nextChar)) {
|
|
223
|
-
result += ch;
|
|
224
|
-
}
|
|
225
|
-
// Otherwise mirror — bracket is in RTL context (unpaired bracket)
|
|
226
|
-
else {
|
|
227
|
-
result += mirrorMap[ch];
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
result += ch;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Apply RTL properties to a node
|
|
239
|
-
* @param {object} node - Document node
|
|
240
|
-
* @param {boolean} forceRTL - Force RTL regardless of content
|
|
241
|
-
* @returns {object} - Node with RTL properties applied
|
|
242
|
-
*/
|
|
243
|
-
function applyRTLToNode(node, forceRTL = false) {
|
|
244
|
-
if (!node || typeof node !== 'object') {
|
|
245
|
-
return node;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Determine if node should be RTL
|
|
249
|
-
let shouldBeRTL = forceRTL;
|
|
250
|
-
|
|
251
|
-
if (!forceRTL && node.text) {
|
|
252
|
-
const textStr = typeof node.text === 'string' ? node.text :
|
|
253
|
-
Array.isArray(node.text) ? node.text.join('') : '';
|
|
254
|
-
shouldBeRTL = getTextDirection(textStr) === 'rtl';
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (shouldBeRTL) {
|
|
258
|
-
// Set structural RTL properties (alignment)
|
|
259
|
-
// Font is resolved later by TextInlines.measure with priority:
|
|
260
|
-
// item font > style font > defaultStyle font > auto-detect (Cairo for RTL, Roboto for LTR)
|
|
261
|
-
// Text shaping (bracket mirroring) is handled at render time in ElementWriter
|
|
262
|
-
if (!node.alignment) {
|
|
263
|
-
node.alignment = 'right';
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return node;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Process table for RTL layout
|
|
272
|
-
* @param {object} tableNode - Table definition object
|
|
273
|
-
* @param {boolean} forceRTL - Force RTL layout
|
|
274
|
-
* @returns {object} - Processed table node
|
|
275
|
-
*/
|
|
276
|
-
/**
|
|
277
|
-
* Reverse a table row for RTL layout, correctly handling colSpan groups.
|
|
278
|
-
* A colSpan cell and its trailing empty placeholders ({}) are kept together
|
|
279
|
-
* as a single logical group and reversed as a unit.
|
|
280
|
-
* @param {Array} row - Table row array
|
|
281
|
-
* @returns {Array} - Reversed row
|
|
282
|
-
*/
|
|
283
|
-
function reverseTableRow(row) {
|
|
284
|
-
// Build logical groups: each group is either a single cell or a colSpan cell + its placeholders
|
|
285
|
-
let groups = [];
|
|
286
|
-
let i = 0;
|
|
287
|
-
while (i < row.length) {
|
|
288
|
-
let cell = row[i];
|
|
289
|
-
let span = (cell && typeof cell === 'object' && cell.colSpan && cell.colSpan > 1) ? cell.colSpan : 1;
|
|
290
|
-
let group = row.slice(i, i + span);
|
|
291
|
-
groups.push(group);
|
|
292
|
-
i += span;
|
|
293
|
-
}
|
|
294
|
-
// Reverse the groups, then flatten back to a single array
|
|
295
|
-
groups.reverse();
|
|
296
|
-
let result = [];
|
|
297
|
-
for (let g = 0; g < groups.length; g++) {
|
|
298
|
-
for (let k = 0; k < groups[g].length; k++) {
|
|
299
|
-
result.push(groups[g][k]);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return result;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function processRTLTable(tableNode, forceRTL = false) {
|
|
306
|
-
if (!tableNode || !tableNode.table || !tableNode.table.body) {
|
|
307
|
-
return tableNode;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Support rtl: true directly on the table object: table: { rtl: true, body: [...] }
|
|
311
|
-
// This forces RTL layout regardless of content detection
|
|
312
|
-
if (tableNode.table.rtl === true) {
|
|
313
|
-
forceRTL = true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Determine if table should be RTL
|
|
317
|
-
let shouldBeRTL = forceRTL;
|
|
318
|
-
|
|
319
|
-
if (!forceRTL) {
|
|
320
|
-
// Auto-detect based on content
|
|
321
|
-
let rtlCellCount = 0;
|
|
322
|
-
let totalCells = 0;
|
|
323
|
-
|
|
324
|
-
tableNode.table.body.forEach(row => {
|
|
325
|
-
if (Array.isArray(row)) {
|
|
326
|
-
row.forEach(cell => {
|
|
327
|
-
totalCells++;
|
|
328
|
-
const cellText = typeof cell === 'string' ? cell :
|
|
329
|
-
(cell && cell.text ? (typeof cell.text === 'string' ? cell.text : String(cell.text)) : '');
|
|
330
|
-
if (containsRTL(cellText)) {
|
|
331
|
-
rtlCellCount++;
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// If more than 30% of cells contain RTL content, treat as RTL table
|
|
338
|
-
shouldBeRTL = totalCells > 0 && (rtlCellCount / totalCells) >= 0.3;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (shouldBeRTL) {
|
|
342
|
-
//
|
|
343
|
-
tableNode.table.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
*
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (item.
|
|
414
|
-
item.
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
*
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (element.
|
|
470
|
-
element.
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1
|
+
/**
|
|
2
|
+
* RTL (Right-to-Left) utilities for handling Arabic, Persian (Farsi), and Urdu languages
|
|
3
|
+
* Production-ready module for pdfmake RTL support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Unicode ranges for Arabic script (includes Persian and Urdu characters)
|
|
7
|
+
const ARABIC_RANGE = [
|
|
8
|
+
[0x0600, 0x06FF], // Arabic block
|
|
9
|
+
[0x0750, 0x077F], // Arabic Supplement
|
|
10
|
+
[0x08A0, 0x08FF], // Arabic Extended-A
|
|
11
|
+
[0xFB50, 0xFDFF], // Arabic Presentation Forms-A
|
|
12
|
+
[0xFE70, 0xFEFF] // Arabic Presentation Forms-B
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Unicode ranges for Persian (Farsi) specific characters
|
|
16
|
+
const PERSIAN_RANGE = [
|
|
17
|
+
[0x06A9, 0x06AF], // Persian Kaf, Gaf
|
|
18
|
+
[0x06C0, 0x06C3], // Persian Heh, Marbuta variants
|
|
19
|
+
[0x06CC, 0x06CE], // Persian Yeh variants
|
|
20
|
+
[0x06D0, 0x06D5], // Persian Yeh Barree, Arabic-Indic digits
|
|
21
|
+
[0x200C, 0x200D] // Zero Width Non-Joiner, Zero Width Joiner (used in Persian)
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Unicode ranges for Urdu specific characters
|
|
25
|
+
const URDU_RANGE = [
|
|
26
|
+
[0x0679, 0x0679], // Urdu Tteh
|
|
27
|
+
[0x067E, 0x067E], // Urdu Peh
|
|
28
|
+
[0x0686, 0x0686], // Urdu Tcheh
|
|
29
|
+
[0x0688, 0x0688], // Urdu Ddal
|
|
30
|
+
[0x0691, 0x0691], // Urdu Rreh
|
|
31
|
+
[0x0698, 0x0698], // Urdu Jeh
|
|
32
|
+
[0x06A9, 0x06A9], // Urdu Keheh
|
|
33
|
+
[0x06AF, 0x06AF], // Urdu Gaf
|
|
34
|
+
[0x06BA, 0x06BA], // Urdu Noon Ghunna
|
|
35
|
+
[0x06BE, 0x06BE], // Urdu Heh Doachashmee
|
|
36
|
+
[0x06C1, 0x06C1], // Urdu Heh Goal
|
|
37
|
+
[0x06D2, 0x06D2], // Urdu Yeh Barree
|
|
38
|
+
[0x06D3, 0x06D3] // Urdu Yeh Barree with Hamza
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Strong RTL characters (Arabic, Persian, Urdu)
|
|
42
|
+
const RTL_CHARS = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u200C-\u200D]/;
|
|
43
|
+
|
|
44
|
+
// Strong LTR characters (Latin, etc.)
|
|
45
|
+
const LTR_CHARS = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a character is in Arabic script (includes Persian and Urdu)
|
|
49
|
+
* @param {string} char - Single character to check
|
|
50
|
+
* @returns {boolean} - True if character is Arabic/Persian/Urdu
|
|
51
|
+
*/
|
|
52
|
+
function isArabicChar(char) {
|
|
53
|
+
const code = char.charCodeAt(0);
|
|
54
|
+
return ARABIC_RANGE.some(range => code >= range[0] && code <= range[1]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a character is in Persian (Farsi) script
|
|
59
|
+
* @param {string} char - Single character to check
|
|
60
|
+
* @returns {boolean} - True if character is Persian
|
|
61
|
+
*/
|
|
62
|
+
function isPersianChar(char) {
|
|
63
|
+
const code = char.charCodeAt(0);
|
|
64
|
+
return PERSIAN_RANGE.some(range => code >= range[0] && code <= range[1]) || isArabicChar(char);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a character is in Urdu script
|
|
69
|
+
* @param {string} char - Single character to check
|
|
70
|
+
* @returns {boolean} - True if character is Urdu
|
|
71
|
+
*/
|
|
72
|
+
function isUrduChar(char) {
|
|
73
|
+
const code = char.charCodeAt(0);
|
|
74
|
+
return URDU_RANGE.some(range => code >= range[0] && code <= range[1]) || isArabicChar(char);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a character requires RTL rendering
|
|
79
|
+
* @param {string} char - Single character to check
|
|
80
|
+
* @returns {boolean} - True if character requires RTL
|
|
81
|
+
*/
|
|
82
|
+
function isRTLChar(char) {
|
|
83
|
+
return RTL_CHARS.test(char);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a character is strongly LTR
|
|
88
|
+
* @param {string} char - Single character to check
|
|
89
|
+
* @returns {boolean} - True if character is strongly LTR
|
|
90
|
+
*/
|
|
91
|
+
function isLTRChar(char) {
|
|
92
|
+
return LTR_CHARS.test(char);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if text contains any RTL characters
|
|
97
|
+
* @param {string} text - Text to check
|
|
98
|
+
* @returns {boolean} - True if text contains RTL characters
|
|
99
|
+
*/
|
|
100
|
+
function containsRTL(text) {
|
|
101
|
+
if (!text || typeof text !== 'string') {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return RTL_CHARS.test(text);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Determine the predominant text direction of a string
|
|
109
|
+
* @param {string} text - Text to analyze
|
|
110
|
+
* @returns {string} - 'rtl', 'ltr', or 'neutral'
|
|
111
|
+
*/
|
|
112
|
+
function getTextDirection(text) {
|
|
113
|
+
if (!text || typeof text !== 'string') {
|
|
114
|
+
return 'neutral';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let rtlCount = 0;
|
|
118
|
+
let ltrCount = 0;
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < text.length; i++) {
|
|
121
|
+
const char = text.charAt(i);
|
|
122
|
+
if (isRTLChar(char)) {
|
|
123
|
+
rtlCount++;
|
|
124
|
+
} else if (isLTRChar(char)) {
|
|
125
|
+
ltrCount++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If we have any strong directional characters
|
|
130
|
+
if (rtlCount > 0 || ltrCount > 0) {
|
|
131
|
+
if (rtlCount > ltrCount) {
|
|
132
|
+
return 'rtl';
|
|
133
|
+
} else if (ltrCount > rtlCount) {
|
|
134
|
+
return 'ltr';
|
|
135
|
+
} else {
|
|
136
|
+
// Equal counts - slight preference for RTL if both exist
|
|
137
|
+
return rtlCount > 0 ? 'rtl' : 'ltr';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return 'neutral';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fix bracket directionality for RTL text
|
|
146
|
+
* Only mirrors brackets that are in RTL context (adjacent to RTL characters)
|
|
147
|
+
* Brackets next to numbers or LTR text are preserved (e.g. "1)" stays as "1)")
|
|
148
|
+
* @param {string} text - Text to process
|
|
149
|
+
* @returns {string} - Text with contextually mirrored brackets
|
|
150
|
+
*/
|
|
151
|
+
function fixArabicTextUsingReplace(text) {
|
|
152
|
+
if (!text || typeof text !== 'string') {
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Remove leading dot if present
|
|
157
|
+
if (text.startsWith('.')) {
|
|
158
|
+
text = text.slice(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const DIGIT_OR_LTR = /[0-9A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
|
|
162
|
+
const mirrorMap = { '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<' };
|
|
163
|
+
const openBrackets = { '(': ')', '[': ']', '{': '}', '<': '>' };
|
|
164
|
+
|
|
165
|
+
// --- Pre-pass: find matched bracket pairs ---
|
|
166
|
+
// Any bracket that is part of a balanced open/close pair within the same
|
|
167
|
+
// text should NOT be mirrored, because the PDF engine will display them
|
|
168
|
+
// in visual order and mirroring would invert them.
|
|
169
|
+
let pairedIndices = new Set();
|
|
170
|
+
let stack = [];
|
|
171
|
+
for (let i = 0; i < text.length; i++) {
|
|
172
|
+
let ch = text[i];
|
|
173
|
+
if (openBrackets[ch] !== undefined) {
|
|
174
|
+
stack.push({ ch: ch, idx: i });
|
|
175
|
+
} else if (ch === ')' || ch === ']' || ch === '}' || ch === '>') {
|
|
176
|
+
// Find matching opening bracket on stack
|
|
177
|
+
for (let s = stack.length - 1; s >= 0; s--) {
|
|
178
|
+
if (openBrackets[stack[s].ch] === ch) {
|
|
179
|
+
pairedIndices.add(stack[s].idx);
|
|
180
|
+
pairedIndices.add(i);
|
|
181
|
+
stack.splice(s, 1);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let result = '';
|
|
189
|
+
for (let i = 0; i < text.length; i++) {
|
|
190
|
+
const ch = text[i];
|
|
191
|
+
if (mirrorMap[ch] !== undefined) {
|
|
192
|
+
// If this bracket is part of a balanced pair, do NOT mirror it
|
|
193
|
+
if (pairedIndices.has(i)) {
|
|
194
|
+
result += ch;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Find the previous non-space character
|
|
199
|
+
let prevChar = null;
|
|
200
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
201
|
+
if (text[j] !== ' ') {
|
|
202
|
+
prevChar = text[j];
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Find the next non-space character
|
|
207
|
+
let nextChar = null;
|
|
208
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
209
|
+
if (text[j] !== ' ') {
|
|
210
|
+
nextChar = text[j];
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If previous char is a digit or LTR letter, bracket belongs to it — don't mirror
|
|
216
|
+
// e.g. "1)" or "a)" — the bracket is part of numbering
|
|
217
|
+
if (prevChar && DIGIT_OR_LTR.test(prevChar)) {
|
|
218
|
+
result += ch;
|
|
219
|
+
}
|
|
220
|
+
// If next char is a digit or LTR letter and no RTL before, don't mirror
|
|
221
|
+
// e.g. "(Hello"
|
|
222
|
+
else if (!prevChar && nextChar && DIGIT_OR_LTR.test(nextChar)) {
|
|
223
|
+
result += ch;
|
|
224
|
+
}
|
|
225
|
+
// Otherwise mirror — bracket is in RTL context (unpaired bracket)
|
|
226
|
+
else {
|
|
227
|
+
result += mirrorMap[ch];
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
result += ch;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Apply RTL properties to a node
|
|
239
|
+
* @param {object} node - Document node
|
|
240
|
+
* @param {boolean} forceRTL - Force RTL regardless of content
|
|
241
|
+
* @returns {object} - Node with RTL properties applied
|
|
242
|
+
*/
|
|
243
|
+
function applyRTLToNode(node, forceRTL = false) {
|
|
244
|
+
if (!node || typeof node !== 'object') {
|
|
245
|
+
return node;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Determine if node should be RTL
|
|
249
|
+
let shouldBeRTL = forceRTL;
|
|
250
|
+
|
|
251
|
+
if (!forceRTL && node.text) {
|
|
252
|
+
const textStr = typeof node.text === 'string' ? node.text :
|
|
253
|
+
Array.isArray(node.text) ? node.text.join('') : '';
|
|
254
|
+
shouldBeRTL = getTextDirection(textStr) === 'rtl';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (shouldBeRTL) {
|
|
258
|
+
// Set structural RTL properties (alignment)
|
|
259
|
+
// Font is resolved later by TextInlines.measure with priority:
|
|
260
|
+
// item font > style font > defaultStyle font > auto-detect (Cairo for RTL, Roboto for LTR)
|
|
261
|
+
// Text shaping (bracket mirroring) is handled at render time in ElementWriter
|
|
262
|
+
if (!node.alignment) {
|
|
263
|
+
node.alignment = 'right';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return node;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Process table for RTL layout
|
|
272
|
+
* @param {object} tableNode - Table definition object
|
|
273
|
+
* @param {boolean} forceRTL - Force RTL layout
|
|
274
|
+
* @returns {object} - Processed table node
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* Reverse a table row for RTL layout, correctly handling colSpan groups.
|
|
278
|
+
* A colSpan cell and its trailing empty placeholders ({}) are kept together
|
|
279
|
+
* as a single logical group and reversed as a unit.
|
|
280
|
+
* @param {Array} row - Table row array
|
|
281
|
+
* @returns {Array} - Reversed row
|
|
282
|
+
*/
|
|
283
|
+
function reverseTableRow(row) {
|
|
284
|
+
// Build logical groups: each group is either a single cell or a colSpan cell + its placeholders
|
|
285
|
+
let groups = [];
|
|
286
|
+
let i = 0;
|
|
287
|
+
while (i < row.length) {
|
|
288
|
+
let cell = row[i];
|
|
289
|
+
let span = (cell && typeof cell === 'object' && cell.colSpan && cell.colSpan > 1) ? cell.colSpan : 1;
|
|
290
|
+
let group = row.slice(i, i + span);
|
|
291
|
+
groups.push(group);
|
|
292
|
+
i += span;
|
|
293
|
+
}
|
|
294
|
+
// Reverse the groups, then flatten back to a single array
|
|
295
|
+
groups.reverse();
|
|
296
|
+
let result = [];
|
|
297
|
+
for (let g = 0; g < groups.length; g++) {
|
|
298
|
+
for (let k = 0; k < groups[g].length; k++) {
|
|
299
|
+
result.push(groups[g][k]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function processRTLTable(tableNode, forceRTL = false) {
|
|
306
|
+
if (!tableNode || !tableNode.table || !tableNode.table.body) {
|
|
307
|
+
return tableNode;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Support rtl: true directly on the table object: table: { rtl: true, body: [...] }
|
|
311
|
+
// This forces RTL layout regardless of content detection
|
|
312
|
+
if (tableNode.table.rtl === true) {
|
|
313
|
+
forceRTL = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Determine if table should be RTL
|
|
317
|
+
let shouldBeRTL = forceRTL;
|
|
318
|
+
|
|
319
|
+
if (!forceRTL) {
|
|
320
|
+
// Auto-detect based on content
|
|
321
|
+
let rtlCellCount = 0;
|
|
322
|
+
let totalCells = 0;
|
|
323
|
+
|
|
324
|
+
tableNode.table.body.forEach(row => {
|
|
325
|
+
if (Array.isArray(row)) {
|
|
326
|
+
row.forEach(cell => {
|
|
327
|
+
totalCells++;
|
|
328
|
+
const cellText = typeof cell === 'string' ? cell :
|
|
329
|
+
(cell && cell.text ? (typeof cell.text === 'string' ? cell.text : String(cell.text)) : '');
|
|
330
|
+
if (containsRTL(cellText)) {
|
|
331
|
+
rtlCellCount++;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// If more than 30% of cells contain RTL content, treat as RTL table
|
|
338
|
+
shouldBeRTL = totalCells > 0 && (rtlCellCount / totalCells) >= 0.3;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (shouldBeRTL) {
|
|
342
|
+
// Mark the table as RTL for the drawing phase (TableProcessor uses this)
|
|
343
|
+
tableNode.table._rtl = true;
|
|
344
|
+
|
|
345
|
+
// Reverse table columns for RTL layout, handling colSpan correctly
|
|
346
|
+
tableNode.table.body = tableNode.table.body.map(row => {
|
|
347
|
+
if (Array.isArray(row)) {
|
|
348
|
+
return reverseTableRow(row);
|
|
349
|
+
}
|
|
350
|
+
return row;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Reverse widths if defined
|
|
354
|
+
if (tableNode.table.widths && Array.isArray(tableNode.table.widths)) {
|
|
355
|
+
tableNode.table.widths = tableNode.table.widths.slice().reverse();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Apply RTL properties to cells (skip empty span placeholders)
|
|
359
|
+
tableNode.table.body = tableNode.table.body.map(row => {
|
|
360
|
+
if (Array.isArray(row)) {
|
|
361
|
+
return row.map(cell => {
|
|
362
|
+
// Convert string cells to objects so we can set alignment
|
|
363
|
+
if (typeof cell === 'string') {
|
|
364
|
+
return { text: cell, alignment: 'right' };
|
|
365
|
+
}
|
|
366
|
+
// Skip null/undefined
|
|
367
|
+
if (!cell || typeof cell !== 'object') return cell;
|
|
368
|
+
// Skip empty span placeholders {} — they must stay empty for colSpan/rowSpan
|
|
369
|
+
if (Object.keys(cell).length === 0) return cell;
|
|
370
|
+
// Skip cells that are only span markers (e.g. {_span: true})
|
|
371
|
+
if (cell._span) return cell;
|
|
372
|
+
return applyRTLToNode(cell, true);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return row;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return tableNode;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Process list items for RTL support
|
|
384
|
+
* @param {Array} listItems - ul/ol content
|
|
385
|
+
* @param {boolean} forceRTL - Force RTL layout
|
|
386
|
+
* @returns {Array} - Processed list
|
|
387
|
+
*/
|
|
388
|
+
function processRTLList(listItems, forceRTL = false) {
|
|
389
|
+
if (!listItems || !Array.isArray(listItems)) {
|
|
390
|
+
return listItems;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return listItems.map(item => {
|
|
394
|
+
if (typeof item === 'string') {
|
|
395
|
+
const shouldBeRTL = forceRTL || getTextDirection(item) === 'rtl';
|
|
396
|
+
if (shouldBeRTL) {
|
|
397
|
+
return {
|
|
398
|
+
text: item,
|
|
399
|
+
alignment: 'right'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return item;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (typeof item === 'object') {
|
|
406
|
+
const shouldBeRTL = forceRTL || (item.text && getTextDirection(String(item.text)) === 'rtl');
|
|
407
|
+
|
|
408
|
+
if (shouldBeRTL) {
|
|
409
|
+
item = applyRTLToNode(item, true);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Process nested lists recursively
|
|
413
|
+
if (item.ul) {
|
|
414
|
+
item.ul = processRTLList(item.ul, forceRTL);
|
|
415
|
+
}
|
|
416
|
+
if (item.ol) {
|
|
417
|
+
item.ol = processRTLList(item.ol, forceRTL);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return item;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Process document element for RTL support
|
|
427
|
+
* @param {any} element - Document element
|
|
428
|
+
* @param {boolean} forceRTL - Force RTL layout
|
|
429
|
+
* @returns {any} - Processed element
|
|
430
|
+
*/
|
|
431
|
+
function processRTLElement(element, forceRTL = false) {
|
|
432
|
+
if (!element) {
|
|
433
|
+
return element;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Handle arrays
|
|
437
|
+
if (Array.isArray(element)) {
|
|
438
|
+
return element.map(item => processRTLElement(item, forceRTL));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Handle strings
|
|
442
|
+
if (typeof element === 'string') {
|
|
443
|
+
const shouldBeRTL = forceRTL || getTextDirection(element) === 'rtl';
|
|
444
|
+
if (shouldBeRTL) {
|
|
445
|
+
return {
|
|
446
|
+
text: element,
|
|
447
|
+
alignment: 'right'
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return element;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Handle objects
|
|
454
|
+
if (typeof element === 'object') {
|
|
455
|
+
// Check for explicit rtl property
|
|
456
|
+
const elementForceRTL = element.rtl === true || forceRTL;
|
|
457
|
+
|
|
458
|
+
// Process text nodes
|
|
459
|
+
if (element.text !== undefined) {
|
|
460
|
+
element = applyRTLToNode(element, elementForceRTL);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Process tables
|
|
464
|
+
if (element.table) {
|
|
465
|
+
element = processRTLTable(element, elementForceRTL);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Process lists
|
|
469
|
+
if (element.ul) {
|
|
470
|
+
element.ul = processRTLList(element.ul, elementForceRTL);
|
|
471
|
+
}
|
|
472
|
+
if (element.ol) {
|
|
473
|
+
element.ol = processRTLList(element.ol, elementForceRTL);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Process columns
|
|
477
|
+
if (element.columns && Array.isArray(element.columns)) {
|
|
478
|
+
element.columns = element.columns.map(col => processRTLElement(col, elementForceRTL));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Process stack
|
|
482
|
+
if (element.stack && Array.isArray(element.stack)) {
|
|
483
|
+
element.stack = element.stack.map(item => processRTLElement(item, elementForceRTL));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return element;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export {
|
|
491
|
+
isArabicChar,
|
|
492
|
+
isPersianChar,
|
|
493
|
+
isUrduChar,
|
|
494
|
+
isRTLChar,
|
|
495
|
+
isLTRChar,
|
|
496
|
+
containsRTL,
|
|
497
|
+
getTextDirection,
|
|
498
|
+
fixArabicTextUsingReplace,
|
|
499
|
+
applyRTLToNode,
|
|
500
|
+
processRTLTable,
|
|
501
|
+
processRTLList,
|
|
502
|
+
processRTLElement
|
|
503
|
+
};
|