reamkit 1.31.0 → 1.32.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.
Files changed (83) hide show
  1. package/README.md +10 -0
  2. package/dist/esm/core/bidi/index.d.ts +10 -0
  3. package/dist/esm/core/bidi/index.js +18 -2
  4. package/dist/esm/core/converter/facade.js +10 -5
  5. package/dist/esm/core/converter/project.js +1 -0
  6. package/dist/esm/core/converter/ream.js +17 -6
  7. package/dist/esm/core/converter/sheet-images.d.ts +17 -0
  8. package/dist/esm/core/converter/sheet-images.js +33 -0
  9. package/dist/esm/core/document-model/index.d.ts +1 -1
  10. package/dist/esm/core/document-model/types.d.ts +112 -0
  11. package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
  12. package/dist/esm/core/drawingml/chart-geometry.js +198 -108
  13. package/dist/esm/core/drawingml/chart-parser.js +73 -2
  14. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  15. package/dist/esm/core/drawingml/colors.js +23 -3
  16. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  17. package/dist/esm/core/drawingml/shape-render.js +4 -2
  18. package/dist/esm/core/font/measure.d.ts +9 -0
  19. package/dist/esm/core/font/measure.js +9 -1
  20. package/dist/esm/core/fonts/families.js +4 -2
  21. package/dist/esm/core/fonts/scripts.js +4 -2
  22. package/dist/esm/core/ir/flow.d.ts +6 -0
  23. package/dist/esm/core/ir/sheet.d.ts +43 -0
  24. package/dist/esm/core/number-format.js +16 -11
  25. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  26. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  27. package/dist/esm/core/style-cascade/resolver.js +4 -2
  28. package/dist/esm/excel/column-bands.d.ts +39 -6
  29. package/dist/esm/excel/column-bands.js +41 -17
  30. package/dist/esm/excel/comments-parser.js +19 -0
  31. package/dist/esm/excel/conditional-format.d.ts +7 -3
  32. package/dist/esm/excel/conditional-format.js +77 -39
  33. package/dist/esm/excel/formula/context.d.ts +18 -0
  34. package/dist/esm/excel/formula/eval.js +51 -0
  35. package/dist/esm/excel/formula/index.d.ts +2 -2
  36. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  37. package/dist/esm/excel/formula/lexer.js +41 -0
  38. package/dist/esm/excel/formula/parser.d.ts +25 -0
  39. package/dist/esm/excel/formula/parser.js +95 -0
  40. package/dist/esm/excel/print-model.d.ts +38 -1
  41. package/dist/esm/excel/print-model.js +522 -80
  42. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  43. package/dist/esm/excel/shared-strings-parser.js +11 -1
  44. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  45. package/dist/esm/excel/sheet-drawing.js +47 -13
  46. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  47. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  48. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  49. package/dist/esm/excel/sheet-to-flow.js +434 -47
  50. package/dist/esm/excel/styles-parser.d.ts +9 -0
  51. package/dist/esm/excel/styles-parser.js +80 -26
  52. package/dist/esm/excel/table-parser.js +5 -0
  53. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  54. package/dist/esm/excel/table-style-presets.js +1555 -0
  55. package/dist/esm/excel/table-style.d.ts +13 -0
  56. package/dist/esm/excel/table-style.js +75 -0
  57. package/dist/esm/excel/tint.d.ts +10 -0
  58. package/dist/esm/excel/tint.js +75 -0
  59. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  60. package/dist/esm/excel/vml-drawing.js +143 -10
  61. package/dist/esm/excel/worksheet-parser.js +89 -15
  62. package/dist/esm/excel/xls/biff-reader.js +10 -2
  63. package/dist/esm/excel/xlsx-reader.js +195 -14
  64. package/dist/esm/excel/xlsx-writer.js +8 -7
  65. package/dist/esm/html/html-writer.js +202 -20
  66. package/dist/esm/layout/fit-pages.d.ts +11 -0
  67. package/dist/esm/layout/fit-pages.js +86 -0
  68. package/dist/esm/layout/line-paint.d.ts +58 -0
  69. package/dist/esm/layout/line-paint.js +107 -0
  70. package/dist/esm/layout/page-doc.d.ts +51 -0
  71. package/dist/esm/layout/page-doc.js +141 -1
  72. package/dist/esm/layout/styled-layout.d.ts +7 -0
  73. package/dist/esm/layout/styled-layout.js +274 -51
  74. package/dist/esm/layout/turned-section.js +14 -2
  75. package/dist/esm/markdown/markdown-writer.js +17 -6
  76. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  77. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  78. package/dist/esm/pdf-reader/font.js +3 -3
  79. package/dist/esm/svg/svg-text.d.ts +58 -0
  80. package/dist/esm/svg/svg-text.js +322 -0
  81. package/dist/esm/svg/svg-writer.js +135 -71
  82. package/dist/esm/word/docx-writer.js +11 -2
  83. package/package.json +5 -1
@@ -1,3 +1,4 @@
1
+ import { resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
1
2
  import { FEATURES } from "../core/ir/features.js";
2
3
  import { headingLevelOf } from "../core/outline.js";
3
4
  import { detectImageFormat } from "../core/images.js";
@@ -7,7 +8,7 @@ import "../core/style-cascade/index.js";
7
8
  import { buildSparkline } from "../core/drawingml/sparkline-geometry.js";
8
9
  import { arcPoint, arcToBeziers } from "../core/arc-to-bezier.js";
9
10
  import { buildChartScene } from "../core/drawingml/chart-geometry.js";
10
- import { buildShapePaths, buildShapeTransform, buildStroke, gradientSvgDef } from "../core/drawingml/shape-render.js";
11
+ import { buildShapePaths, buildShapeTransform, buildStroke, gradientSvgDef, lineEndPaths } from "../core/drawingml/shape-render.js";
11
12
  import { sanitizeHref } from "../core/links.js";
12
13
  import { toBase64 } from "../core/bytes.js";
13
14
  //#region src/html/html-writer.ts
@@ -47,13 +48,15 @@ function writeHtml(flow) {
47
48
  out.push(`<style>${BASE_CSS}</style>`);
48
49
  out.push("</head>");
49
50
  out.push("<body>");
50
- out.push(`<article style="max-width: ${fmt(contentWidthPt(flow))}pt">`);
51
+ const sheets = flow.sections.some((s) => s.sheet !== void 0);
52
+ out.push(sheets ? "<article>" : `<article style="max-width: ${fmt(contentWidthPt(flow))}pt">`);
51
53
  if (flow.headersFooters && flow.headersFooters.size > 0) losses.push({
52
54
  severity: "dropped",
53
55
  feature: FEATURES.headersFooters,
54
56
  detail: "headers/footers have no fixed pages to band onto in flowed HTML"
55
57
  });
56
- for (const el of flow.body) emitBlock(out, el, ctx);
58
+ if (sheets) emitSheets(out, flow, ctx);
59
+ else for (const el of flow.body) emitBlock(out, el, ctx);
57
60
  emitNotesSection(out, flow.footnotes, ctx.notes.footnotes, "fn", ctx);
58
61
  emitNotesSection(out, flow.endnotes, ctx.notes.endnotes, "en", ctx);
59
62
  emitCommentsSection(out, flow.comments, ctx.notes.comments, ctx);
@@ -95,12 +98,73 @@ var BASE_CSS = [
95
98
  "th{text-align:inherit}",
96
99
  ".tab{display:inline-block;min-width:18pt}",
97
100
  "img{vertical-align:baseline}",
101
+ ".sheet{margin:0 0 24pt}",
102
+ ".sheet-name{font:600 11pt/1.4 sans-serif;margin:0 0 6pt;color:#333}",
103
+ ".surface{position:relative}",
104
+ ".drawing{z-index:1}",
98
105
  ".notes{margin-top:18pt;font-size:smaller}",
99
106
  ".notes hr{margin:0 0 6pt;border:none;border-top:0.75pt solid #000;width:144pt;margin-left:0}",
100
107
  ".comment-range{background:#fff3a3}",
101
108
  ".comment-replies{margin-left:18pt}",
102
109
  ".comments .resolved>.comment-meta::after{content:\" \\2713\";color:#3a7d3a}"
103
110
  ].join("");
111
+ /**
112
+ * A workbook laid out for the screen: each sheet a section of its own, headed
113
+ * by its tab's name, its grid and drawings on one surface. The surface is the
114
+ * positioned box the drawings are placed in, and it is made as large as the
115
+ * furthest of them — a chart below the last row is outside the grid, and a box
116
+ * only as tall as the grid let the next sheet run under it.
117
+ */
118
+ function emitSheets(out, flow, ctx) {
119
+ let start = 0;
120
+ for (const section of flow.sections) {
121
+ const els = flow.body.slice(start, section.endIndex);
122
+ start = section.endIndex;
123
+ if (!section.sheet) {
124
+ for (const el of els) emitBlock(out, el, ctx);
125
+ continue;
126
+ }
127
+ const name = section.sheet.name;
128
+ out.push(`<section class="sheet" data-sheet="${escapeAttr(name)}">`);
129
+ out.push(`<h2 class="sheet-name">${escapeText(name)}</h2>`);
130
+ let right = 0;
131
+ let bottom = 0;
132
+ for (const el of els) {
133
+ const at = surfaceBox(el);
134
+ if (!at) continue;
135
+ right = Math.max(right, at.x + at.width);
136
+ bottom = Math.max(bottom, at.y + at.height);
137
+ }
138
+ const reach = right > 0 || bottom > 0 ? `min-width:${fmt(right)}pt;min-height:${fmt(bottom)}pt` : "";
139
+ out.push(`<div class="surface"${reach ? ` style="${reach}"` : ""}>`);
140
+ const onSurface = {
141
+ ...ctx,
142
+ surface: true
143
+ };
144
+ for (const el of els) emitBlock(out, el, onSurface);
145
+ out.push("</div>");
146
+ out.push("</section>");
147
+ }
148
+ for (const el of flow.body.slice(start)) emitBlock(out, el, ctx);
149
+ }
150
+ /**
151
+ * Where a floating drawing sits on a worksheet's surface, in points from its
152
+ * top-left corner — or `undefined` for a block that is not a float placed by
153
+ * offsets (a paragraph, a table, an inline picture).
154
+ */
155
+ function surfaceBox(el) {
156
+ if (el.kind === "paragraph" || el.kind === "table") return void 0;
157
+ const block = el.kind === "image" ? el.image : el.kind === "chart" ? el.chart : el.shape;
158
+ const x = block.float?.posH?.offsetPt;
159
+ const y = block.float?.posV?.offsetPt;
160
+ if (x === void 0 || y === void 0) return void 0;
161
+ return {
162
+ x,
163
+ y,
164
+ width: block.width,
165
+ height: block.height
166
+ };
167
+ }
104
168
  function collectReferencedAnchors(body) {
105
169
  const out = /* @__PURE__ */ new Set();
106
170
  const visit = (els) => {
@@ -190,6 +254,16 @@ function emitCommentsSection(out, comments, numbers, ctx) {
190
254
  out.push("</ol></section>");
191
255
  }
192
256
  function emitBlock(out, el, ctx) {
257
+ const at = ctx.surface ? surfaceBox(el) : void 0;
258
+ if (at) {
259
+ out.push(`<div class="drawing" style="position:absolute;left:${fmt(at.x)}pt;top:${fmt(at.y)}pt;width:${fmt(at.width)}pt;height:${fmt(at.height)}pt">`);
260
+ emitBlock(out, el, {
261
+ ...ctx,
262
+ surface: false
263
+ });
264
+ out.push("</div>");
265
+ return;
266
+ }
193
267
  if (el.kind === "paragraph") emitParagraph(out, el.paragraph, ctx);
194
268
  else if (el.kind === "table") emitTable(out, el.table, ctx);
195
269
  else if (el.kind === "image") emitImageBlock(out, el.image, ctx);
@@ -205,7 +279,13 @@ var blockFigureCss = (pp) => {
205
279
  if (pp.spacingAfter !== void 0 && pp.spacingAfter > 0) css.push(`margin-bottom:${fmt(pp.spacingAfter)}pt`);
206
280
  return css.length > 0 ? ` style="${css.join(";")}"` : "";
207
281
  };
208
- var svgOpen = (w, h, label) => `<svg viewBox="0 0 ${fmt(w)} ${fmt(h)}" width="${fmt(w)}pt" height="${fmt(h)}pt" style="max-width:100%;height:auto" overflow="visible" xmlns="http://www.w3.org/2000/svg" role="img"${label ? ` aria-label="${escapeAttr(label)}"` : ""}>`;
282
+ var LINE_PAD_PT = .5;
283
+ var svgOpen = (w, h, label) => {
284
+ const padX = w < 2 * LINE_PAD_PT ? LINE_PAD_PT : 0;
285
+ const padY = h < 2 * LINE_PAD_PT ? LINE_PAD_PT : 0;
286
+ const style = padX > 0 || padY > 0 ? `margin:${fmt(-padY)}pt 0 0 ${fmt(-padX)}pt;vertical-align:top` : "max-width:100%;height:auto";
287
+ return `<svg viewBox="${fmt(-padX)} ${fmt(-padY)} ${fmt(w + 2 * padX)} ${fmt(h + 2 * padY)}" width="${fmt(w + 2 * padX)}pt" height="${fmt(h + 2 * padY)}pt" style="${style}" overflow="visible" xmlns="http://www.w3.org/2000/svg" role="img"${label ? ` aria-label="${escapeAttr(label)}"` : ""}>`;
288
+ };
209
289
  var strokeAttrs = (stroke) => {
210
290
  if (!stroke) return "";
211
291
  let out = ` stroke="#${stroke.colorHex}" stroke-width="${fmt(stroke.widthPt)}"`;
@@ -293,17 +373,20 @@ function emitOneShape(out, shape, ctx) {
293
373
  const paths = buildShapePaths(shape.geometry, w, h);
294
374
  const gradDef = shape.fill.kind === "gradient" && shape.fill.gradient ? gradientSvgDef("grad0", shape.fill.gradient) : void 0;
295
375
  const fill = gradDef ? "url(#grad0)" : shape.fill.kind === "solid" && shape.fill.colorHex ? `#${shape.fill.colorHex}` : "none";
296
- const stroke = strokeAttrs(buildStroke(shape.line));
376
+ const strokeStyle = buildStroke(shape.line);
377
+ const stroke = strokeAttrs(strokeStyle);
297
378
  const t = shape.transform;
298
379
  const transform = ` transform="matrix(${flipTransform(buildShapeTransform(0, 0, w, h, t?.rotation60k ?? 0, t?.flipH ?? false, t?.flipV ?? false), h).map(fmt).join(" ")})"`;
299
380
  const svg = [svgOpen(w, h, shape.altText)];
300
381
  if (gradDef) svg.push(`<defs>${gradDef}</defs>`);
301
382
  const shadow = shape.shadow;
302
- const shadowFilter = shadow ? ` filter="drop-shadow(${fmt(shadow.dxPt)}px ${fmt(shadow.dyPt)}px ${fmt(shadow.blurPt / 2)}px rgba(${hexToRgbCss(shadow.colorHex)},${fmt(shadow.alpha)}))"` : "";
383
+ if (shadow) svg.push(`<g filter="drop-shadow(${fmt(shadow.dxPt)}px ${fmt(shadow.dyPt)}px ${fmt(shadow.blurPt / 2)}px rgba(${hexToRgbCss(shadow.colorHex)},${fmt(shadow.alpha)}))">`);
303
384
  for (const path of paths) {
304
385
  const rule = path.fillRule === "evenodd" ? " fill-rule=\"evenodd\"" : "";
305
- svg.push(`<path d="${svgPathData(path.segments, fmt)}" fill="${fill}"${rule}${stroke}${transform}${shadowFilter}/>`);
386
+ svg.push(`<path d="${svgPathData(path.segments, fmt)}" fill="${fill}"${rule}${stroke}${transform}/>`);
306
387
  }
388
+ if (shadow) svg.push("</g>");
389
+ if (strokeStyle) for (const end of lineEndPaths(paths, shape.line, strokeStyle.widthPt)) svg.push(`<path d="${svgPathData(end.segments, fmt)}" fill="#${strokeStyle.colorHex}"${transform}/>`);
307
390
  svg.push("</svg>");
308
391
  const figureCss = blockFigureCss(shape.paragraphProperties);
309
392
  const text = shape.text;
@@ -336,12 +419,16 @@ function emitParagraph(out, p, ctx) {
336
419
  }
337
420
  function paragraphCss(r) {
338
421
  const css = [];
339
- if (r.alignment === "center" || r.alignment === "right") css.push(`text-align:${r.alignment}`);
340
- else if (r.alignment === "both" || r.alignment === "distribute") css.push("text-align:justify");
422
+ const bidi = r.bidi === true;
423
+ const align = bidi && r.alignment === "left" ? "right" : bidi && r.alignment === "right" ? "left" : r.alignment;
424
+ if (align === "center" || align === "right" || bidi && align === "left") css.push(`text-align:${align}`);
425
+ else if (align === "both" || align === "distribute") css.push("text-align:justify");
341
426
  if (r.spacingBefore > 0) css.push(`margin-top:${fmt(r.spacingBefore)}pt`);
342
427
  if (r.spacingAfter > 0) css.push(`margin-bottom:${fmt(r.spacingAfter)}pt`);
343
- if (r.indentLeft !== 0) css.push(`margin-left:${fmt(r.indentLeft)}pt`);
344
- if (r.indentRight !== 0) css.push(`margin-right:${fmt(r.indentRight)}pt`);
428
+ const indentLeft = bidi ? r.indentRight : r.indentLeft;
429
+ const indentRight = bidi ? r.indentLeft : r.indentRight;
430
+ if (indentLeft !== 0) css.push(`margin-left:${fmt(indentLeft)}pt`);
431
+ if (indentRight !== 0) css.push(`margin-right:${fmt(indentRight)}pt`);
345
432
  if (r.indentFirstLine !== 0) css.push(`text-indent:${fmt(r.indentFirstLine)}pt`);
346
433
  if (r.spacingLine > 0) if (r.spacingLineRule === "auto") css.push(`line-height:${fmt(r.spacingLine / 12)}`);
347
434
  else css.push(`line-height:${fmt(r.spacingLine)}pt`);
@@ -379,7 +466,7 @@ function runHtml(run, p, ctx) {
379
466
  if (run.text.length === 0) return "";
380
467
  const resolved = resolveRunProperties(run.properties, p.properties, EMPTY_STYLE_SHEET);
381
468
  const style = runCss(resolved);
382
- let html = `<span${resolved.rtl ? " dir=\"rtl\"" : ""}${style ? ` style="${style}"` : ""}>${textHtml(run.text)}</span>`;
469
+ let html = `<span${resolved.rtl ? " dir=\"rtl\"" : ""}${style ? ` style="${escapeAttr(style)}"` : ""}>${textHtml(run.text)}</span>`;
383
470
  if (resolved.verticalAlign === "superscript") html = `<sup>${html}</sup>`;
384
471
  else if (resolved.verticalAlign === "subscript") html = `<sub>${html}</sub>`;
385
472
  if (run.commentRangeRefs && run.commentRangeRefs.length > 0) html = `<span class="comment-range">${html}</span>`;
@@ -398,7 +485,7 @@ function runHtml(run, p, ctx) {
398
485
  function runCss(r) {
399
486
  const css = [];
400
487
  const family = r.fontFamily.ascii;
401
- if (family) css.push(`font-family:${JSON.stringify(family)}`);
488
+ if (family) css.push(`font-family:${fontStack(family)}`);
402
489
  css.push(`font-size:${fmt(r.fontSizePt)}pt`);
403
490
  if (r.bold) css.push("font-weight:700");
404
491
  if (r.italic) css.push("font-style:italic");
@@ -414,6 +501,28 @@ function runCss(r) {
414
501
  if (r.colorHex !== "000000") css.push(`color:#${r.colorHex}`);
415
502
  return css.join(";");
416
503
  }
504
+ /**
505
+ * A family as CSS asks for it: the document's own, then — where the page is
506
+ * read without that font, which for Calibri is anywhere Office is not — its
507
+ * open metric twin, then the class of face it is. Without the class a browser
508
+ * that lacks the font falls to its default, a serif: a Calibri sheet came out
509
+ * in Times.
510
+ *
511
+ * The name is a CSS string in SINGLE quotes. The declaration goes into a
512
+ * double-quoted `style` attribute, where a double quote ended the attribute
513
+ * and took the size, weight and colour after the family with it.
514
+ */
515
+ function fontStack(family) {
516
+ return [`'${family.replace(/\s+/gu, " ").replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`, ...FALLBACKS[resolveFamilyStyle(family).key]].join(",");
517
+ }
518
+ /** What follows a family in its stack, by the open substitute it maps to. */
519
+ var FALLBACKS = {
520
+ carlito: ["Carlito", "sans-serif"],
521
+ caladea: ["Caladea", "serif"],
522
+ arimo: ["sans-serif"],
523
+ tinos: ["serif"],
524
+ cousine: ["monospace"]
525
+ };
417
526
  function decorationStyle(u) {
418
527
  switch (u) {
419
528
  case "double": return "double";
@@ -430,8 +539,11 @@ function textHtml(text) {
430
539
  }
431
540
  function emitTable(out, table, ctx) {
432
541
  const css = [];
542
+ const fixed = table.properties.layout === "fixed";
433
543
  if (table.properties.widthPt !== void 0) css.push(`width:${fmt(table.properties.widthPt)}pt`);
434
544
  else if (table.properties.widthFraction !== void 0) css.push(`width:${fmt(table.properties.widthFraction * 100)}%`);
545
+ else if (fixed && table.grid.length > 0) css.push(`width:${fmt(table.grid.reduce((sum, w) => sum + w, 0))}pt`);
546
+ if (fixed) css.push("table-layout:fixed");
435
547
  if (table.properties.alignment === "center") css.push("margin-left:auto;margin-right:auto");
436
548
  else if (table.properties.alignment === "right") css.push("margin-left:auto");
437
549
  out.push(`<table${css.length > 0 ? ` style="${css.join(";")}"` : ""}>`);
@@ -448,6 +560,7 @@ function emitTable(out, table, ctx) {
448
560
  return start;
449
561
  });
450
562
  });
563
+ const cellGrid = table.properties.gridlines ? gridOfCells(table, colStarts) : void 0;
451
564
  const frozen = table.properties.frozen;
452
565
  const leftOffsets = [];
453
566
  const topOffsets = [];
@@ -465,14 +578,19 @@ function emitTable(out, table, ctx) {
465
578
  }
466
579
  for (let ri = 0; ri < table.rows.length; ri++) {
467
580
  const row = table.rows[ri];
468
- out.push("<tr>");
581
+ const { height, heightRule } = row.properties;
582
+ out.push(height !== void 0 && heightRule !== "auto" ? `<tr style="height:${fmt(height)}pt">` : "<tr>");
469
583
  for (let ci = 0; ci < row.cells.length; ci++) {
470
584
  const cell = row.cells[ci];
471
585
  const merge = cell.properties.merge;
472
586
  if (merge === "middle" || merge === "end") continue;
473
587
  const cs = colStarts[ri][ci];
474
588
  const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, cs) : 1;
589
+ const claimed = cellGrid ? claimedEdges(cellGrid, ri, cs, cs + (cell.properties.colSpan ?? 1) - 1, rowSpan) : void 0;
590
+ const flagInsetPt = noteFlagInset(cell, cs, table.grid);
475
591
  emitCell(out, cell, table, ctx, {
592
+ ...flagInsetPt > 0 ? { flagInsetPt } : {},
593
+ ...claimed && claimed.size > 0 ? { claimed } : {},
476
594
  isHeader: row.properties.isHeader === true,
477
595
  firstRow: ri === 0,
478
596
  lastRow: ri === table.rows.length - 1,
@@ -487,6 +605,56 @@ function emitTable(out, table, ctx) {
487
605
  }
488
606
  out.push("</table>");
489
607
  }
608
+ /**
609
+ * How far a note's flag stands in from the cell's side: the width of the
610
+ * columns its text only borrowed (CellProperties.paintColumns) — after its own
611
+ * column, or before it where the span runs leftwards (paintAtEnd).
612
+ */
613
+ function noteFlagInset(cell, colStart, grid) {
614
+ const p = cell.properties;
615
+ const span = p.colSpan ?? 1;
616
+ if (!p.noteFlag || p.paintColumns === void 0 || p.paintColumns >= span) return 0;
617
+ return (p.paintAtEnd ? grid.slice(colStart, colStart + span - p.paintColumns) : grid.slice(colStart + p.paintColumns, colStart + span)).reduce((sum, w) => sum + w, 0);
618
+ }
619
+ /** The cell at each row × grid column (a spanning cell at every column it covers). */
620
+ function gridOfCells(table, colStarts) {
621
+ return table.rows.map((row, ri) => {
622
+ const line = new Array(table.grid.length).fill(void 0);
623
+ row.cells.forEach((cell, ci) => {
624
+ const start = colStarts[ri][ci];
625
+ for (let k = 0; k < (cell.properties.colSpan ?? 1); k++) line[start + k] = cell;
626
+ });
627
+ return line;
628
+ });
629
+ }
630
+ /**
631
+ * The edges of a cell (rows `ri`…`ri + rowSpan − 1`, columns `c0`…`c1`) that a
632
+ * neighbour across them CLAIMS: one that rules its facing side, or is filled.
633
+ * A window's gridline gives way there. Collapsed borders are settled edge by
634
+ * edge, wider first and then by position, and a gridline is as wide as a
635
+ * thin rule once the browser rounds both to a pixel — so the cell above or to
636
+ * the left won, and a merged range ruled all round showed only two of its four
637
+ * sides. Excel draws no gridline along a filled cell either. Where only some of
638
+ * the neighbours claim the edge, the others draw their own gridline on it.
639
+ */
640
+ function claimedEdges(grid, ri, c0, c1, rowSpan) {
641
+ const claims = (cell, facing) => {
642
+ if (!cell) return false;
643
+ if (cell.properties.shading) return true;
644
+ const b = cell.properties.borders?.[facing];
645
+ return b !== void 0 && b.style !== "none";
646
+ };
647
+ const out = /* @__PURE__ */ new Set();
648
+ for (let c = c0; c <= c1; c++) {
649
+ if (claims(grid[ri - 1]?.[c], "bottom")) out.add("top");
650
+ if (claims(grid[ri + rowSpan]?.[c], "top")) out.add("bottom");
651
+ }
652
+ for (let r = ri; r < ri + rowSpan; r++) {
653
+ if (claims(grid[r]?.[c0 - 1], "right")) out.add("left");
654
+ if (claims(grid[r]?.[c1 + 1], "left")) out.add("right");
655
+ }
656
+ return out;
657
+ }
490
658
  function mergeRowSpan(table, colStarts, startRow, colStart) {
491
659
  let span = 1;
492
660
  for (let r = startRow + 1; r < table.rows.length; r++) {
@@ -508,11 +676,15 @@ function emitCell(out, cell, table, ctx, pos) {
508
676
  const css = [];
509
677
  const t = table.properties.borders;
510
678
  const c = cell.properties.borders;
511
- pushBorder(css, "top", c?.top ?? (pos.firstRow ? t?.top : t?.insideH));
512
- pushBorder(css, "bottom", c?.bottom ?? (pos.lastRow ? t?.bottom : t?.insideH));
513
- pushBorder(css, "left", c?.left ?? (pos.firstCol ? t?.left : t?.insideV));
514
- pushBorder(css, "right", c?.right ?? (pos.lastCol ? t?.right : t?.insideV));
679
+ const g = cell.properties.shading ? void 0 : table.properties.gridlines;
680
+ const gridOn = (side) => pos.claimed?.has(side) ? void 0 : g;
681
+ pushBorder(css, "top", c?.top ?? (pos.firstRow ? t?.top : t?.insideH) ?? gridOn("top"));
682
+ pushBorder(css, "bottom", c?.bottom ?? (pos.lastRow ? t?.bottom : t?.insideH) ?? gridOn("bottom"));
683
+ pushBorder(css, "left", c?.left ?? (pos.firstCol ? t?.left : t?.insideV) ?? gridOn("left"));
684
+ pushBorder(css, "right", c?.right ?? (pos.lastCol ? t?.right : t?.insideV) ?? gridOn("right"));
515
685
  if (cell.properties.shading) css.push(`background-color:#${cell.properties.shading.colorHex}`);
686
+ if (cell.properties.verticalAlign === "center") css.push("vertical-align:middle");
687
+ else if (cell.properties.verticalAlign === "bottom") css.push("vertical-align:bottom");
516
688
  if (cell.properties.noWrap) css.push("white-space:nowrap", "overflow:hidden");
517
689
  if (cell.properties.dataBar) {
518
690
  const db = cell.properties.dataBar;
@@ -520,7 +692,8 @@ function emitCell(out, cell, table, ctx, pos) {
520
692
  const start = (clamp(db.startFraction ?? 0) * 100).toFixed(2);
521
693
  const end = (clamp((db.startFraction ?? 0) + db.fraction) * 100).toFixed(2);
522
694
  const c = db.colorHex;
523
- css.push(`background-image:linear-gradient(to right,transparent ${start}%,#${c} ${start}%,#${c} ${end}%,transparent ${end}%)`);
695
+ const [from, to] = db.solid ? [c, c] : db.negative ? ["FFFFFF", c] : [c, "FFFFFF"];
696
+ css.push(`background-image:linear-gradient(to right,transparent ${start}%,#${from} ${start}%,#${to} ${end}%,transparent ${end}%)`, "background-repeat:no-repeat", "background-size:calc(100% - 3pt) calc(100% - 3pt)", "background-position:1.5pt 1.5pt");
524
697
  }
525
698
  const margins = cell.properties.margins ?? table.properties.defaultCellMargins;
526
699
  const padTop = margins?.top ?? 0;
@@ -536,13 +709,22 @@ function emitCell(out, cell, table, ctx, pos) {
536
709
  css.push(`z-index:${z}`);
537
710
  if (!cell.properties.shading) css.push("background-color:#fff");
538
711
  }
712
+ const note = cell.properties.noteFlag;
713
+ if (note) {
714
+ attrs.push(`title="${escapeAttr(note.text)}"`);
715
+ if (pos.stickyTop === void 0 && pos.stickyLeft === void 0) css.push("position:relative");
716
+ }
539
717
  out.push(`<${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""} style="${css.join(";")}">`);
718
+ if (note) out.push(cellNoteFlag(note, pos.flagInsetPt ?? 0));
540
719
  if (cell.properties.dropdown) out.push(cellDropdownSvg());
541
720
  if (cell.properties.icon) out.push(cellIconSvg(cell.properties.icon));
542
721
  if (cell.properties.sparkline) out.push(cellSparklineSvg(cell.properties.sparkline));
543
722
  for (const child of cell.content) emitBlock(out, child, ctx);
544
723
  out.push(`</${tag}>`);
545
724
  }
725
+ function cellNoteFlag(note, insetPt) {
726
+ return `<span aria-hidden="true" style="position:absolute;top:0;${note.atLeft ? "left" : "right"}:${fmt(insetPt)}pt;width:0;height:0;border-top:4.5pt solid #${note.kind === "thread" ? "7030A0" : "FF0000"};border-${note.atLeft ? "right" : "left"}:4.5pt solid transparent"></span>`;
727
+ }
546
728
  function cellSparklineSvg(sp) {
547
729
  const W = 56;
548
730
  const H = 16;
@@ -598,7 +780,7 @@ function cellIconSvg(icon) {
598
780
  body = `<circle cx="5" cy="5" r="4" fill="${fill}"/>`;
599
781
  break;
600
782
  }
601
- return `<svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:middle;margin-right:3px">${body}</svg>`;
783
+ return `<svg width="10" height="10" viewBox="0 0 10 10" style="float:left;margin:2px 3px 0 0">${body}</svg>`;
602
784
  }
603
785
  var round2 = (n) => (Math.round(n * 100) / 100).toString();
604
786
  function barsIconSvg(fill, color, empty) {
@@ -0,0 +1,11 @@
1
+ import { LaidOutDocument } from './page-doc.js';
2
+ /**
3
+ * Each page cut down to the right and bottom edges of what is drawn on it,
4
+ * plus `pad`. The top-left corner stays where it is — the content's origin is
5
+ * the page's, and the layout already kept its margin there.
6
+ *
7
+ * @param laid The laid-out document, its pages larger than their content.
8
+ * @param pad The room kept past the content on the right and at the bottom.
9
+ * @returns The same document on pages no larger than what they hold.
10
+ */
11
+ export declare function fitPagesToContent(laid: LaidOutDocument, pad: number): LaidOutDocument;
@@ -0,0 +1,86 @@
1
+ import { pt } from "../core/ir/units.js";
2
+ //#region src/layout/fit-pages.ts
3
+ /**
4
+ * Each page cut down to the right and bottom edges of what is drawn on it,
5
+ * plus `pad`. The top-left corner stays where it is — the content's origin is
6
+ * the page's, and the layout already kept its margin there.
7
+ *
8
+ * @param laid The laid-out document, its pages larger than their content.
9
+ * @param pad The room kept past the content on the right and at the bottom.
10
+ * @returns The same document on pages no larger than what they hold.
11
+ */
12
+ function fitPagesToContent(laid, pad) {
13
+ return {
14
+ ...laid,
15
+ pages: laid.pages.map((page) => fitPage(page, pad))
16
+ };
17
+ }
18
+ function fitPage(page, pad) {
19
+ let right = 0;
20
+ let bottom = 0;
21
+ for (const item of page.commands) {
22
+ const reach = reachOf(item);
23
+ const w = item.window;
24
+ right = Math.max(right, w ? Math.min(reach.right, w.x + w.width) : reach.right);
25
+ bottom = Math.max(bottom, w ? Math.min(reach.bottom, w.y + w.height) : reach.bottom);
26
+ }
27
+ return {
28
+ ...page,
29
+ width: pt(Math.min(page.width, Math.ceil(right + pad))),
30
+ height: pt(Math.min(page.height, Math.ceil(bottom + pad)))
31
+ };
32
+ }
33
+ /** How far right and down one item paints. */
34
+ function reachOf(item) {
35
+ switch (item.type) {
36
+ case "fill":
37
+ case "image": return {
38
+ right: item.x + item.width,
39
+ bottom: item.y + item.height
40
+ };
41
+ case "border": {
42
+ const half = item.borderSizePt / 2;
43
+ return {
44
+ right: item.x + item.width + half,
45
+ bottom: item.y + item.height + half
46
+ };
47
+ }
48
+ case "line": {
49
+ if (item.clip) return {
50
+ right: item.clip.x + item.clip.width,
51
+ bottom: item.clip.y + item.clip.height
52
+ };
53
+ const descent = item.line.metricDescentPt ?? item.line.maxFontSizePt * .25;
54
+ return {
55
+ right: item.originX + item.line.contentWidthPt,
56
+ bottom: item.baselineY + descent
57
+ };
58
+ }
59
+ case "shape": return shapeReach(item.shape);
60
+ }
61
+ }
62
+ /** The far corner of a shape's paths on the page, its stroke included (control points bound a curve). */
63
+ function shapeReach(shape) {
64
+ const [a, b, c, d, e, f] = shape.transform;
65
+ const half = (shape.stroke?.widthPt ?? 0) / 2;
66
+ let right = 0;
67
+ let bottom = 0;
68
+ const take = (x, y) => {
69
+ right = Math.max(right, a * x + c * y + e + half);
70
+ bottom = Math.max(bottom, b * x + d * y + f + half);
71
+ };
72
+ for (const path of shape.paths) for (const s of path.segments) {
73
+ if (s.op === "close") continue;
74
+ if (s.op === "cubic") {
75
+ take(s.x1, s.y1);
76
+ take(s.x2, s.y2);
77
+ }
78
+ take(s.x, s.y);
79
+ }
80
+ return {
81
+ right,
82
+ bottom
83
+ };
84
+ }
85
+ //#endregion
86
+ export { fitPagesToContent };
@@ -0,0 +1,58 @@
1
+ import { BorderStyle } from '../core/document-model/index.js';
2
+ import { Line } from './page-doc.js';
3
+ /** The slant of a faked italic — tan(12°), the angle a text italic leans at. */
4
+ export declare const FAUX_ITALIC_SHEAR = 0.2126;
5
+ /**
6
+ * §18.18.3 — the dash pattern of a rule that is not solid, in points: dashes
7
+ * and gaps alternating, from a dash.
8
+ */
9
+ export declare const BORDER_DASHES: ReadonlyMap<BorderStyle, ReadonlyArray<number>>;
10
+ /**
11
+ * UAX #9 rule L2 over a line's tokens: the token indices in visual order. Each
12
+ * token carries a single embedding level (the tokenizer split runs at level
13
+ * boundaries), so reordering tokens is equivalent to reordering their
14
+ * characters.
15
+ *
16
+ * @param line The laid-out line.
17
+ * @returns Its token indices, left to right as they are drawn.
18
+ */
19
+ export declare function lineVisualOrder(line: Line): Array<number>;
20
+ /**
21
+ * The extra width each space of a justified line takes — 0 for a line that is
22
+ * not justified, or the last line of a paragraph (which stays flush by
23
+ * convention).
24
+ *
25
+ * @param line The laid-out line.
26
+ * @returns Points added to (or, squeezed, taken from) each space token.
27
+ */
28
+ export declare function computeJustifyExtra(line: Line): number;
29
+ /**
30
+ * Whether a line is ONE text matrix and a run of glyphs: nothing in it placed
31
+ * on its own — a tab, a stretched space, a picture, a formula, a right-to-left
32
+ * run, a rise, a faked italic — and nothing drawn beside its glyphs, a run's
33
+ * shading, a highlight or a rule. Only such a line can be turned by its matrix
34
+ * alone; any other is placed token by token in page space.
35
+ *
36
+ * @param line The laid-out line.
37
+ * @returns True when a single text matrix sets the whole of it.
38
+ */
39
+ export declare function settableInOneMatrix(line: Line): boolean;
40
+ /**
41
+ * §14.1.2.10 — the wash a picture is drawn through, as the flat veil it is.
42
+ * `gain`/`blacklevel` are contrast and brightness about mid grey, so a source
43
+ * value `in` prints as `(in - 0.5) * gain + 0.5 + black` — and painting a
44
+ * colour `c` at opacity `a` gives `in * (1 - a) + c * a`, the same line with
45
+ * `a = 1 - gain`. Exact for every wash a document can state, and it needs no
46
+ * decoder: the picture's own pixels are not rewritten.
47
+ *
48
+ * @param wash The picture's wash, if it has one.
49
+ * @returns The grey (0…1) and opacity of the veil laid over the picture, or
50
+ * nothing where the wash changes nothing.
51
+ */
52
+ export declare function washVeil(wash: {
53
+ readonly gain: number;
54
+ readonly black: number;
55
+ } | undefined): {
56
+ alpha: number;
57
+ grey: number;
58
+ } | undefined;
@@ -0,0 +1,107 @@
1
+ import { reorderVisual } from "../core/bidi/algorithm.js";
2
+ import "../core/bidi/index.js";
3
+ import { GLUE_SHRINK_RATIO } from "./styled-layout.js";
4
+ //#region src/layout/line-paint.ts
5
+ /** The slant of a faked italic — tan(12°), the angle a text italic leans at. */
6
+ var FAUX_ITALIC_SHEAR = .2126;
7
+ /**
8
+ * §18.18.3 — the dash pattern of a rule that is not solid, in points: dashes
9
+ * and gaps alternating, from a dash.
10
+ */
11
+ var BORDER_DASHES = new Map([
12
+ ["dashed", [8, 2.5]],
13
+ ["dashSmallGap", [3, 1]],
14
+ ["dotted", [.5, 1]],
15
+ ["dashDot", [
16
+ 8,
17
+ 2.5,
18
+ 2.5,
19
+ 2.5
20
+ ]],
21
+ ["dashDotDot", [
22
+ 8,
23
+ 2.5,
24
+ 2.5,
25
+ 2.5,
26
+ 2.5,
27
+ 2.5
28
+ ]]
29
+ ]);
30
+ /**
31
+ * UAX #9 rule L2 over a line's tokens: the token indices in visual order. Each
32
+ * token carries a single embedding level (the tokenizer split runs at level
33
+ * boundaries), so reordering tokens is equivalent to reordering their
34
+ * characters.
35
+ *
36
+ * @param line The laid-out line.
37
+ * @returns Its token indices, left to right as they are drawn.
38
+ */
39
+ function lineVisualOrder(line) {
40
+ return reorderVisual(line.tokens.map((t) => t.bidiLevel));
41
+ }
42
+ /**
43
+ * The extra width each space of a justified line takes — 0 for a line that is
44
+ * not justified, or the last line of a paragraph (which stays flush by
45
+ * convention).
46
+ *
47
+ * @param line The laid-out line.
48
+ * @returns Points added to (or, squeezed, taken from) each space token.
49
+ */
50
+ function computeJustifyExtra(line) {
51
+ const alignment = line.resolved.alignment;
52
+ if (alignment !== "both" && alignment !== "distribute") return 0;
53
+ if (line.noJustify === true) return 0;
54
+ if (line.isLastInParagraph && alignment !== "distribute") return 0;
55
+ let last = line.tokens.length - 1;
56
+ while (last >= 0 && line.tokens[last].kind === "text" && line.tokens[last].isSpace) last--;
57
+ let spaces = 0;
58
+ let narrowest = Infinity;
59
+ let contentWidthPt = 0;
60
+ for (let i = 0; i <= last; i++) {
61
+ const tok = line.tokens[i];
62
+ contentWidthPt += tok.widthPt;
63
+ if (!tok.isSpace) continue;
64
+ spaces++;
65
+ if (tok.widthPt < narrowest) narrowest = tok.widthPt;
66
+ }
67
+ if (spaces === 0) return 0;
68
+ const extra = (line.availableWidthPt - contentWidthPt) / spaces;
69
+ return extra < 0 ? Math.max(extra, -narrowest * GLUE_SHRINK_RATIO) : extra;
70
+ }
71
+ /**
72
+ * Whether a line is ONE text matrix and a run of glyphs: nothing in it placed
73
+ * on its own — a tab, a stretched space, a picture, a formula, a right-to-left
74
+ * run, a rise, a faked italic — and nothing drawn beside its glyphs, a run's
75
+ * shading, a highlight or a rule. Only such a line can be turned by its matrix
76
+ * alone; any other is placed token by token in page space.
77
+ *
78
+ * @param line The laid-out line.
79
+ * @returns True when a single text matrix sets the whole of it.
80
+ */
81
+ function settableInOneMatrix(line) {
82
+ return computeJustifyExtra(line) === 0 && line.tokens.every((t) => t.kind === "text" && t.tab !== true && t.bidiLevel % 2 === 0 && t.risePt === void 0 && t.synthetic?.italic !== true && t.highlight !== true && t.resolvedRun.shadingColorHex === void 0 && t.resolvedRun.underline === "none" && !t.resolvedRun.strike);
83
+ }
84
+ /**
85
+ * §14.1.2.10 — the wash a picture is drawn through, as the flat veil it is.
86
+ * `gain`/`blacklevel` are contrast and brightness about mid grey, so a source
87
+ * value `in` prints as `(in - 0.5) * gain + 0.5 + black` — and painting a
88
+ * colour `c` at opacity `a` gives `in * (1 - a) + c * a`, the same line with
89
+ * `a = 1 - gain`. Exact for every wash a document can state, and it needs no
90
+ * decoder: the picture's own pixels are not rewritten.
91
+ *
92
+ * @param wash The picture's wash, if it has one.
93
+ * @returns The grey (0…1) and opacity of the veil laid over the picture, or
94
+ * nothing where the wash changes nothing.
95
+ */
96
+ function washVeil(wash) {
97
+ if (!wash) return void 0;
98
+ const alpha = 1 - wash.gain;
99
+ if (!(alpha > .004)) return void 0;
100
+ const offset = .5 * (1 - wash.gain) + wash.black;
101
+ return {
102
+ alpha: Math.min(1, alpha),
103
+ grey: Math.max(0, Math.min(1, offset / alpha))
104
+ };
105
+ }
106
+ //#endregion
107
+ export { BORDER_DASHES, FAUX_ITALIC_SHEAR, computeJustifyExtra, lineVisualOrder, settableInOneMatrix, washVeil };