documonster 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
211
211
 
212
212
  ```html
213
213
  <!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
214
- <script src="https://unpkg.com/documonster@0.16.1/dist/iife/documonster.excel.iife.min.js"></script>
214
+ <script src="https://unpkg.com/documonster@0.17.0/dist/iife/documonster.excel.iife.min.js"></script>
215
215
  <script>
216
216
  const { Workbook, Cell } = Documonster.Excel;
217
217
  const wb = Workbook.create();
package/README_zh.md CHANGED
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
198
198
 
199
199
  ```html
200
200
  <!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
201
- <script src="https://unpkg.com/documonster@0.16.1/dist/iife/documonster.excel.iife.min.js"></script>
201
+ <script src="https://unpkg.com/documonster@0.17.0/dist/iife/documonster.excel.iife.min.js"></script>
202
202
  <script>
203
203
  const { Workbook, Cell } = Documonster.Excel;
204
204
  const wb = Workbook.create();
@@ -26,7 +26,7 @@ import { copyStyle } from "../utils/copy-style.js";
26
26
  import { isExternalImage } from "../utils/drawing-utils.js";
27
27
  import { applyMergeBorders, collectMergeBorders } from "../utils/merge-borders.js";
28
28
  import { buildSheetProtection, verifySheetPassword } from "../utils/sheet-protection.js";
29
- import { calculateAutoFitWidth, getMaxDigitWidth, getColumnContentWidthPx, getCellTextWidthPx, getCellHeightPt } from "../utils/text-metrics.js";
29
+ import { calculateAutoFitWidth, getMaxDigitWidth, charWidthToPixel, getPixelPadding, getCellTextWidthPx, getCellHeightPt } from "../utils/text-metrics.js";
30
30
  // Worksheet requirements
31
31
  // Operate as sheet inside workbook or standalone
32
32
  // Load and Save from file and stream
@@ -1200,12 +1200,23 @@ export function removeConditionalFormatting(ws, filter) {
1200
1200
  ws.conditionalFormattings = [];
1201
1201
  }
1202
1202
  }
1203
- export function autoFitColumn(ws, col) {
1203
+ /**
1204
+ * Set a column's width to fit its content, as Excel's AutoFit does.
1205
+ *
1206
+ * Cells in hidden rows are ignored unless `includeHiddenRows` is set. A
1207
+ * merged cell counts as visible while any row of the merge is, since that is
1208
+ * where Excel draws it; a merge spanning several columns is not measured,
1209
+ * because its width cannot be attributed to one of them.
1210
+ */
1211
+ export function autoFitColumn(ws, col, options) {
1204
1212
  const colNum = typeof col === "string" ? colCache.l2n(col) : col;
1205
- _autoFitColumnImpl(ws, colNum);
1213
+ _autoFitColumnImpl(ws, colNum, options);
1206
1214
  return ws;
1207
1215
  }
1208
- export function autoFitColumns(ws, startCol, endCol) {
1216
+ export function autoFitColumns(ws, startColOrOptions, endCol, rangeOptions) {
1217
+ const rangeGiven = startColOrOptions === null || typeof startColOrOptions !== "object";
1218
+ const startCol = rangeGiven ? startColOrOptions : undefined;
1219
+ const options = rangeGiven ? rangeOptions : startColOrOptions;
1209
1220
  const dims = getSheetDimensions(ws);
1210
1221
  if (!dims || dims.left === undefined) {
1211
1222
  return ws;
@@ -1217,7 +1228,7 @@ export function autoFitColumns(ws, startCol, endCol) {
1217
1228
  : dims.left;
1218
1229
  const end = endCol != null ? (typeof endCol === "string" ? colCache.l2n(endCol) : endCol) : dims.right;
1219
1230
  for (let c = start; c <= end; c++) {
1220
- _autoFitColumnImpl(ws, c);
1231
+ _autoFitColumnImpl(ws, c, options);
1221
1232
  }
1222
1233
  return ws;
1223
1234
  }
@@ -1237,8 +1248,45 @@ export function autoFitRows(ws, startRow, endRow) {
1237
1248
  }
1238
1249
  return ws;
1239
1250
  }
1240
- function _autoFitColumnImpl(ws, colNum) {
1251
+ /** A row that has never been created has default visibility. */
1252
+ function _isRowHidden(ws, rowNumber) {
1253
+ const row = ws._rows[rowNumber - 1];
1254
+ return row ? rowHidden(row) : false;
1255
+ }
1256
+ function _isColumnHidden(ws, colNum) {
1257
+ return ws._columns[colNum - 1]?.hidden === true;
1258
+ }
1259
+ function _anyRowVisible(ws, top, bottom) {
1260
+ for (let r = top; r <= bottom; r++) {
1261
+ if (!_isRowHidden(ws, r)) {
1262
+ return true;
1263
+ }
1264
+ }
1265
+ return false;
1266
+ }
1267
+ function _anyColumnVisible(ws, left, right) {
1268
+ for (let c = left; c <= right; c++) {
1269
+ if (!_isColumnHidden(ws, c)) {
1270
+ return true;
1271
+ }
1272
+ }
1273
+ return false;
1274
+ }
1275
+ /**
1276
+ * The merged range a master cell heads, or `undefined` for any other cell.
1277
+ *
1278
+ * A merge's content is stored only in its master, yet it is displayed wherever
1279
+ * any part of the merge is. Auto-fit therefore decides a master's visibility
1280
+ * over this range rather than its own row or column — which is what keeps a
1281
+ * merge whose master is hidden, one inside a collapsed outline group say, from
1282
+ * being dropped.
1283
+ */
1284
+ function _mergeRangeOf(ws, cell) {
1285
+ return cellIsMerged(cell) ? ws._merges[cell.address] : undefined;
1286
+ }
1287
+ function _autoFitColumnImpl(ws, colNum, options) {
1241
1288
  const mdw = getMaxDigitWidth(); // default font MDW
1289
+ const includeHiddenRows = options?.includeHiddenRows === true;
1242
1290
  // Check if this column is under an autofilter
1243
1291
  const hasAutoFilter = _isColumnInAutoFilter(ws, colNum);
1244
1292
  let maxWidthPx = 0;
@@ -1247,26 +1295,23 @@ function _autoFitColumnImpl(ws, colNum) {
1247
1295
  if (!row) {
1248
1296
  return;
1249
1297
  }
1250
- // Skip hidden rows — Excel excludes them from auto-fit
1251
- if (rowHidden(row)) {
1252
- return;
1253
- }
1254
1298
  const cell = rowFindCell(row, colNum);
1255
1299
  if (!cell) {
1256
1300
  return;
1257
1301
  }
1258
1302
  // Skip merged cell slaves — the content belongs to the master cell.
1259
- // For the master cell of a multi-column merge, skip too (the width
1260
- // should not be attributed to a single column).
1261
1303
  if (cellType(cell) === Enums.ValueType.Merge) {
1262
1304
  return;
1263
1305
  }
1264
- if (cellIsMerged(cell)) {
1265
- // This is a master cell with merges spanning multiple columns
1266
- const mergeRange = ws._merges[cell.address];
1267
- if (mergeRange && mergeRange.left !== mergeRange.right) {
1268
- return; // multi-column merge — skip
1269
- }
1306
+ const merge = _mergeRangeOf(ws, cell);
1307
+ // A multi-column merge's width should not be attributed to one column.
1308
+ if (merge && merge.left !== merge.right) {
1309
+ return;
1310
+ }
1311
+ // Skip content in hidden rows — Excel excludes it from auto-fit
1312
+ if (!includeHiddenRows &&
1313
+ (merge ? !_anyRowVisible(ws, merge.top, merge.bottom) : rowHidden(row))) {
1314
+ return;
1270
1315
  }
1271
1316
  // Skip shrinkToFit cells — they adapt to the column, not vice versa
1272
1317
  if (cellAlignment(cell)?.shrinkToFit) {
@@ -1303,19 +1348,17 @@ function _autoFitRowImpl(ws, rowNumber) {
1303
1348
  if (cellType(cell) === Enums.ValueType.Merge) {
1304
1349
  return;
1305
1350
  }
1306
- // Skip multi-row merged masters
1307
- if (cellIsMerged(cell)) {
1308
- const mergeRange = ws._merges[cell.address];
1309
- if (mergeRange && mergeRange.top !== mergeRange.bottom) {
1310
- return;
1311
- }
1351
+ const merge = _mergeRangeOf(ws, cell);
1352
+ // A multi-row merge's height should not be attributed to one row.
1353
+ if (merge && merge.top !== merge.bottom) {
1354
+ return;
1312
1355
  }
1313
- // Skip cells in hidden columns
1314
- const col = ws._columns[cellCol(cell) - 1];
1315
- if (col?.hidden) {
1356
+ // Skip content in hidden columns
1357
+ const col = cellCol(cell);
1358
+ if (merge ? !_anyColumnVisible(ws, merge.left, merge.right) : _isColumnHidden(ws, col)) {
1316
1359
  return;
1317
1360
  }
1318
- const columnWidthPx = _getColumnContentWidthForCell(ws, cell, mdw);
1361
+ const columnWidthPx = _getColumnContentWidthForCell(ws, cell, mdw, merge?.left ?? col, merge?.right ?? col);
1319
1362
  const heightPt = getCellHeightPt(cellView(cell), mdw, columnWidthPx);
1320
1363
  if (heightPt > maxHeightPt) {
1321
1364
  maxHeightPt = heightPt;
@@ -1326,14 +1369,23 @@ function _autoFitRowImpl(ws, rowNumber) {
1326
1369
  row.customHeight = true;
1327
1370
  }
1328
1371
  }
1329
- function _getColumnContentWidthForCell(ws, cell, mdw) {
1372
+ function _getColumnContentWidthForCell(ws, cell, mdw, left, right) {
1330
1373
  if (!cellAlignment(cell)?.wrapText) {
1331
1374
  return undefined;
1332
1375
  }
1333
- // Try to get explicit column width; avoid creating a column as side effect
1334
- const col = ws._columns[cellCol(cell) - 1];
1335
- const colWidth = col?.width ?? ws.properties.defaultColWidth ?? 9;
1336
- return getColumnContentWidthPx(colWidth, mdw);
1376
+ // Text wraps across the visible columns the cell spans, with the cell padding
1377
+ // charged once for the whole merge rather than once per column.
1378
+ let totalPx = 0;
1379
+ for (let c = left; c <= right; c++) {
1380
+ if (!_isColumnHidden(ws, c)) {
1381
+ totalPx += charWidthToPixel(_columnWidthChars(ws, c), mdw);
1382
+ }
1383
+ }
1384
+ return Math.max(0, totalPx - getPixelPadding(mdw));
1385
+ }
1386
+ /** Width of a column in characters, without creating it as a side effect. */
1387
+ function _columnWidthChars(ws, colNum) {
1388
+ return ws._columns[colNum - 1]?.width ?? ws.properties.defaultColWidth ?? 9;
1337
1389
  }
1338
1390
  function _isColumnInAutoFilter(ws, colNum) {
1339
1391
  if (!ws.autoFilter) {
@@ -41,7 +41,7 @@ import { erDiagramNodes, erNodeSizer, erToFlowchart } from "./render/er.js";
41
41
  import { flowchartNodes } from "./render/flowchart.js";
42
42
  import { ganttDrawList } from "./render/gantt.js";
43
43
  import { blockDrawList, c4DiagramNodes, c4NodeSizer, c4ToFlowchart, requirementDiagramNodes, requirementNodeSizer, requirementToFlowchart } from "./render/model.js";
44
- import { backdrop } from "./render/shared.js";
44
+ import { backdrop, centredText } from "./render/shared.js";
45
45
  import { pieDrawList, sequenceDrawList } from "./render/simple.js";
46
46
  import { stateToFlowchart } from "./render/state.js";
47
47
  import { journeyDrawList, timelineDrawList } from "./render/track.js";
@@ -177,3 +177,30 @@ export function mermaidToSvg(source, options = {}) {
177
177
  }
178
178
  export { MermaidSyntaxError, parseMermaid };
179
179
  export { layoutFlowchart };
180
+ /**
181
+ * Draw a laid-out diagram yourself, in the colours and at the text positions the built-in
182
+ * renderer would have used.
183
+ *
184
+ * {@link layoutFlowchart} is public, so "use the layout and render it your own way" is a
185
+ * supported route — a caller wanting a different node shape, an annotation of their own, or
186
+ * an HTML/canvas target rather than a `DrawList` takes it. It was half-open: the geometry
187
+ * came back, and everything needed to *paint* it was internal.
188
+ *
189
+ * {@link Theme} was published without either the function that produces one or any signature
190
+ * accepting one, which made it a name for a thing a consumer could not obtain, could not
191
+ * construct — ten required `Rgba01` fields plus a `paletteText` callback — and could not pass
192
+ * anywhere. `resolveTheme` is what it was missing: {@link MermaidRenderOptions.theme} takes
193
+ * CSS strings, and this is the same resolution the renderer applies to them, so a
194
+ * self-rendered diagram matches one from {@link mermaidToSvg} instead of approximating it.
195
+ * Without it the only route to the palette was to copy the hex literals out of this module's
196
+ * source and hope they did not move.
197
+ *
198
+ * `centredText` is exported rather than the two constants behind it (`LINE_HEIGHT`,
199
+ * `BASELINE_SHIFT`) on purpose. A display list positions text by its baseline, because that
200
+ * is the one thing every backend can honour, so stacking `NodeBox.lines` in the middle of a
201
+ * box is arithmetic the caller has to do — and handing out the constants invites a ninth copy
202
+ * of that arithmetic, free to disagree with the eight inside. The box shape it takes is
203
+ * exactly what the layout already returns: `NodeBox`, `GroupBox` and `EdgeRoute.label` all
204
+ * carry `x`/`y`/`width`/`height`, and the last two carry the wrapped `lines` with them.
205
+ */
206
+ export { centredText, resolveTheme };
@@ -1110,11 +1110,23 @@ function buildMergeMap(sheet) {
1110
1110
  }
1111
1111
  for (const rangeStr of merges) {
1112
1112
  const range = parseRangeRef(rangeStr);
1113
+ const top = range.s.r + 1;
1114
+ const left = range.s.c + 1;
1115
+ const bottom = range.e.r + 1;
1116
+ const right = range.e.c + 1;
1117
+ const rowShown = (r) => !sheet.rows.get(r)?.hidden;
1118
+ const colShown = (c) => !sheet.columns.get(c)?.hidden;
1119
+ const [shownTop, shownBottom] = shownSpan(top, bottom, rowShown);
1120
+ const [shownLeft, shownRight] = shownSpan(left, right, colShown);
1113
1121
  const region = {
1114
- top: range.s.r + 1,
1115
- left: range.s.c + 1,
1116
- bottom: range.e.r + 1,
1117
- right: range.e.c + 1
1122
+ top,
1123
+ left,
1124
+ bottom,
1125
+ right,
1126
+ shownTop,
1127
+ shownLeft,
1128
+ shownBottom,
1129
+ shownRight
1118
1130
  };
1119
1131
  for (let r = region.top; r <= region.bottom; r++) {
1120
1132
  for (let c = region.left; c <= region.right; c++) {
@@ -1124,6 +1136,21 @@ function buildMergeMap(sheet) {
1124
1136
  }
1125
1137
  return map;
1126
1138
  }
1139
+ /**
1140
+ * The first and last tracks in `first..last` that are shown, or the range
1141
+ * itself when none is.
1142
+ */
1143
+ function shownSpan(first, last, shown) {
1144
+ let lo = first;
1145
+ while (lo < last && !shown(lo)) {
1146
+ lo++;
1147
+ }
1148
+ let hi = last;
1149
+ while (hi > lo && !shown(hi)) {
1150
+ hi--;
1151
+ }
1152
+ return shown(lo) ? [lo, hi] : [first, last];
1153
+ }
1127
1154
  /**
1128
1155
  * How far a merged region reaches along one axis of one page, starting at the
1129
1156
  * page track `at`.
@@ -1292,20 +1319,25 @@ function mergePieceInput(piece, sheet) {
1292
1319
  const { region } = piece;
1293
1320
  const borderAt = (row, col) => sheet.rows.get(row)?.cells.get(col)?.style?.border;
1294
1321
  // Excel formats the whole region from the master and draws its value once, in
1295
- // the piece that holds it — the others are the same box continued, not a
1296
- // repeat of its contents.
1322
+ // the piece that begins where the region is first displayed — the others are
1323
+ // the same box continued, not a repeat of its contents. That is the master's
1324
+ // own piece unless the master's row or column is hidden.
1297
1325
  const master = sheet.rows.get(region.top)?.cells.get(region.left);
1298
- const holdsMaster = piece.top === region.top && piece.left === region.left;
1326
+ const holdsValue = piece.top === region.shownTop && piece.left === region.shownLeft;
1327
+ // Each edge is the region's own only where the piece reaches its displayed
1328
+ // boundary; the style is still read from the boundary cells that own it.
1299
1329
  return {
1300
1330
  styleCell: master,
1301
- valueCell: holdsMaster ? master : undefined,
1331
+ valueCell: holdsValue ? master : undefined,
1302
1332
  colSpan: piece.colSpan,
1303
1333
  rowSpan: piece.rowSpan,
1304
1334
  borders: {
1305
- top: piece.top === region.top ? borderAt(region.top, piece.left)?.top : undefined,
1306
- bottom: piece.bottom === region.bottom ? borderAt(region.bottom, piece.left)?.bottom : undefined,
1307
- left: piece.left === region.left ? borderAt(piece.top, region.left)?.left : undefined,
1308
- right: piece.right === region.right ? borderAt(piece.top, region.right)?.right : undefined
1335
+ top: piece.top === region.shownTop ? borderAt(region.top, piece.left)?.top : undefined,
1336
+ bottom: piece.bottom === region.shownBottom
1337
+ ? borderAt(region.bottom, piece.left)?.bottom
1338
+ : undefined,
1339
+ left: piece.left === region.shownLeft ? borderAt(piece.top, region.left)?.left : undefined,
1340
+ right: piece.right === region.shownRight ? borderAt(piece.top, region.right)?.right : undefined
1309
1341
  }
1310
1342
  };
1311
1343
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.16.1
2
+ * documonster v0.17.0
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.16.1
2
+ * documonster v0.17.0
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * documonster v0.16.1
2
+ * documonster v0.17.0
3
3
  * Zero-dependency TypeScript toolkit for Excel, Word, PDF, CSV, Markdown, XML & ZIP — one API across Node.js, Bun & browsers.
4
4
  * (c) 2025 cjnoname
5
5
  * Released under the Apache-2.0 License