@muhammara/native-core 7.0.0-beta.3 → 7.0.0-beta.4

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.
@@ -1,9 +1,9 @@
1
- function clone(object) {
2
- return JSON.parse(JSON.stringify(object));
3
- }
1
+ var { htmlToTextObjects } = require("./htmlToTextObjects");
2
+ var { cloneOptions: clone } = require("./utils");
4
3
 
4
+ /** Converts a table cell style into text options. */
5
5
  function getCellOptions(options, cell = "cell") {
6
- const cellOptions = clone(options);
6
+ var cellOptions = clone(options);
7
7
 
8
8
  if (cellOptions[cell]) {
9
9
  // convert cell options to textBox options
@@ -13,65 +13,117 @@ function getCellOptions(options, cell = "cell") {
13
13
  return cellOptions;
14
14
  }
15
15
 
16
+ /** Measures the same text and outer box height that text() will draw. */
16
17
  function getCellHeight(self, text, column, options) {
17
- let colOptions = self._merge(options, { textBox: { width: column.width } });
18
- const originCoord = self._calibrateCoordinate(
18
+ var colOptions = self._merge(options, { textBox: { width: column.width } });
19
+ var originCoord = self._calibrateCoordinate(
19
20
  column.x,
20
21
  column.y,
21
22
  0,
22
23
  0,
23
24
  self.pageNumber,
24
25
  );
25
- const pathOptions = self._getPathOptions(
26
+ var pathOptions = self._getPathOptions(
26
27
  colOptions,
27
28
  originCoord.nx,
28
29
  originCoord.ny,
29
30
  );
30
- const textObjects = self._makeTextObject(text, pathOptions.size, colOptions);
31
- const textBox = self._makeTextBox(colOptions);
32
- const { textHeight } = self._layoutText(textObjects, textBox, pathOptions);
33
-
34
- return textHeight;
31
+ pathOptions.html = colOptions.html;
32
+ var textObjects = colOptions.html
33
+ ? htmlToTextObjects(text, colOptions)
34
+ : self._makeTextObject(text, pathOptions.size, colOptions);
35
+ var textBox = self._makeTextBox(colOptions);
36
+ var { textHeight } = self._layoutText(textObjects, textBox, pathOptions);
37
+
38
+ return (
39
+ textBox.height ||
40
+ Math.max(
41
+ textBox.minHeight,
42
+ textHeight + textBox.paddingTop + textBox.paddingBottom,
43
+ )
44
+ );
35
45
  }
36
46
 
47
+ /** Draws a completed table segment without duplicating its bottom edge. */
37
48
  function drawTableBorder(self, x, y, width, height, rowLines, options) {
38
- if (options.border) {
39
- let borderOptions = options.border === true ? {} : options.border;
49
+ // A segment without rows has nothing to enclose.
50
+ if (!options.border || height <= 0) {
51
+ return;
52
+ }
53
+ var borderOptions = Object.assign(
54
+ {},
55
+ options.border === true ? {} : options.border,
56
+ // Keep borders from extending outside of the enclosing box.
57
+ { lineCap: "butt" },
58
+ );
59
+ if (!borderOptions.width) {
60
+ borderOptions.width = 0.5;
61
+ }
40
62
 
41
- if (!borderOptions.width) {
42
- borderOptions = Object.assign({}, borderOptions, { width: 0.5 });
43
- }
63
+ self.rectangle(x, y, width, height, borderOptions);
64
+ var columns = self._layouts["_table_"];
65
+
66
+ // Draw verticals
67
+ for (var index = 0; index < columns.length - 1; index++) {
68
+ var column = columns[index];
69
+ self.line(
70
+ [
71
+ [column.x + column.width, y],
72
+ [column.x + column.width, y + height],
73
+ ],
74
+ borderOptions,
75
+ );
76
+ }
77
+ // Draw horizontals; the last row line is the rectangle's bottom edge.
78
+ for (var index = 0; index < rowLines.length - 1; index++) {
79
+ var yPos = rowLines[index];
80
+ self.line(
81
+ [
82
+ [x, yPos],
83
+ [x + width, yPos],
84
+ ],
85
+ borderOptions,
86
+ );
87
+ }
88
+ }
44
89
 
45
- self.rectangle(x, y, width, height, borderOptions);
46
- const columns = self._layouts["_table_"];
47
-
48
- // Draw verticles
49
- for (let index = 0; index < columns.length - 1; index++) {
50
- const column = columns[index];
51
- self.line(
52
- [
53
- [column.x + column.width, y],
54
- [column.x + column.width, y + height],
55
- ],
56
- borderOptions,
57
- );
58
- }
59
- // Draw horizontals
60
- for (let index = 0; index < rowLines.length - 1; index++) {
61
- const yPos = rowLines[index];
62
- self.line(
63
- [
64
- [x, yPos],
65
- [x + width, yPos],
66
- ],
67
- borderOptions,
68
- );
90
+ /**
91
+ * Resolves the table's data fields: `order` when given, otherwise the names
92
+ * of the configured `columns`, otherwise every field found in any record, in
93
+ * first-seen order.
94
+ * @private
95
+ */
96
+ function tableFields(contents, options) {
97
+ if (options.order && options.order.length) {
98
+ return typeof options.order === "string"
99
+ ? options.order
100
+ .split(",")
101
+ .map((field) => field.trim())
102
+ .filter(Boolean)
103
+ : options.order.slice();
104
+ }
105
+ if (options.columns && options.columns.length) {
106
+ return options.columns.map((column) => column.name);
107
+ }
108
+ var fields = [];
109
+ for (var record of contents) {
110
+ for (var field of Object.keys(record || {})) {
111
+ if (!fields.includes(field)) {
112
+ fields.push(field);
113
+ }
69
114
  }
70
115
  }
116
+ return fields;
71
117
  }
72
118
 
73
119
  /**
74
120
  * Display text data in tabular form
121
+ * Rows and headers use their rendered text-box heights, including padding,
122
+ * minimum heights, fixed heights, and HTML layout. Empty contents or no selected
123
+ * columns leave the Recipe unchanged. Array-form order preserves exact keys.
124
+ * Header text styles are independent of body styles: column header options
125
+ * (or defaults) are overridden by table header options, then alignToData
126
+ * and column hcell box overrides are applied.
75
127
  * @name table
76
128
  * @function
77
129
  * @memberof Recipe#
@@ -108,6 +160,7 @@ function drawTableBorder(self, x, y, width, height, rowLines, options) {
108
160
  * @param {function} [options.overflow] - Called when the next table entry is going to expand the table
109
161
  * beyond the given height or page boundary. Its parameters are (self, row) where 'self' is the recipe handle so
110
162
  * that other recipe interfaces can be called, and the row number of the data which caused the data overflow.
163
+ * The callback's `this` is also the Recipe instance.
111
164
  * The return value can be 'true' which indicates that data processing should stop, or 'false' which indicates that
112
165
  * the data should continue being processed with the original [x,y] coordinates, or it can be an object containing
113
166
  * a 'position' property indicating the [x,y] coordinates where the next table for the remaining data should start.
@@ -116,88 +169,80 @@ function drawTableBorder(self, x, y, width, height, rowLines, options) {
116
169
  * @param {string} [options.row.nth] - 'even|odd', indicating that the properties should be applied only to
117
170
  * 'even' or 'odd' rows.
118
171
  * @returns {Recipe} The recipe instance.
172
+ * @throws {RangeError} If the overflow callback continues into an area too small
173
+ * for the pending row and its repeated header. Return true to stop, or provide
174
+ * enough space; rows are not split and the callback is called once per overflow.
175
+ * @throws {Error} If the overflow callback continues after ending the page
176
+ * without starting another one.
119
177
  */
120
178
  exports.table = function table(x, y, contents, options = {}) {
121
- let tableBottom = options.height ? y + options.height : 0;
122
- let fields = Object.keys(contents[0]);
123
- let order = options.order || [];
124
- let columns = [];
125
-
126
- if (order.length !== 0) {
127
- if (typeof options.order === "string") {
128
- order = options.order.split(",");
129
- }
130
- } else {
131
- if (!options.columns || options.columns.length > fields.length) {
132
- order = fields; // all fields emitted, columns in order of first record
133
- } else {
134
- // columns in order found in options
135
- for (const column of options.columns) {
136
- order.push(column.name);
137
- }
138
- }
179
+ if (!Array.isArray(contents) || contents.length === 0) {
180
+ return this;
139
181
  }
140
-
141
- for (const field of order) {
142
- if (fields.includes(field)) {
143
- let found = false;
144
- if (options.columns) {
145
- for (const column of options.columns) {
146
- if (column.name && column.name === field) {
147
- columns.push(column);
148
- found = true;
149
- break;
150
- }
151
- }
152
- }
153
-
154
- if (!found) {
155
- columns.push({ text: field, name: field });
156
- }
157
- }
182
+ var columns = tableFields(contents, options).map((field) => {
183
+ var column =
184
+ options.columns &&
185
+ options.columns.find((definition) => definition.name === field);
186
+ return column || { text: field, name: field };
187
+ });
188
+ if (columns.length === 0) {
189
+ return this;
158
190
  }
159
191
  this.layout("_table_", x, y, 0, 0, { columns: columns, reset: true });
160
192
 
161
- const tableWidth = this._layouts["_table_"].reduce((width, column) => {
193
+ var tableWidth = this._layouts["_table_"].reduce((width, column) => {
162
194
  width += column.width;
163
195
  return width;
164
196
  }, 0);
165
197
 
166
- let tableHeight = 0;
167
- let headerHeight = 0;
168
- let rowLines = [];
169
- let currentY = y;
170
198
  this._previousTextObjects = [];
171
- let nth;
172
- let rowOptions = {};
199
+ var nth;
200
+ var rowOptions = {};
201
+
202
+ /** Resolves header styles identically for measurement and drawing. */
203
+ var headerOptions = (column) => {
204
+ var colOptions = clone(column.options.header);
205
+ if (typeof options.header === "object") {
206
+ colOptions = this._merge(colOptions, getCellOptions(options.header));
207
+ }
208
+ // Have header alignment match data alignment?
209
+ if (options.header.alignToData && column.options.textBox.textAlign) {
210
+ colOptions.textBox.textAlign = column.options.textBox.textAlign;
211
+ }
212
+ // Is there a specific header cell override in this column?
213
+ if (column.options.hcell) {
214
+ var cellOptions = getCellOptions(column.options, "hcell");
215
+ colOptions.textBox = this._merge(
216
+ colOptions.textBox,
217
+ clone(cellOptions.textBox),
218
+ );
219
+ }
220
+ return colOptions;
221
+ };
173
222
 
223
+ var headerHeight = 0;
174
224
  if (options.header) {
175
- // Wade through columns to determine minimum header height
176
- for (const column of this._layouts["_table_"]) {
177
- let colOptions = clone(column.options.header);
178
- if (typeof options.header === "object") {
179
- const cellOptions = getCellOptions(options.header);
180
- colOptions = this._merge(colOptions, cellOptions);
181
- }
182
-
183
- const cellHeight = getCellHeight(this, column.text, column, colOptions);
225
+ for (var column of this._layouts["_table_"]) {
226
+ var cellHeight = getCellHeight(
227
+ this,
228
+ column.text,
229
+ column,
230
+ headerOptions(column),
231
+ );
184
232
  headerHeight = Math.max(headerHeight, cellHeight);
185
233
  }
186
234
  }
187
235
 
188
- if (options.overflow) {
189
- this._tableFullNotifier = options.overflow;
190
-
191
- const pageBottom =
236
+ /** Recomputes bounds for each continuation position and page. */
237
+ var segmentBottom = (top) => {
238
+ var bottom = options.height ? top + options.height : 0;
239
+ var pageBottom =
192
240
  this.pageInfo(this.pageNumber).height - this._margin.bottom;
193
- if (tableBottom === 0 || tableBottom > pageBottom) {
194
- tableBottom = pageBottom;
241
+ if (bottom === 0 || bottom > pageBottom) {
242
+ bottom = pageBottom;
195
243
  }
196
- }
197
-
198
- if (options.border) {
199
- options.border.lineCap = "butt"; // keep borders from extending outside of enclosing box.
200
- }
244
+ return bottom;
245
+ };
201
246
 
202
247
  if (options.row) {
203
248
  rowOptions = getCellOptions(options.row);
@@ -221,46 +266,78 @@ exports.table = function table(x, y, contents, options = {}) {
221
266
  }
222
267
  }
223
268
 
224
- let firstTime = true;
225
- let row = 0;
269
+ var tableBottom = options.overflow ? segmentBottom(y) : 0;
270
+ var tableHeight = 0;
271
+ var rowLines = [];
272
+ var currentY = y;
273
+ var firstTime = true;
274
+ var row = 0;
226
275
 
227
- for (const record of contents) {
228
- let rowHeight = 0;
276
+ for (var record of contents) {
229
277
  row++;
230
278
 
231
- // Wade through row data to determine row height
232
- for (const column of this._layouts["_table_"]) {
233
- const field = column.field;
234
- const text = record[field];
235
- if (text !== undefined) {
236
- let colOptions = clone(options);
237
- colOptions = this._merge(colOptions, clone(column.options));
238
- if (nth && nth(row)) {
239
- colOptions = this._merge(colOptions, clone(rowOptions));
279
+ // Resolve every cell once: the renderer runs once per cell, and its
280
+ // options size the row as well as style the drawn text.
281
+ var cells = this._layouts["_table_"].map((column) => {
282
+ var field = column.field;
283
+ var value = Object.prototype.hasOwnProperty.call(record, field)
284
+ ? record[field]
285
+ : undefined;
286
+ var text = value === undefined || value === null ? "" : value;
287
+ var colOptions = clone(options);
288
+ // The table callback is not a text-flow overflow callback.
289
+ delete colOptions.overflow;
290
+ colOptions = this._merge(colOptions, clone(column.options));
291
+ if (nth && nth(row)) {
292
+ colOptions = this._merge(colOptions, clone(rowOptions));
293
+ }
294
+ if (column.options.renderer) {
295
+ var renderOptions = column.options.renderer(text, record, field, row);
296
+ if (renderOptions) {
297
+ colOptions = this._merge(colOptions, renderOptions);
240
298
  }
241
- const cellHeight = getCellHeight(this, text, column, colOptions);
242
- rowHeight = Math.max(rowHeight, cellHeight);
243
299
  }
300
+ return { column, text: String(text), options: colOptions };
301
+ });
302
+
303
+ var rowHeight = 0;
304
+ for (var cell of cells) {
305
+ var cellHeight = getCellHeight(
306
+ this,
307
+ cell.text,
308
+ cell.column,
309
+ cell.options,
310
+ );
311
+ rowHeight = Math.max(rowHeight, cellHeight);
244
312
  }
245
313
 
246
314
  // When table gets 'full', let user know when overflow callback provided.
247
315
  // They can choose to bail out of table filling loop, or keep on going with
248
316
  // appropriate variables reset to initial values. This gives the user the
249
317
  // opportunity to change to a new page to continue table production with
250
- // remaining rows of data.
251
- if (currentY + rowHeight > tableBottom && this._tableFullNotifier) {
318
+ // remaining rows of data. A continuation reserves room for its repeated
319
+ // header as well as the row.
320
+ var needed = rowHeight + (firstTime && options.header ? headerHeight : 0);
321
+ if (options.overflow && currentY + needed > tableBottom) {
252
322
  drawTableBorder(this, x, y, tableWidth, tableHeight, rowLines, options);
253
323
 
254
- const orders = this._tableFullNotifier(this, row);
324
+ var orders = options.overflow.call(this, this, row);
255
325
 
256
326
  if (orders === true) {
257
- return this;
258
- } // stop processing table data
259
- if (orders.position) {
327
+ // stop processing table data
328
+ tableHeight = 0;
329
+ break;
330
+ }
331
+ if (!this.page) {
332
+ throw new Error(
333
+ "Recipe.table: the overflow callback must leave an active page to continue on.",
334
+ );
335
+ }
336
+ if (orders && orders.position) {
260
337
  [x, y] = orders.position;
261
- let xx = x;
338
+ var xx = x;
262
339
  // Make sure x position adjusted in all columns
263
- for (const column of this._layouts["_table_"]) {
340
+ for (var column of this._layouts["_table_"]) {
264
341
  column.x = xx;
265
342
  xx += column.width;
266
343
  }
@@ -270,32 +347,18 @@ exports.table = function table(x, y, contents, options = {}) {
270
347
  currentY = y;
271
348
  tableHeight = 0;
272
349
  rowLines = [];
350
+ tableBottom = segmentBottom(y);
351
+ if (currentY + rowHeight + headerHeight > tableBottom) {
352
+ throw new RangeError(
353
+ `Recipe.table: row ${row} and its header do not fit in the continuation area.`,
354
+ );
355
+ }
273
356
  }
274
357
 
275
358
  if (firstTime && options.header) {
276
359
  // Display table header
277
- for (const column of this._layouts["_table_"]) {
278
- let colOptions = clone(column.options.header);
279
- if (typeof options.header === "object") {
280
- const cellOptions = getCellOptions(options.header);
281
- colOptions = this._merge(colOptions, clone(cellOptions));
282
- }
283
-
284
- // Have header alignment match data alignment?
285
- if (options.header.alignToData && column.options.textBox.textAlign) {
286
- colOptions.textBox.textAlign = column.options.textBox.textAlign;
287
- }
288
-
289
- // Is there a specific header cell override in this column?
290
- if (column.options.hcell) {
291
- const cellOptions = getCellOptions(column.options, "hcell");
292
- colOptions.textBox = this._merge(
293
- colOptions.textBox,
294
- clone(cellOptions.textBox),
295
- );
296
- }
297
-
298
- colOptions = this._merge(colOptions, {
360
+ for (var column of this._layouts["_table_"]) {
361
+ var colOptions = this._merge(headerOptions(column), {
299
362
  textBox: { minHeight: headerHeight, width: column.width },
300
363
  });
301
364
  this.text(column.text, column.x, currentY, colOptions);
@@ -309,27 +372,11 @@ exports.table = function table(x, y, contents, options = {}) {
309
372
  firstTime = false;
310
373
 
311
374
  // Now write out table cells for current record
312
- for (const column of this._layouts["_table_"]) {
313
- const field = column.field;
314
- let text = record[field];
315
- if (text === undefined) {
316
- text = "";
317
- }
318
- let colOptions = clone(options);
319
- colOptions = this._merge(colOptions, clone(column.options));
320
- if (nth && nth(row)) {
321
- colOptions = this._merge(colOptions, clone(rowOptions));
322
- }
323
- if (column.options.renderer) {
324
- let renderOptions = column.options.renderer(text, record, field, row);
325
- if (renderOptions) {
326
- colOptions = this._merge(colOptions, renderOptions);
327
- }
328
- }
329
- colOptions = this._merge(colOptions, {
330
- textBox: { minHeight: rowHeight, width: column.width },
375
+ for (var cell of cells) {
376
+ var colOptions = this._merge(cell.options, {
377
+ textBox: { minHeight: rowHeight, width: cell.column.width },
331
378
  });
332
- this.text(text, column.x, currentY, colOptions);
379
+ this.text(cell.text, cell.column.x, currentY, colOptions);
333
380
  }
334
381
 
335
382
  currentY += rowHeight;
@@ -339,5 +386,12 @@ exports.table = function table(x, y, contents, options = {}) {
339
386
 
340
387
  drawTableBorder(this, x, y, tableWidth, tableHeight, rowLines, options);
341
388
 
389
+ // Leave the text cursor at the table's left edge, below its last segment.
390
+ this.x = x;
391
+ this.y = currentY;
392
+ this.box = { x, y: currentY };
393
+ this._flow = false;
394
+ this._previousTextObjects = [];
395
+
342
396
  return this;
343
397
  };
@@ -1,3 +1,5 @@
1
+ var { cloneOptions, resolveFontSize } = require("./utils");
2
+
1
3
  const charSpacing = function charSpacing(text, charSpace) {
2
4
  let txt = text.replace(/^(?:(?!\u00a0)\s)+|(?:(?!\u00a0)\s)+$/g, "");
3
5
  return txt.length ? (txt.length - 1) * charSpace : 0;
@@ -246,7 +248,7 @@ exports.textDimensions = function textDimensions(text, options = {}) {
246
248
  if (options.charSpace) {
247
249
  charSpaces = charSpacing(text, options.charSpace);
248
250
  }
249
- const fontSize = options.size || this.current.defaultFontSize;
251
+ const fontSize = resolveFontSize(options, this.current.defaultFontSize);
250
252
  dimensions = font.calculateTextDimensions(text, fontSize);
251
253
  dimensions.xMax += charSpaces;
252
254
  }
@@ -276,7 +278,7 @@ exports.Column = class Column {
276
278
  this._field = field; // associated data field
277
279
  this._text = text || field; // for column title
278
280
  this._gap = 0;
279
- this._options = JSON.parse(JSON.stringify(options)); // cloning
281
+ this._options = cloneOptions(options);
280
282
 
281
283
  this._options.textBox = this._options.cell;
282
284