@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
@@ -1,411 +1,736 @@
1
- 'use strict';
2
-
3
- var Line = require('./line');
4
- var isNumber = require('./helpers').isNumber;
5
- var pack = require('./helpers').pack;
6
- var offsetVector = require('./helpers').offsetVector;
7
- var DocumentContext = require('./documentContext');
8
-
9
- /**
10
- * Creates an instance of ElementWriter - a line/vector writer, which adds
11
- * elements to current page and sets their positions based on the context
12
- */
13
- function ElementWriter(context, tracker) {
14
- this.context = context;
15
- this.contextStack = [];
16
- this.tracker = tracker;
17
- }
18
-
19
- function addPageItem(page, item, index) {
20
- if (index === null || index === undefined || index < 0 || index > page.items.length) {
21
- page.items.push(item);
22
- } else {
23
- page.items.splice(index, 0, item);
24
- }
25
- }
26
-
27
- ElementWriter.prototype.addLine = function (line, dontUpdateContextPosition, index) {
28
- var height = line.getHeight();
29
- var context = this.context;
30
- var page = context.getCurrentPage(),
31
- position = this.getCurrentPositionOnPage();
32
-
33
- if (context.availableHeight < height || !page) {
34
- return false;
35
- }
36
-
37
- line.x = context.x + (line.x || 0);
38
- line.y = context.y + (line.y || 0);
39
-
40
- this.alignLine(line);
41
-
42
- addPageItem(page, {
43
- type: 'line',
44
- item: line
45
- }, index);
46
- this.tracker.emit('lineAdded', line);
47
-
48
- if (!dontUpdateContextPosition) {
49
- context.moveDown(height);
50
- }
51
-
52
- return position;
53
- };
54
-
55
- ElementWriter.prototype.alignLine = function (line) {
56
- var width = this.context.availableWidth;
57
- var lineWidth = line.getWidth();
58
-
59
- var alignment = line.inlines && line.inlines.length > 0 && line.inlines[0].alignment;
60
- var isRTL = line.isRTL && line.isRTL();
61
-
62
- var offset = 0;
63
-
64
- // For RTL lines, we need special handling
65
- if (isRTL) {
66
- // If it's RTL and no explicit alignment, default to right
67
- if (!alignment || alignment === 'left') {
68
- alignment = 'right';
69
- }
70
-
71
- // For RTL, we need to reverse the order of inlines and adjust their positions
72
- this.adjustRTLInlines(line, width);
73
- }
74
-
75
- switch (alignment) {
76
- case 'right':
77
- offset = width - lineWidth;
78
- break;
79
- case 'center':
80
- offset = (width - lineWidth) / 2;
81
- break;
82
- }
83
-
84
- if (offset) {
85
- line.x = (line.x || 0) + offset;
86
- }
87
-
88
- if (alignment === 'justify' &&
89
- !line.newLineForced &&
90
- !line.lastLineInParagraph &&
91
- line.inlines.length > 1) {
92
- var additionalSpacing = (width - lineWidth) / (line.inlines.length - 1);
93
-
94
- for (var i = 1, l = line.inlines.length; i < l; i++) {
95
- offset = i * additionalSpacing;
96
-
97
- line.inlines[i].x += offset;
98
- line.inlines[i].justifyShift = additionalSpacing;
99
- }
100
- }
101
- };
102
-
103
- ElementWriter.prototype.addImage = function (image, index, type) {
104
- var context = this.context;
105
- var page = context.getCurrentPage(),
106
- position = this.getCurrentPositionOnPage();
107
-
108
- if (!page || (image.absolutePosition === undefined && context.availableHeight < image._height && page.items.length > 0)) {
109
- return false;
110
- }
111
-
112
- if (image._x === undefined) {
113
- image._x = image.x || 0;
114
- }
115
-
116
- image.x = context.x + image._x;
117
- image.y = context.y;
118
-
119
- this.alignImage(image);
120
-
121
- addPageItem(page, {
122
- type: type || 'image',
123
- item: image
124
- }, index);
125
-
126
- context.moveDown(image._height);
127
-
128
- return position;
129
- };
130
-
131
- ElementWriter.prototype.addSVG = function (image, index) {
132
- return this.addImage(image, index, 'svg');
133
- };
134
-
135
- ElementWriter.prototype.addQr = function (qr, index) {
136
- var context = this.context;
137
- var page = context.getCurrentPage(),
138
- position = this.getCurrentPositionOnPage();
139
-
140
- if (!page || (qr.absolutePosition === undefined && context.availableHeight < qr._height)) {
141
- return false;
142
- }
143
-
144
- if (qr._x === undefined) {
145
- qr._x = qr.x || 0;
146
- }
147
-
148
- qr.x = context.x + qr._x;
149
- qr.y = context.y;
150
-
151
- this.alignImage(qr);
152
-
153
- for (var i = 0, l = qr._canvas.length; i < l; i++) {
154
- var vector = qr._canvas[i];
155
- vector.x += qr.x;
156
- vector.y += qr.y;
157
- this.addVector(vector, true, true, index);
158
- }
159
-
160
- context.moveDown(qr._height);
161
-
162
- return position;
163
- };
164
-
165
- ElementWriter.prototype.alignImage = function (image) {
166
- var width = this.context.availableWidth;
167
- var imageWidth = image._minWidth;
168
- var offset = 0;
169
- switch (image._alignment) {
170
- case 'right':
171
- offset = width - imageWidth;
172
- break;
173
- case 'center':
174
- offset = (width - imageWidth) / 2;
175
- break;
176
- }
177
-
178
- if (offset) {
179
- image.x = (image.x || 0) + offset;
180
- }
181
- };
182
-
183
- ElementWriter.prototype.alignCanvas = function (node) {
184
- var width = this.context.availableWidth;
185
- var canvasWidth = node._minWidth;
186
- var offset = 0;
187
- switch (node._alignment) {
188
- case 'right':
189
- offset = width - canvasWidth;
190
- break;
191
- case 'center':
192
- offset = (width - canvasWidth) / 2;
193
- break;
194
- }
195
- if (offset) {
196
- node.canvas.forEach(function (vector) {
197
- offsetVector(vector, offset, 0);
198
- });
199
- }
200
- };
201
-
202
- ElementWriter.prototype.addVector = function (vector, ignoreContextX, ignoreContextY, index, forcePage) {
203
- var context = this.context;
204
- var page = context.getCurrentPage();
205
- if (isNumber(forcePage)) {
206
- page = context.pages[forcePage];
207
- }
208
- var position = this.getCurrentPositionOnPage();
209
-
210
- if (page) {
211
- offsetVector(vector, ignoreContextX ? 0 : context.x, ignoreContextY ? 0 : context.y);
212
- addPageItem(page, {
213
- type: 'vector',
214
- item: vector
215
- }, index);
216
- return position;
217
- }
218
- };
219
-
220
- ElementWriter.prototype.beginClip = function (width, height) {
221
- var ctx = this.context;
222
- var page = ctx.getCurrentPage();
223
- page.items.push({
224
- type: 'beginClip',
225
- item: { x: ctx.x, y: ctx.y, width: width, height: height }
226
- });
227
- return true;
228
- };
229
-
230
- ElementWriter.prototype.endClip = function () {
231
- var ctx = this.context;
232
- var page = ctx.getCurrentPage();
233
- page.items.push({
234
- type: 'endClip'
235
- });
236
- return true;
237
- };
238
-
239
- /**
240
- * Adjust RTL inline positioning
241
- * @param {Line} line - Line containing RTL text
242
- * @param {number} availableWidth - Available width for the line
243
- */
244
- ElementWriter.prototype.adjustRTLInlines = function (line, availableWidth) {
245
- if (!line.inlines || line.inlines.length === 0) {
246
- return;
247
- }
248
-
249
- // For RTL text, we need to reverse the visual order of inlines
250
- // and recalculate their positions from right to left
251
- var rtlInlines = [];
252
- var ltrInlines = [];
253
- var neutralInlines = [];
254
-
255
- // Separate RTL, LTR, and neutral inlines
256
- line.inlines.forEach(function(inline) {
257
- if (inline.isRTL || inline.direction === 'rtl') {
258
- rtlInlines.push(inline);
259
- } else if (inline.direction === 'ltr') {
260
- ltrInlines.push(inline);
261
- } else {
262
- neutralInlines.push(inline);
263
- }
264
- });
265
-
266
- // If we have RTL inlines, reverse their order and recalculate positions
267
- if (rtlInlines.length > 0) {
268
- // Reverse the order of RTL inlines for proper display
269
- rtlInlines.reverse();
270
-
271
- // Recalculate x positions from right to left
272
- var currentX = 0;
273
- var reorderedInlines = [];
274
-
275
- // Add LTR inlines first (if any)
276
- ltrInlines.forEach(function(inline) {
277
- inline.x = currentX;
278
- currentX += inline.width;
279
- reorderedInlines.push(inline);
280
- });
281
-
282
- // Add neutral inlines
283
- neutralInlines.forEach(function(inline) {
284
- inline.x = currentX;
285
- currentX += inline.width;
286
- reorderedInlines.push(inline);
287
- });
288
-
289
- // Add RTL inlines (already reversed)
290
- rtlInlines.forEach(function(inline) {
291
- inline.x = currentX;
292
- currentX += inline.width;
293
- reorderedInlines.push(inline);
294
- });
295
-
296
- // Replace the line's inlines with the reordered ones
297
- line.inlines = reorderedInlines;
298
- }
299
- };
300
-
301
- function cloneLine(line) {
302
- var result = new Line(line.maxWidth);
303
-
304
- for (var key in line) {
305
- if (line.hasOwnProperty(key)) {
306
- result[key] = line[key];
307
- }
308
- }
309
-
310
- return result;
311
- }
312
-
313
- ElementWriter.prototype.addFragment = function (block, useBlockXOffset, useBlockYOffset, dontUpdateContextPosition) {
314
- var ctx = this.context;
315
- var page = ctx.getCurrentPage();
316
-
317
- if (!useBlockXOffset && block.height > ctx.availableHeight) {
318
- return false;
319
- }
320
-
321
- block.items.forEach(function (item) {
322
- switch (item.type) {
323
- case 'line':
324
- var l = cloneLine(item.item);
325
-
326
- if (l._node) {
327
- l._node.positions[0].pageNumber = ctx.page + 1;
328
- }
329
- l.x = (l.x || 0) + (useBlockXOffset ? (block.xOffset || 0) : ctx.x);
330
- l.y = (l.y || 0) + (useBlockYOffset ? (block.yOffset || 0) : ctx.y);
331
-
332
- page.items.push({
333
- type: 'line',
334
- item: l
335
- });
336
- break;
337
-
338
- case 'vector':
339
- var v = pack(item.item);
340
-
341
- offsetVector(v, useBlockXOffset ? (block.xOffset || 0) : ctx.x, useBlockYOffset ? (block.yOffset || 0) : ctx.y);
342
- if (v._isFillColorFromUnbreakable) {
343
- // If the item is a fillColor from an unbreakable block
344
- // We have to add it at the beginning of the items body array of the page
345
- delete v._isFillColorFromUnbreakable;
346
- const endOfBackgroundItemsIndex = ctx.backgroundLength[ctx.page];
347
- page.items.splice(endOfBackgroundItemsIndex, 0, {
348
- type: 'vector',
349
- item: v
350
- });
351
- } else {
352
- page.items.push({
353
- type: 'vector',
354
- item: v
355
- });
356
- }
357
- break;
358
-
359
- case 'image':
360
- case 'svg':
361
- var img = pack(item.item);
362
-
363
- img.x = (img.x || 0) + (useBlockXOffset ? (block.xOffset || 0) : ctx.x);
364
- img.y = (img.y || 0) + (useBlockYOffset ? (block.yOffset || 0) : ctx.y);
365
-
366
- page.items.push({
367
- type: item.type,
368
- item: img
369
- });
370
- break;
371
- }
372
- });
373
-
374
- if (!dontUpdateContextPosition) {
375
- ctx.moveDown(block.height);
376
- }
377
-
378
- return true;
379
- };
380
-
381
- /**
382
- * Pushes the provided context onto the stack or creates a new one
383
- *
384
- * pushContext(context) - pushes the provided context and makes it current
385
- * pushContext(width, height) - creates and pushes a new context with the specified width and height
386
- * pushContext() - creates a new context for unbreakable blocks (with current availableWidth and full-page-height)
387
- */
388
- ElementWriter.prototype.pushContext = function (contextOrWidth, height) {
389
- if (contextOrWidth === undefined) {
390
- height = this.context.getCurrentPage().height - this.context.pageMargins.top - this.context.pageMargins.bottom;
391
- contextOrWidth = this.context.availableWidth;
392
- }
393
-
394
- if (isNumber(contextOrWidth)) {
395
- contextOrWidth = new DocumentContext({ width: contextOrWidth, height: height }, { left: 0, right: 0, top: 0, bottom: 0 });
396
- }
397
-
398
- this.contextStack.push(this.context);
399
- this.context = contextOrWidth;
400
- };
401
-
402
- ElementWriter.prototype.popContext = function () {
403
- this.context = this.contextStack.pop();
404
- };
405
-
406
- ElementWriter.prototype.getCurrentPositionOnPage = function () {
407
- return (this.contextStack[0] || this.context).getCurrentPosition();
408
- };
409
-
410
-
411
- module.exports = ElementWriter;
1
+ import { isNumber } from './helpers/variableType';
2
+ import { pack, offsetVector } from './helpers/tools';
3
+ import DocumentContext from './DocumentContext';
4
+ import { EventEmitter } from 'events';
5
+ import { containsRTL, fixArabicTextUsingReplace } from './rtlUtils';
6
+
7
+ /**
8
+ * A line/vector writer, which adds elements to current page and sets
9
+ * their positions based on the context
10
+ */
11
+ class ElementWriter extends EventEmitter {
12
+
13
+ /**
14
+ * @param {DocumentContext} context
15
+ */
16
+ constructor(context) {
17
+ super();
18
+ this._context = context;
19
+ this.contextStack = [];
20
+ }
21
+
22
+ /**
23
+ * @returns {DocumentContext}
24
+ */
25
+ context() {
26
+ return this._context;
27
+ }
28
+
29
+ addLine(line, dontUpdateContextPosition, index) {
30
+ let height = line.getHeight();
31
+ let context = this.context();
32
+ let page = context.getCurrentPage();
33
+ let position = this.getCurrentPositionOnPage();
34
+
35
+ if (context.availableHeight < height || !page) {
36
+ return false;
37
+ }
38
+
39
+ line.x = context.x + (line.x || 0);
40
+ line.y = context.y + (line.y || 0);
41
+
42
+ this.alignLine(line);
43
+
44
+ addPageItem(page, {
45
+ type: 'line',
46
+ item: line
47
+ }, index);
48
+ this.emit('lineAdded', line);
49
+
50
+ if (!dontUpdateContextPosition) {
51
+ context.moveDown(height);
52
+ }
53
+
54
+ return position;
55
+ }
56
+
57
+ alignLine(line) {
58
+ // Skip alignment for list marker lines - their position is manually
59
+ // calculated in processList and should not be affected by inherited
60
+ // alignment or direction properties
61
+ if (line.listMarker) {
62
+ return;
63
+ }
64
+
65
+ let width = this.context().availableWidth;
66
+ let lineWidth = line.getWidth();
67
+
68
+ let alignment = line.inlines && line.inlines.length > 0 && line.inlines[0].alignment;
69
+ let isRTL = line.isRTL && line.isRTL();
70
+
71
+ let offset = 0;
72
+
73
+ // For RTL lines, apply special handling
74
+ if (isRTL) {
75
+ if (!alignment || alignment === 'left') {
76
+ alignment = 'right';
77
+ }
78
+ this.adjustRTLInlines(line, width);
79
+ }
80
+
81
+ switch (alignment) {
82
+ case 'right':
83
+ offset = width - lineWidth;
84
+ break;
85
+ case 'center':
86
+ offset = (width - lineWidth) / 2;
87
+ break;
88
+ }
89
+
90
+ if (offset) {
91
+ line.x = (line.x || 0) + offset;
92
+ }
93
+
94
+ if (alignment === 'justify' &&
95
+ !line.newLineForced &&
96
+ !line.lastLineInParagraph &&
97
+ line.inlines.length > 1) {
98
+ let additionalSpacing = (width - lineWidth) / (line.inlines.length - 1);
99
+
100
+ for (let i = 1, l = line.inlines.length; i < l; i++) {
101
+ offset = i * additionalSpacing;
102
+
103
+ line.inlines[i].x += offset;
104
+ line.inlines[i].justifyShift = additionalSpacing;
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Adjust RTL inline positioning - reorder inlines for proper visual display.
111
+ *
112
+ * Implements a simplified Unicode Bidirectional Algorithm (UBA):
113
+ * 0. Pre-split inlines at RTL↔neutral boundaries so punctuation like "/" between
114
+ * Arabic and Latin text is treated as a separate neutral inline
115
+ * 1. Classify each inline as RTL, LTR, or neutral
116
+ * 2. Group consecutive same-direction inlines into directional "runs"
117
+ * 3. Resolve neutral runs: attach to adjacent run based on surrounding context
118
+ * 4. Reverse the order of runs (base direction is RTL)
119
+ * 5. Within each LTR run keep order; within each RTL run reverse inlines
120
+ * 6. Recalculate x positions
121
+ *
122
+ * This preserves the positional relationship between adjacent text and
123
+ * punctuation (e.g. "العربية/arabic" keeps the "/" attached correctly).
124
+ *
125
+ * @param {object} line - Line containing RTL text
126
+ */
127
+ adjustRTLInlines(line) {
128
+ if (!line.inlines || line.inlines.length === 0) {
129
+ return;
130
+ }
131
+
132
+ const LTR_REGEX = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
133
+ const NUMBER_PUNCTUATION_REGEX = /^(\d+)([.:/\-)(]+)(\s*)$/;
134
+ // Characters that are "boundary neutral" — separators/punctuation between scripts
135
+ const BOUNDARY_NEUTRAL = /[\/\\\-()[\]{}<>:;.,!?@#$%^&*_=+|~`'"،؛؟\s]/;
136
+
137
+ // --- Step 0: Pre-split inlines at RTL↔neutral and LTR↔neutral boundaries ---
138
+ // e.g. "العربية/" → ["العربية", "/"] and "hello-" → ["hello", "-"]
139
+ let splitInlines = [];
140
+ line.inlines.forEach(inline => {
141
+ let text = inline.text;
142
+ if (!text || text.length === 0) {
143
+ splitInlines.push(inline);
144
+ return;
145
+ }
146
+
147
+ let hasStrongRTL = containsRTL(text);
148
+ let hasStrongLTR = LTR_REGEX.test(text);
149
+
150
+ // Only split if the inline has strong directional chars AND trailing/leading neutrals
151
+ if ((hasStrongRTL || hasStrongLTR) && text.length > 1) {
152
+ // Split trailing neutral characters (e.g. "العربية/" → "العربية" + "/")
153
+ let trailingStart = text.length;
154
+ while (trailingStart > 0) {
155
+ let ch = text[trailingStart - 1];
156
+ if (BOUNDARY_NEUTRAL.test(ch) && !containsRTL(ch) && !LTR_REGEX.test(ch)) {
157
+ trailingStart--;
158
+ } else {
159
+ break;
160
+ }
161
+ }
162
+
163
+ // Split leading neutral characters (e.g. "/العربية" → "/" + "العربية")
164
+ let leadingEnd = 0;
165
+ while (leadingEnd < text.length) {
166
+ let ch = text[leadingEnd];
167
+ if (BOUNDARY_NEUTRAL.test(ch) && !containsRTL(ch) && !LTR_REGEX.test(ch)) {
168
+ leadingEnd++;
169
+ } else {
170
+ break;
171
+ }
172
+ }
173
+
174
+ // Only split if there's a meaningful core left
175
+ if ((leadingEnd > 0 || trailingStart < text.length) && leadingEnd < trailingStart) {
176
+ let leadingText = text.slice(0, leadingEnd);
177
+ let coreText = text.slice(leadingEnd, trailingStart);
178
+ let trailingText = text.slice(trailingStart);
179
+
180
+ if (leadingText) {
181
+ let clone = Object.assign({}, inline);
182
+ clone.text = leadingText;
183
+ clone.width = inline.font ? inline.font.widthOfString(leadingText, inline.fontSize, inline.fontFeatures) + ((inline.characterSpacing || 0) * (leadingText.length - 1)) : 0;
184
+ clone._isSplit = true;
185
+ splitInlines.push(clone);
186
+ }
187
+
188
+ if (coreText) {
189
+ let clone = Object.assign({}, inline);
190
+ clone.text = coreText;
191
+ clone.width = inline.font ? inline.font.widthOfString(coreText, inline.fontSize, inline.fontFeatures) + ((inline.characterSpacing || 0) * (coreText.length - 1)) : 0;
192
+ clone._isSplit = true;
193
+ splitInlines.push(clone);
194
+ }
195
+
196
+ if (trailingText) {
197
+ let clone = Object.assign({}, inline);
198
+ clone.text = trailingText;
199
+ clone.width = inline.font ? inline.font.widthOfString(trailingText, inline.fontSize, inline.fontFeatures) + ((inline.characterSpacing || 0) * (trailingText.length - 1)) : 0;
200
+ clone._isSplit = true;
201
+ splitInlines.push(clone);
202
+ }
203
+ } else {
204
+ splitInlines.push(inline);
205
+ }
206
+ } else {
207
+ splitInlines.push(inline);
208
+ }
209
+ });
210
+
211
+ // --- Step 1: Classify each inline ---
212
+ const classified = splitInlines.map(inline => {
213
+ let hasStrongLTR = LTR_REGEX.test(inline.text);
214
+ let hasStrongRTL = containsRTL(inline.text);
215
+ let dir;
216
+ if (hasStrongRTL && hasStrongLTR) {
217
+ // Mixed — treat as RTL (predominant for RTL lines)
218
+ dir = 'rtl';
219
+ } else if (hasStrongRTL) {
220
+ dir = 'rtl';
221
+ } else if (hasStrongLTR) {
222
+ dir = 'ltr';
223
+ } else {
224
+ dir = 'neutral'; // punctuation, digits, spaces only
225
+ }
226
+ return { inline, dir };
227
+ });
228
+
229
+ // --- Step 2: Build directional runs (groups of consecutive same-direction) ---
230
+ let runs = [];
231
+ let currentRun = null;
232
+ classified.forEach(item => {
233
+ if (!currentRun || currentRun.dir !== item.dir) {
234
+ currentRun = { dir: item.dir, inlines: [] };
235
+ runs.push(currentRun);
236
+ }
237
+ currentRun.inlines.push(item.inline);
238
+ });
239
+
240
+ // --- Step 3: Resolve neutral runs ---
241
+ // Step 3a: Bracket pair resolution (UBA rule N0).
242
+ // Find matching bracket pairs across runs. If the content between
243
+ // a "(" neutral run and a ")" neutral run is predominantly one direction,
244
+ // merge the opening bracket, content, and closing bracket into that direction.
245
+ const OPEN_BRACKETS = /[(\[{<]/;
246
+ const CLOSE_BRACKETS = /[)\]}>]/;
247
+ const BRACKET_MATCH = { '(': ')', '[': ']', '{': '}', '<': '>' };
248
+
249
+ for (let i = 0; i < runs.length; i++) {
250
+ if (runs[i].dir !== 'neutral') continue;
251
+
252
+ // Check if this neutral run contains an opening bracket
253
+ let openBracket = null;
254
+ for (let k = 0; k < runs[i].inlines.length; k++) {
255
+ let txt = runs[i].inlines[k].text.trim();
256
+ if (OPEN_BRACKETS.test(txt)) {
257
+ openBracket = txt.match(OPEN_BRACKETS)[0];
258
+ break;
259
+ }
260
+ }
261
+ if (!openBracket) continue;
262
+
263
+ let closeBracket = BRACKET_MATCH[openBracket];
264
+
265
+ // Search forward for the matching closing bracket
266
+ for (let j = i + 1; j < runs.length; j++) {
267
+ if (runs[j].dir === 'neutral') {
268
+ let hasClose = false;
269
+ for (let k = 0; k < runs[j].inlines.length; k++) {
270
+ if (runs[j].inlines[k].text.indexOf(closeBracket) >= 0) {
271
+ hasClose = true;
272
+ break;
273
+ }
274
+ }
275
+ if (!hasClose) continue;
276
+
277
+ // Found matching close bracket at run j.
278
+ // Determine predominant direction of content between i and j
279
+ let innerLtr = 0, innerRtl = 0;
280
+ for (let m = i + 1; m < j; m++) {
281
+ if (runs[m].dir === 'ltr') innerLtr += runs[m].inlines.length;
282
+ else if (runs[m].dir === 'rtl') innerRtl += runs[m].inlines.length;
283
+ }
284
+
285
+ // Resolve bracket pair to inner content direction, or LTR if neutral-only
286
+ let pairDir = innerLtr >= innerRtl ? 'ltr' : 'rtl';
287
+
288
+ // Set the direction for the opening and closing bracket runs
289
+ runs[i].dir = pairDir;
290
+ runs[j].dir = pairDir;
291
+ break; // only match the first closing bracket
292
+ }
293
+ }
294
+ }
295
+
296
+ // Step 3b: General neutral resolution.
297
+ // A neutral run takes the direction of its neighbors. If both neighbors
298
+ // agree, use that direction. If they disagree, use the base direction (RTL).
299
+ // If only one neighbor exists, use that neighbor's resolved direction.
300
+ for (let i = 0; i < runs.length; i++) {
301
+ if (runs[i].dir !== 'neutral') continue;
302
+
303
+ let prevDir = null;
304
+ for (let j = i - 1; j >= 0; j--) {
305
+ if (runs[j].dir !== 'neutral') { prevDir = runs[j].dir; break; }
306
+ }
307
+ let nextDir = null;
308
+ for (let j = i + 1; j < runs.length; j++) {
309
+ if (runs[j].dir !== 'neutral') { nextDir = runs[j].dir; break; }
310
+ }
311
+
312
+ if (prevDir && nextDir) {
313
+ runs[i].dir = (prevDir === nextDir) ? prevDir : 'rtl';
314
+ } else if (prevDir) {
315
+ runs[i].dir = prevDir;
316
+ } else if (nextDir) {
317
+ runs[i].dir = nextDir;
318
+ } else {
319
+ runs[i].dir = 'rtl'; // all neutral → base direction
320
+ }
321
+ }
322
+
323
+ // --- Step 3c: Merge adjacent runs that now share the same direction ---
324
+ let merged = [runs[0]];
325
+ for (let i = 1; i < runs.length; i++) {
326
+ let last = merged[merged.length - 1];
327
+ if (last.dir === runs[i].dir) {
328
+ last.inlines = last.inlines.concat(runs[i].inlines);
329
+ } else {
330
+ merged.push(runs[i]);
331
+ }
332
+ }
333
+ runs = merged;
334
+
335
+ // --- Step 4: Reverse run order (base direction is RTL) ---
336
+ runs.reverse();
337
+
338
+ // --- Step 5: Within each RTL run, reverse the inline order ---
339
+ runs.forEach(run => {
340
+ if (run.dir === 'rtl') {
341
+ run.inlines.reverse();
342
+ }
343
+ // LTR runs keep their original inline order
344
+ });
345
+
346
+ // --- Step 6: Flatten, apply bracket mirroring, recalculate x positions ---
347
+ // UBA Rule L4: after reordering, mirror bracket glyphs in RTL context
348
+ let reorderedInlines = [];
349
+ let currentX = 0;
350
+ const MIRROR_MAP = { '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<' };
351
+
352
+ runs.forEach(run => {
353
+ run.inlines.forEach(inline => {
354
+ // Apply context-aware bracket mirroring for RTL inlines that contain Arabic text
355
+ if (run.dir === 'rtl' && containsRTL(inline.text)) {
356
+ inline.text = fixArabicTextUsingReplace(inline.text);
357
+ }
358
+
359
+ // UBA Rule L4: Mirror standalone bracket characters in RTL runs.
360
+ // After Step 5 reversed the inline order, brackets like "(" and ")"
361
+ // are in swapped positions. Mirroring the glyph restores correct visuals.
362
+ // e.g. reversed ")" at position 0 → mirror to "(" → visually correct.
363
+ if (run.dir === 'rtl' && !containsRTL(inline.text) && !LTR_REGEX.test(inline.text)) {
364
+ let mirrored = '';
365
+ for (let c = 0; c < inline.text.length; c++) {
366
+ let ch = inline.text[c];
367
+ mirrored += MIRROR_MAP[ch] !== undefined ? MIRROR_MAP[ch] : ch;
368
+ }
369
+ inline.text = mirrored;
370
+ }
371
+
372
+ // Fix number+punctuation rendering in RTL context
373
+ if (run.dir === 'rtl' && NUMBER_PUNCTUATION_REGEX.test(inline.text)) {
374
+ inline.text = inline.text.replace(NUMBER_PUNCTUATION_REGEX, ' $3$2$1');
375
+ }
376
+
377
+ inline.x = currentX;
378
+ currentX += inline.width;
379
+ reorderedInlines.push(inline);
380
+ });
381
+ });
382
+
383
+ line.inlines = reorderedInlines;
384
+ }
385
+
386
+ addImage(image, index) {
387
+ let context = this.context();
388
+ let page = context.getCurrentPage();
389
+ let position = this.getCurrentPositionOnPage();
390
+
391
+ if (!page || (image.absolutePosition === undefined && context.availableHeight < image._height && page.items.length > 0)) {
392
+ return false;
393
+ }
394
+
395
+ if (image._x === undefined) {
396
+ image._x = image.x || 0;
397
+ }
398
+
399
+ image.x = context.x + image._x;
400
+ image.y = context.y;
401
+
402
+ this.alignImage(image);
403
+
404
+ addPageItem(page, {
405
+ type: 'image',
406
+ item: image
407
+ }, index);
408
+
409
+ context.moveDown(image._height);
410
+
411
+ return position;
412
+ }
413
+
414
+ addCanvas(node, index) {
415
+ let context = this.context();
416
+ let page = context.getCurrentPage();
417
+ let positions = [];
418
+ let height = node._minHeight;
419
+
420
+ if (!page || (node.absolutePosition === undefined && context.availableHeight < height)) {
421
+ // TODO: support for canvas larger than a page
422
+ // TODO: support for other overflow methods
423
+
424
+ return false;
425
+ }
426
+
427
+ this.alignCanvas(node);
428
+
429
+ node.canvas.forEach(function (vector) {
430
+ let position = this.addVector(vector, false, false, index);
431
+ positions.push(position);
432
+ if (index !== undefined) {
433
+ index++;
434
+ }
435
+ }, this);
436
+
437
+ context.moveDown(height);
438
+
439
+ return positions;
440
+ }
441
+
442
+ addSVG(image, index) {
443
+ // TODO: same as addImage
444
+ let context = this.context();
445
+ let page = context.getCurrentPage();
446
+ let position = this.getCurrentPositionOnPage();
447
+
448
+ if (!page || (image.absolutePosition === undefined && context.availableHeight < image._height && page.items.length > 0)) {
449
+ return false;
450
+ }
451
+
452
+ if (image._x === undefined) {
453
+ image._x = image.x || 0;
454
+ }
455
+
456
+ image.x = context.x + image._x;
457
+ image.y = context.y;
458
+
459
+ this.alignImage(image);
460
+
461
+ addPageItem(page, {
462
+ type: 'svg',
463
+ item: image
464
+ }, index);
465
+
466
+ context.moveDown(image._height);
467
+
468
+ return position;
469
+ }
470
+
471
+ addQr(qr, index) {
472
+ let context = this.context();
473
+ let page = context.getCurrentPage();
474
+ let position = this.getCurrentPositionOnPage();
475
+
476
+ if (!page || (qr.absolutePosition === undefined && context.availableHeight < qr._height)) {
477
+ return false;
478
+ }
479
+
480
+ if (qr._x === undefined) {
481
+ qr._x = qr.x || 0;
482
+ }
483
+
484
+ qr.x = context.x + qr._x;
485
+ qr.y = context.y;
486
+
487
+ this.alignImage(qr);
488
+
489
+ for (let i = 0, l = qr._canvas.length; i < l; i++) {
490
+ let vector = qr._canvas[i];
491
+ vector.x += qr.x;
492
+ vector.y += qr.y;
493
+ this.addVector(vector, true, true, index);
494
+ }
495
+
496
+ context.moveDown(qr._height);
497
+
498
+ return position;
499
+ }
500
+
501
+ addAttachment(attachment, index) {
502
+ let context = this.context();
503
+ let page = context.getCurrentPage();
504
+ let position = this.getCurrentPositionOnPage();
505
+
506
+ if (!page || (attachment.absolutePosition === undefined && context.availableHeight < attachment._height && page.items.length > 0)) {
507
+ return false;
508
+ }
509
+
510
+ if (attachment._x === undefined) {
511
+ attachment._x = attachment.x || 0;
512
+ }
513
+
514
+ attachment.x = context.x + attachment._x;
515
+ attachment.y = context.y;
516
+
517
+ addPageItem(page, {
518
+ type: 'attachment',
519
+ item: attachment
520
+ }, index);
521
+
522
+ context.moveDown(attachment._height);
523
+
524
+ return position;
525
+ }
526
+
527
+ alignImage(image) {
528
+ let width = this.context().availableWidth;
529
+ let imageWidth = image._minWidth;
530
+ let offset = 0;
531
+ switch (image._alignment) {
532
+ case 'right':
533
+ offset = width - imageWidth;
534
+ break;
535
+ case 'center':
536
+ offset = (width - imageWidth) / 2;
537
+ break;
538
+ }
539
+
540
+ if (offset) {
541
+ image.x = (image.x || 0) + offset;
542
+ }
543
+ }
544
+
545
+ alignCanvas(node) {
546
+ let width = this.context().availableWidth;
547
+ let canvasWidth = node._minWidth;
548
+ let offset = 0;
549
+ switch (node._alignment) {
550
+ case 'right':
551
+ offset = width - canvasWidth;
552
+ break;
553
+ case 'center':
554
+ offset = (width - canvasWidth) / 2;
555
+ break;
556
+ }
557
+ if (offset) {
558
+ node.canvas.forEach(vector => {
559
+ offsetVector(vector, offset, 0);
560
+ });
561
+ }
562
+ }
563
+
564
+ addVector(vector, ignoreContextX, ignoreContextY, index, forcePage) {
565
+ let context = this.context();
566
+ let page = context.getCurrentPage();
567
+ if (isNumber(forcePage)) {
568
+ page = context.pages[forcePage];
569
+ }
570
+ let position = this.getCurrentPositionOnPage();
571
+
572
+ if (page) {
573
+ offsetVector(vector, ignoreContextX ? 0 : context.x, ignoreContextY ? 0 : context.y);
574
+ addPageItem(page, {
575
+ type: 'vector',
576
+ item: vector
577
+ }, index);
578
+ return position;
579
+ }
580
+ }
581
+
582
+ beginClip(width, height) {
583
+ let ctx = this.context();
584
+ let page = ctx.getCurrentPage();
585
+ page.items.push({
586
+ type: 'beginClip',
587
+ item: { x: ctx.x, y: ctx.y, width: width, height: height }
588
+ });
589
+ return true;
590
+ }
591
+
592
+ endClip() {
593
+ let ctx = this.context();
594
+ let page = ctx.getCurrentPage();
595
+ page.items.push({
596
+ type: 'endClip'
597
+ });
598
+ return true;
599
+ }
600
+
601
+ beginVerticalAlignment(verticalAlignment) {
602
+ let page = this.context().getCurrentPage();
603
+ let item = {
604
+ type: 'beginVerticalAlignment',
605
+ item: { verticalAlignment: verticalAlignment }
606
+ };
607
+ page.items.push(item);
608
+ return item;
609
+ }
610
+
611
+ endVerticalAlignment(verticalAlignment) {
612
+ let page = this.context().getCurrentPage();
613
+ let item = {
614
+ type: 'endVerticalAlignment',
615
+ item: { verticalAlignment: verticalAlignment }
616
+ };
617
+ page.items.push(item);
618
+ return item;
619
+ }
620
+
621
+ addFragment(block, useBlockXOffset, useBlockYOffset, dontUpdateContextPosition) {
622
+ let ctx = this.context();
623
+ let page = ctx.getCurrentPage();
624
+
625
+ if (!useBlockXOffset && block.height > ctx.availableHeight) {
626
+ return false;
627
+ }
628
+
629
+ block.items.forEach(item => {
630
+ switch (item.type) {
631
+ case 'line':
632
+ var l = item.item.clone();
633
+
634
+ if (l._node) {
635
+ l._node.positions[0].pageNumber = ctx.page + 1;
636
+ }
637
+ l.x = (l.x || 0) + (useBlockXOffset ? (block.xOffset || 0) : ctx.x);
638
+ l.y = (l.y || 0) + (useBlockYOffset ? (block.yOffset || 0) : ctx.y);
639
+
640
+ page.items.push({
641
+ type: 'line',
642
+ item: l
643
+ });
644
+ break;
645
+
646
+ case 'vector':
647
+ var v = pack(item.item);
648
+
649
+ offsetVector(v, useBlockXOffset ? (block.xOffset || 0) : ctx.x, useBlockYOffset ? (block.yOffset || 0) : ctx.y);
650
+ if (v._isFillColorFromUnbreakable) {
651
+ // If the item is a fillColor from an unbreakable block
652
+ // We have to add it at the beginning of the items body array of the page
653
+ delete v._isFillColorFromUnbreakable;
654
+ const endOfBackgroundItemsIndex = ctx.backgroundLength[ctx.page];
655
+ page.items.splice(endOfBackgroundItemsIndex, 0, {
656
+ type: 'vector',
657
+ item: v
658
+ });
659
+ } else {
660
+ page.items.push({
661
+ type: 'vector',
662
+ item: v
663
+ });
664
+ }
665
+ break;
666
+
667
+ case 'image':
668
+ case 'svg':
669
+ case 'beginClip':
670
+ case 'endClip':
671
+ case 'beginVerticalAlignment':
672
+ case 'endVerticalAlignment':
673
+ var img = pack(item.item);
674
+
675
+ img.x = (img.x || 0) + (useBlockXOffset ? (block.xOffset || 0) : ctx.x);
676
+ img.y = (img.y || 0) + (useBlockYOffset ? (block.yOffset || 0) : ctx.y);
677
+
678
+ page.items.push({
679
+ type: item.type,
680
+ item: img
681
+ });
682
+ break;
683
+ }
684
+ });
685
+
686
+ if (!dontUpdateContextPosition) {
687
+ ctx.moveDown(block.height);
688
+ }
689
+
690
+ return true;
691
+ }
692
+
693
+ /**
694
+ * Pushes the provided context onto the stack or creates a new one
695
+ *
696
+ * pushContext(context) - pushes the provided context and makes it current
697
+ * pushContext(width, height) - creates and pushes a new context with the specified width and height
698
+ * pushContext() - creates a new context for unbreakable blocks (with current availableWidth and full-page-height)
699
+ *
700
+ * @param {DocumentContext|number} contextOrWidth
701
+ * @param {number} height
702
+ */
703
+ pushContext(contextOrWidth, height) {
704
+ if (contextOrWidth === undefined) {
705
+ height = this.context().getCurrentPage().height - this.context().pageMargins.top - this.context().pageMargins.bottom;
706
+ contextOrWidth = this.context().availableWidth;
707
+ }
708
+
709
+ if (isNumber(contextOrWidth)) {
710
+ let width = contextOrWidth;
711
+ contextOrWidth = new DocumentContext();
712
+ contextOrWidth.addPage({ width: width, height: height }, { left: 0, right: 0, top: 0, bottom: 0 });
713
+ }
714
+
715
+ this.contextStack.push(this.context());
716
+ this._context = contextOrWidth;
717
+ }
718
+
719
+ popContext() {
720
+ this._context = this.contextStack.pop();
721
+ }
722
+
723
+ getCurrentPositionOnPage() {
724
+ return (this.contextStack[0] || this.context()).getCurrentPosition();
725
+ }
726
+ }
727
+
728
+ function addPageItem(page, item, index) {
729
+ if (index === null || index === undefined || index < 0 || index > page.items.length) {
730
+ page.items.push(item);
731
+ } else {
732
+ page.items.splice(index, 0, item);
733
+ }
734
+ }
735
+
736
+ export default ElementWriter;