@digicole/pdfmake-rtl 1.2.0 → 2.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.
Files changed (100) hide show
  1. package/.vscode/tasks.json +17 -0
  2. package/CHANGELOG.md +83 -128
  3. package/LICENSE +22 -22
  4. package/README.md +188 -681
  5. package/build/fonts/Cairo/Cairo-Black.ttf +0 -0
  6. package/build/fonts/Cairo/Cairo-Bold.ttf +0 -0
  7. package/build/fonts/Cairo/Cairo-ExtraLight.ttf +0 -0
  8. package/build/fonts/Cairo/Cairo-Light.ttf +0 -0
  9. package/build/fonts/Cairo/Cairo-Regular.ttf +0 -0
  10. package/build/fonts/Cairo/Cairo-SemiBold.ttf +0 -0
  11. package/build/fonts/Cairo.js +27 -0
  12. package/build/fonts/Roboto/Roboto-Italic.ttf +0 -0
  13. package/build/fonts/Roboto/Roboto-Medium.ttf +0 -0
  14. package/build/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
  15. package/build/fonts/Roboto/Roboto-Regular.ttf +0 -0
  16. package/build/fonts/Roboto.js +27 -0
  17. package/build/pdfmake.js +63736 -71285
  18. package/build/pdfmake.js.map +1 -1
  19. package/build/pdfmake.min.js +2 -2
  20. package/build/pdfmake.min.js.map +1 -1
  21. package/build/standard-fonts/Courier.js +27 -0
  22. package/build/standard-fonts/Helvetica.js +27 -0
  23. package/build/standard-fonts/Symbol.js +21 -0
  24. package/build/standard-fonts/Times.js +27 -0
  25. package/build/standard-fonts/ZapfDingbats.js +21 -0
  26. package/build/vfs_fonts.js +11 -7
  27. package/build-vfs.js +44 -44
  28. package/fonts/Cairo/Cairo-Black.ttf +0 -0
  29. package/fonts/Cairo/Cairo-Bold.ttf +0 -0
  30. package/fonts/Cairo/Cairo-ExtraLight.ttf +0 -0
  31. package/fonts/Cairo/Cairo-Light.ttf +0 -0
  32. package/fonts/Cairo/Cairo-Regular.ttf +0 -0
  33. package/fonts/Cairo/Cairo-SemiBold.ttf +0 -0
  34. package/fonts/Cairo.js +8 -0
  35. package/fonts/Roboto/Roboto-Italic.ttf +0 -0
  36. package/fonts/Roboto/Roboto-Medium.ttf +0 -0
  37. package/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
  38. package/fonts/Roboto/Roboto-Regular.ttf +0 -0
  39. package/fonts/Roboto.js +8 -0
  40. package/index.js +26 -26
  41. package/package.json +42 -39
  42. package/src/3rd-party/svg-to-pdfkit/LICENSE +9 -9
  43. package/src/3rd-party/svg-to-pdfkit/source.js +229 -36
  44. package/src/3rd-party/svg-to-pdfkit.js +3 -3
  45. package/src/OutputDocument.js +64 -0
  46. package/src/OutputDocumentServer.js +32 -0
  47. package/src/PDFDocument.js +174 -0
  48. package/src/PageSize.js +53 -0
  49. package/src/Renderer.js +445 -0
  50. package/src/TextBreaker.js +168 -0
  51. package/src/TextInlines.js +263 -0
  52. package/src/URLResolver.js +43 -0
  53. package/src/base.js +70 -0
  54. package/src/browser-extensions/OutputDocumentBrowser.js +80 -0
  55. package/src/browser-extensions/fonts/Cairo.js +27 -0
  56. package/src/browser-extensions/fonts/Roboto.js +27 -0
  57. package/src/browser-extensions/index.js +61 -0
  58. package/src/browser-extensions/pdfMake.js +1 -355
  59. package/src/browser-extensions/standard-fonts/Courier.js +27 -0
  60. package/src/browser-extensions/standard-fonts/Helvetica.js +27 -0
  61. package/src/browser-extensions/standard-fonts/Symbol.js +21 -0
  62. package/src/browser-extensions/standard-fonts/Times.js +27 -0
  63. package/src/browser-extensions/standard-fonts/ZapfDingbats.js +21 -0
  64. package/src/browser-extensions/virtual-fs-cjs.js +1 -0
  65. package/src/columnCalculator.js +154 -157
  66. package/src/docMeasure.js +802 -810
  67. package/src/docPreprocessor.js +306 -273
  68. package/src/documentContext.js +345 -340
  69. package/src/elementWriter.js +736 -411
  70. package/src/helpers/node.js +136 -0
  71. package/src/helpers/tools.js +44 -0
  72. package/src/helpers/variableType.js +50 -0
  73. package/src/index.js +16 -0
  74. package/src/layoutBuilder.js +1393 -1197
  75. package/src/line.js +122 -104
  76. package/src/pageElementWriter.js +187 -174
  77. package/src/printer.js +370 -727
  78. package/src/qrEnc.js +796 -791
  79. package/src/rtlUtils.js +500 -485
  80. package/src/standardPageSizes.js +52 -54
  81. package/src/styleContextStack.js +208 -138
  82. package/src/svgMeasure.js +109 -70
  83. package/src/tableLayouts.js +100 -0
  84. package/src/tableProcessor.js +620 -606
  85. package/src/textDecorator.js +175 -157
  86. package/src/virtual-fs.js +66 -0
  87. package/standard-fonts/Courier.js +8 -0
  88. package/standard-fonts/Helvetica.js +8 -0
  89. package/standard-fonts/Symbol.js +5 -0
  90. package/standard-fonts/Times.js +8 -0
  91. package/standard-fonts/ZapfDingbats.js +5 -0
  92. package/index.html +0 -396
  93. package/src/browser-extensions/URLBrowserResolver.js +0 -96
  94. package/src/browser-extensions/virtual-fs.js +0 -55
  95. package/src/fontProvider.js +0 -68
  96. package/src/helpers.js +0 -138
  97. package/src/imageMeasure.js +0 -62
  98. package/src/pdfKitEngine.js +0 -21
  99. package/src/textTools.js +0 -391
  100. package/src/traversalTracker.js +0 -47
package/src/rtlUtils.js CHANGED
@@ -1,485 +1,500 @@
1
- 'use strict';
2
-
3
- /**
4
- * RTL (Right-to-Left) utilities for handling Arabic, Persian (Farsi), and Urdu languages
5
- */
6
-
7
- // Unicode ranges for Arabic script (includes Persian and Urdu characters)
8
- var ARABIC_RANGE = [
9
- [0x0600, 0x06FF], // Arabic block
10
- [0x0750, 0x077F], // Arabic Supplement
11
- [0x08A0, 0x08FF], // Arabic Extended-A
12
- [0xFB50, 0xFDFF], // Arabic Presentation Forms-A
13
- [0xFE70, 0xFEFF] // Arabic Presentation Forms-B
14
- ];
15
-
16
- // Unicode ranges for Persian (Farsi) specific characters
17
- var PERSIAN_RANGE = [
18
- [0x06A9, 0x06AF], // Persian Kaf, Gaf
19
- [0x06C0, 0x06C3], // Persian Heh, Teh Marbuta variants
20
- [0x06CC, 0x06CE], // Persian Yeh variants
21
- [0x06D0, 0x06D5], // Persian Yeh Barree, Arabic-Indic digits
22
- [0x200C, 0x200D] // Zero Width Non-Joiner, Zero Width Joiner (used in Persian)
23
- ];
24
-
25
- // Unicode ranges for Urdu specific characters
26
- var URDU_RANGE = [
27
- [0x0679, 0x0679], // Urdu Tteh
28
- [0x067E, 0x067E], // Urdu Peh
29
- [0x0686, 0x0686], // Urdu Tcheh
30
- [0x0688, 0x0688], // Urdu Ddal
31
- [0x0691, 0x0691], // Urdu Rreh
32
- [0x0698, 0x0698], // Urdu Jeh
33
- [0x06A9, 0x06A9], // Urdu Keheh
34
- [0x06AF, 0x06AF], // Urdu Gaf
35
- [0x06BA, 0x06BA], // Urdu Noon Ghunna
36
- [0x06BE, 0x06BE], // Urdu Heh Doachashmee
37
- [0x06C1, 0x06C1], // Urdu Heh Goal
38
- [0x06D2, 0x06D2], // Urdu Yeh Barree
39
- [0x06D3, 0x06D3] // Urdu Yeh Barree with Hamza
40
- ];
41
-
42
- // Strong RTL characters (Arabic, Persian, Urdu)
43
- var RTL_CHARS = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u200C-\u200D]/;
44
-
45
- // Strong LTR characters (Latin, etc.)
46
- var LTR_CHARS = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
47
-
48
- /**
49
- * Check if a character is in Arabic script (includes Persian and Urdu)
50
- * @param {string} char - Single character to check
51
- * @return {boolean} - True if character is Arabic/Persian/Urdu
52
- */
53
- function isArabicChar(char) {
54
- var code = char.charCodeAt(0);
55
- return ARABIC_RANGE.some(function(range) {
56
- return code >= range[0] && code <= range[1];
57
- });
58
- }
59
-
60
- /**
61
- * Check if a character is in Persian (Farsi) script
62
- * @param {string} char - Single character to check
63
- * @return {boolean} - True if character is Persian
64
- */
65
- function isPersianChar(char) {
66
- var code = char.charCodeAt(0);
67
- return PERSIAN_RANGE.some(function(range) {
68
- return code >= range[0] && code <= range[1];
69
- }) || isArabicChar(char); // Persian uses Arabic base + extensions
70
- }
71
-
72
- /**
73
- * Check if a character is in Urdu script
74
- * @param {string} char - Single character to check
75
- * @return {boolean} - True if character is Urdu
76
- */
77
- function isUrduChar(char) {
78
- var code = char.charCodeAt(0);
79
- return URDU_RANGE.some(function(range) {
80
- return code >= range[0] && code <= range[1];
81
- }) || isArabicChar(char); // Urdu uses Arabic base + extensions
82
- }
83
-
84
- /**
85
- * Check if a character requires RTL rendering
86
- * @param {string} char - Single character to check
87
- * @return {boolean} - True if character requires RTL
88
- */
89
- function isRTLChar(char) {
90
- return RTL_CHARS.test(char);
91
- }
92
-
93
- /**
94
- * Check if a character is strongly LTR
95
- * @param {string} char - Single character to check
96
- * @return {boolean} - True if character is strongly LTR
97
- */
98
- function isLTRChar(char) {
99
- return LTR_CHARS.test(char);
100
- }
101
-
102
- /**
103
- * Determine the predominant text direction of a string
104
- * @param {string} text - Text to analyze
105
- * @return {string} - 'rtl', 'ltr', or 'neutral'
106
- */
107
- function getTextDirection(text) {
108
- if (!text || typeof text !== 'string') {
109
- return 'neutral';
110
- }
111
-
112
- var rtlCount = 0;
113
- var ltrCount = 0;
114
-
115
- for (var i = 0; i < text.length; i++) {
116
- var char = text.charAt(i);
117
- if (isRTLChar(char)) {
118
- rtlCount++;
119
- } else if (isLTRChar(char)) {
120
- ltrCount++;
121
- }
122
- }
123
-
124
- // If we have any strong directional characters
125
- if (rtlCount > 0 || ltrCount > 0) {
126
- if (rtlCount > ltrCount) {
127
- return 'rtl';
128
- } else if (ltrCount > rtlCount) {
129
- return 'ltr';
130
- } else {
131
- // Equal counts - slight preference for RTL if both exist
132
- return rtlCount > 0 ? 'rtl' : 'ltr';
133
- }
134
- }
135
-
136
- return 'neutral';
137
- }
138
-
139
- /**
140
- * Check if text contains any RTL characters
141
- * @param {string} text - Text to check
142
- * @return {boolean} - True if text contains RTL characters
143
- */
144
- function containsRTL(text) {
145
- if (!text || typeof text !== 'string') {
146
- return false;
147
- }
148
- return RTL_CHARS.test(text);
149
- }
150
-
151
- /**
152
- * Check if text is primarily Arabic, Persian, or Urdu
153
- * @param {string} text - Text to check
154
- * @return {boolean} - True if text is primarily Arabic/Persian/Urdu
155
- */
156
- function isArabicText(text) {
157
- if (!text || typeof text !== 'string') {
158
- return false;
159
- }
160
-
161
- var rtlCount = 0;
162
- var totalStrongChars = 0;
163
-
164
- for (var i = 0; i < text.length; i++) {
165
- var char = text.charAt(i);
166
- if (isArabicChar(char) || isPersianChar(char) || isUrduChar(char)) {
167
- rtlCount++;
168
- totalStrongChars++;
169
- } else if (isRTLChar(char) || isLTRChar(char)) {
170
- totalStrongChars++;
171
- }
172
- }
173
-
174
- // If we have any strong characters and RTL represents at least 30%
175
- // (lowered threshold for mixed text)
176
- return totalStrongChars > 0 && (rtlCount / totalStrongChars) >= 0.3;
177
- }
178
-
179
- /**
180
- * Process RTL text for proper display
181
- * For modern PDF libraries, we rely on the underlying engine for BiDi processing
182
- * We should NOT reverse word order manually - that breaks Arabic text
183
- * @param {string} text - Text to process
184
- * @return {string} - Text (unchanged for proper BiDi handling)
185
- */
186
- function reverseRTLText(text) {
187
- if (!text || typeof text !== 'string') {
188
- return text;
189
- }
190
-
191
- // DO NOT reverse Arabic text word order!
192
- // Arabic text should maintain its natural word order
193
- // Only the display direction (alignment) should be RTL
194
- // The PDF engine handles proper BiDi rendering
195
- return text;
196
- }
197
-
198
- /**
199
- * Apply RTL processing to text if needed
200
- * @param {string} text - Original text
201
- * @param {string} direction - Explicit direction override ('rtl', 'ltr', or null)
202
- * @return {Object} - { text: processedText, isRTL: boolean }
203
- */
204
- function processRTLText(text, direction) {
205
- if (!text || typeof text !== 'string' || getTextDirection(text) !== 'rtl') {
206
- return { text: text, isRTL: false };
207
- }
208
-
209
- var isRTL = false;
210
-
211
- if (direction === 'rtl') {
212
- isRTL = true;
213
- } else if (direction === 'ltr') {
214
- isRTL = false;
215
- } else {
216
- // Auto-detect direction
217
- var textDir = getTextDirection(text);
218
- isRTL = textDir === 'rtl';
219
- }
220
-
221
- // Keep original text - no word reversal needed
222
- // The PDF engine handles proper BiDi rendering
223
- return {
224
- text: text,
225
- isRTL: isRTL
226
- };
227
- }
228
-
229
- /**
230
- * Reverse table row cells for RTL layout
231
- * @param {Array} row - Table row array
232
- * @return {Array} - Reversed row array
233
- */
234
- function reverseTableRow(row) {
235
- if (!Array.isArray(row)) {
236
- return row;
237
- }
238
- return row.slice().reverse();
239
- }
240
-
241
- /**
242
- * Process table for RTL layout if supportRTL is enabled
243
- * @param {Object} tableNode - Table definition object
244
- * @return {Object} - Processed table node
245
- */
246
- function processRTLTable(tableNode) {
247
- if (!tableNode || !tableNode.supportRTL || !tableNode.table || !tableNode.table.body) {
248
- return tableNode;
249
- }
250
-
251
- // Don't clone the entire object - just modify the table data in place
252
- // Reverse each row in the table body for RTL layout
253
- tableNode.table.body = tableNode.table.body.map(function(row) {
254
- return reverseTableRow(row);
255
- });
256
-
257
- // Also reverse the widths array if it exists
258
- if (tableNode.table.widths && Array.isArray(tableNode.table.widths)) {
259
- tableNode.table.widths = tableNode.table.widths.slice().reverse();
260
- }
261
-
262
- return tableNode;
263
- }
264
-
265
- /**
266
- * Apply automatic RTL detection and formatting to any text element
267
- * @param {Object|string} element - Text element or string
268
- * @return {Object} - Enhanced element with RTL properties
269
- */
270
- function autoApplyRTL(element) {
271
- if (!element) return element;
272
-
273
- // Handle string elements
274
- if (typeof element === 'string') {
275
- var direction = getTextDirection(element);
276
- if (direction === 'rtl') {
277
- return {
278
- text: element,
279
- alignment: 'right',
280
- font: 'Nillima' // Use Arabic font for RTL text
281
- };
282
- }
283
- return element;
284
- }
285
-
286
- // Handle object elements
287
- if (typeof element === 'object' && element.text) {
288
- var textDirection = getTextDirection(element.text);
289
-
290
- if (textDirection === 'rtl') {
291
- // Auto-apply RTL properties if not already set
292
- if (!element.alignment) {
293
- element.alignment = 'right';
294
- }
295
- if (!element.font && isArabicText(element.text)) {
296
- element.font = 'Nillima';
297
- }
298
- } else if (textDirection === 'ltr') {
299
- // Auto-apply LTR properties if not already set
300
- if (!element.alignment) {
301
- element.alignment = 'left';
302
- }
303
- if (!element.font) {
304
- element.font = 'Roboto';
305
- }
306
- }
307
- }
308
-
309
- return element;
310
- }
311
-
312
- /**
313
- * Process list items for RTL support including bullet positioning
314
- * @param {Array|Object} listItems - ul/ol content
315
- * @return {Array|Object} - Processed list with RTL support
316
- */
317
- function processRTLList(listItems) {
318
- if (!listItems) return listItems;
319
-
320
- function processListItem(item) {
321
- if (typeof item === 'string') {
322
- var direction = getTextDirection(item);
323
- if (direction === 'rtl') {
324
- return {
325
- text: item,
326
- alignment: 'right',
327
- font: 'Nillima',
328
- markerColor: '#2c5282'
329
- };
330
- }
331
- return item;
332
- }
333
-
334
- if (typeof item === 'object') {
335
- // Process the main text
336
- if (item.text) {
337
- var textDirection = getTextDirection(item.text);
338
- if (textDirection === 'rtl') {
339
- if (!item.alignment) item.alignment = 'right';
340
- if (!item.font && isArabicText(item.text)) item.font = 'Nillima';
341
- if (!item.markerColor) item.markerColor = '#2c5282';
342
- }
343
- }
344
-
345
- // Process nested ul/ol recursively
346
- if (item.ul) {
347
- item.ul = processRTLList(item.ul);
348
- }
349
- if (item.ol) {
350
- item.ol = processRTLList(item.ol);
351
- }
352
- }
353
-
354
- return item;
355
- }
356
-
357
- if (Array.isArray(listItems)) {
358
- return listItems.map(processListItem);
359
- }
360
-
361
- return processListItem(listItems);
362
- }
363
-
364
- /**
365
- * Process table for automatic RTL detection and layout
366
- * @param {Object} tableNode - Table definition object
367
- * @return {Object} - Processed table node
368
- */
369
- function processAutoRTLTable(tableNode) {
370
- if (!tableNode || !tableNode.table || !tableNode.table.body) {
371
- return tableNode;
372
- }
373
-
374
- // Check if table contains RTL content
375
- var hasRTLContent = false;
376
- var rtlCellCount = 0;
377
- var totalCells = 0;
378
-
379
- tableNode.table.body.forEach(function(row) {
380
- if (Array.isArray(row)) {
381
- row.forEach(function(cell) {
382
- totalCells++;
383
- var cellText = typeof cell === 'string' ? cell : (cell && cell.text ? cell.text : '');
384
- if (containsRTL(cellText)) {
385
- rtlCellCount++;
386
- }
387
- });
388
- }
389
- });
390
-
391
- // If more than 30% of cells contain RTL content, treat as RTL table
392
- hasRTLContent = totalCells > 0 && (rtlCellCount / totalCells) >= 0.3;
393
-
394
- if (hasRTLContent) {
395
- // Reverse table columns for RTL layout
396
- tableNode.table.body = tableNode.table.body.map(function(row) {
397
- return reverseTableRow(row);
398
- });
399
-
400
- // Reverse widths if defined
401
- if (tableNode.table.widths && Array.isArray(tableNode.table.widths)) {
402
- tableNode.table.widths = tableNode.table.widths.slice().reverse();
403
- }
404
-
405
- // Auto-apply RTL styles to cells
406
- tableNode.table.body = tableNode.table.body.map(function(row) {
407
- if (Array.isArray(row)) {
408
- return row.map(function(cell) {
409
- return autoApplyRTL(cell);
410
- });
411
- }
412
- return row;
413
- });
414
- } else {
415
- // For non-RTL tables, still auto-apply font and alignment per cell
416
- tableNode.table.body = tableNode.table.body.map(function(row) {
417
- if (Array.isArray(row)) {
418
- return row.map(function(cell) {
419
- return autoApplyRTL(cell);
420
- });
421
- }
422
- return row;
423
- });
424
- }
425
-
426
- return tableNode;
427
- }
428
-
429
- /**
430
- * Process any document element for automatic RTL detection
431
- * @param {Object|Array|string} element - Document element
432
- * @return {Object|Array|string} - Processed element
433
- */
434
- function processAutoRTLElement(element) {
435
- if (!element) return element;
436
-
437
- // Handle arrays (like content arrays)
438
- if (Array.isArray(element)) {
439
- return element.map(processAutoRTLElement);
440
- }
441
-
442
- // Handle text elements
443
- if (typeof element === 'string' || (element && element.text)) {
444
- element = autoApplyRTL(element);
445
- }
446
-
447
- // Handle tables
448
- if (element && element.table) {
449
- element = processAutoRTLTable(element);
450
- }
451
-
452
- // Handle lists
453
- if (element && element.ul) {
454
- element.ul = processRTLList(element.ul);
455
- }
456
- if (element && element.ol) {
457
- element.ol = processRTLList(element.ol);
458
- }
459
-
460
- // Handle columns
461
- if (element && element.columns && Array.isArray(element.columns)) {
462
- element.columns = element.columns.map(processAutoRTLElement);
463
- }
464
-
465
- return element;
466
- }
467
-
468
- module.exports = {
469
- isArabicChar: isArabicChar,
470
- isPersianChar: isPersianChar,
471
- isUrduChar: isUrduChar,
472
- isRTLChar: isRTLChar,
473
- isLTRChar: isLTRChar,
474
- getTextDirection: getTextDirection,
475
- containsRTL: containsRTL,
476
- isArabicText: isArabicText,
477
- reverseRTLText: reverseRTLText,
478
- processRTLText: processRTLText,
479
- reverseTableRow: reverseTableRow,
480
- processRTLTable: processRTLTable,
481
- autoApplyRTL: autoApplyRTL,
482
- processRTLList: processRTLList,
483
- processAutoRTLTable: processAutoRTLTable,
484
- processAutoRTLElement: processAutoRTLElement
485
- };
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, Teh 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
+ // Reverse table columns for RTL layout, handling colSpan correctly
343
+ tableNode.table.body = tableNode.table.body.map(row => {
344
+ if (Array.isArray(row)) {
345
+ return reverseTableRow(row);
346
+ }
347
+ return row;
348
+ });
349
+
350
+ // Reverse widths if defined
351
+ if (tableNode.table.widths && Array.isArray(tableNode.table.widths)) {
352
+ tableNode.table.widths = tableNode.table.widths.slice().reverse();
353
+ }
354
+
355
+ // Apply RTL properties to cells (skip empty span placeholders)
356
+ tableNode.table.body = tableNode.table.body.map(row => {
357
+ if (Array.isArray(row)) {
358
+ return row.map(cell => {
359
+ // Convert string cells to objects so we can set alignment
360
+ if (typeof cell === 'string') {
361
+ return { text: cell, alignment: 'right' };
362
+ }
363
+ // Skip null/undefined
364
+ if (!cell || typeof cell !== 'object') return cell;
365
+ // Skip empty span placeholders {} they must stay empty for colSpan/rowSpan
366
+ if (Object.keys(cell).length === 0) return cell;
367
+ // Skip cells that are only span markers (e.g. {_span: true})
368
+ if (cell._span) return cell;
369
+ return applyRTLToNode(cell, true);
370
+ });
371
+ }
372
+ return row;
373
+ });
374
+ }
375
+
376
+ return tableNode;
377
+ }
378
+
379
+ /**
380
+ * Process list items for RTL support
381
+ * @param {Array} listItems - ul/ol content
382
+ * @param {boolean} forceRTL - Force RTL layout
383
+ * @returns {Array} - Processed list
384
+ */
385
+ function processRTLList(listItems, forceRTL = false) {
386
+ if (!listItems || !Array.isArray(listItems)) {
387
+ return listItems;
388
+ }
389
+
390
+ return listItems.map(item => {
391
+ if (typeof item === 'string') {
392
+ const shouldBeRTL = forceRTL || getTextDirection(item) === 'rtl';
393
+ if (shouldBeRTL) {
394
+ return {
395
+ text: item,
396
+ alignment: 'right'
397
+ };
398
+ }
399
+ return item;
400
+ }
401
+
402
+ if (typeof item === 'object') {
403
+ const shouldBeRTL = forceRTL || (item.text && getTextDirection(String(item.text)) === 'rtl');
404
+
405
+ if (shouldBeRTL) {
406
+ item = applyRTLToNode(item, true);
407
+ }
408
+
409
+ // Process nested lists recursively
410
+ if (item.ul) {
411
+ item.ul = processRTLList(item.ul, forceRTL);
412
+ }
413
+ if (item.ol) {
414
+ item.ol = processRTLList(item.ol, forceRTL);
415
+ }
416
+ }
417
+
418
+ return item;
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Process document element for RTL support
424
+ * @param {any} element - Document element
425
+ * @param {boolean} forceRTL - Force RTL layout
426
+ * @returns {any} - Processed element
427
+ */
428
+ function processRTLElement(element, forceRTL = false) {
429
+ if (!element) {
430
+ return element;
431
+ }
432
+
433
+ // Handle arrays
434
+ if (Array.isArray(element)) {
435
+ return element.map(item => processRTLElement(item, forceRTL));
436
+ }
437
+
438
+ // Handle strings
439
+ if (typeof element === 'string') {
440
+ const shouldBeRTL = forceRTL || getTextDirection(element) === 'rtl';
441
+ if (shouldBeRTL) {
442
+ return {
443
+ text: element,
444
+ alignment: 'right'
445
+ };
446
+ }
447
+ return element;
448
+ }
449
+
450
+ // Handle objects
451
+ if (typeof element === 'object') {
452
+ // Check for explicit rtl property
453
+ const elementForceRTL = element.rtl === true || forceRTL;
454
+
455
+ // Process text nodes
456
+ if (element.text !== undefined) {
457
+ element = applyRTLToNode(element, elementForceRTL);
458
+ }
459
+
460
+ // Process tables
461
+ if (element.table) {
462
+ element = processRTLTable(element, elementForceRTL);
463
+ }
464
+
465
+ // Process lists
466
+ if (element.ul) {
467
+ element.ul = processRTLList(element.ul, elementForceRTL);
468
+ }
469
+ if (element.ol) {
470
+ element.ol = processRTLList(element.ol, elementForceRTL);
471
+ }
472
+
473
+ // Process columns
474
+ if (element.columns && Array.isArray(element.columns)) {
475
+ element.columns = element.columns.map(col => processRTLElement(col, elementForceRTL));
476
+ }
477
+
478
+ // Process stack
479
+ if (element.stack && Array.isArray(element.stack)) {
480
+ element.stack = element.stack.map(item => processRTLElement(item, elementForceRTL));
481
+ }
482
+ }
483
+
484
+ return element;
485
+ }
486
+
487
+ export {
488
+ isArabicChar,
489
+ isPersianChar,
490
+ isUrduChar,
491
+ isRTLChar,
492
+ isLTRChar,
493
+ containsRTL,
494
+ getTextDirection,
495
+ fixArabicTextUsingReplace,
496
+ applyRTLToNode,
497
+ processRTLTable,
498
+ processRTLList,
499
+ processRTLElement
500
+ };