ooxml.js 2.6.19 → 2.8.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.
@@ -1,5 +1,6 @@
1
1
  import { attr, childrenWithTag, decodeEntities, rootElement } from "../util.js";
2
2
  import { BUILTIN_NUMBER_FORMATS } from "./number-format.js";
3
+ import { colorToRgbHex, rgbHexToColor } from "document-schema.js";
3
4
  //#region src/typed/xlsx/styles.ts
4
5
  const STYLES_PATH = "xl/styles.xml";
5
6
  const GENERAL_NUM_FMT_ID = 0;
@@ -16,37 +17,249 @@ function readNumberFormatCodesById(styleSheet) {
16
17
  }
17
18
  return codes;
18
19
  }
19
- function readCellFormatCodes(pkg) {
20
+ const BORDER_WIDTH_PT = {
21
+ hair: .5,
22
+ thin: .75,
23
+ medium: 1.5,
24
+ thick: 2.25
25
+ };
26
+ const XLSX_BORDER_STYLE = {
27
+ thin: {
28
+ weight: "thin",
29
+ pattern: "solid"
30
+ },
31
+ medium: {
32
+ weight: "medium",
33
+ pattern: "solid"
34
+ },
35
+ thick: {
36
+ weight: "thick",
37
+ pattern: "solid"
38
+ },
39
+ hair: {
40
+ weight: "hair",
41
+ pattern: "solid"
42
+ },
43
+ dashed: {
44
+ weight: "thin",
45
+ pattern: "dashed"
46
+ },
47
+ dotted: {
48
+ weight: "thin",
49
+ pattern: "dotted"
50
+ },
51
+ double: {
52
+ weight: "thin",
53
+ pattern: "double"
54
+ },
55
+ mediumDashed: {
56
+ weight: "medium",
57
+ pattern: "dashed"
58
+ },
59
+ dashDot: {
60
+ weight: "thin",
61
+ pattern: "dashed"
62
+ },
63
+ mediumDashDot: {
64
+ weight: "medium",
65
+ pattern: "dashed"
66
+ },
67
+ dashDotDot: {
68
+ weight: "thin",
69
+ pattern: "dashed"
70
+ },
71
+ mediumDashDotDot: {
72
+ weight: "medium",
73
+ pattern: "dashed"
74
+ },
75
+ slantDashDot: {
76
+ weight: "medium",
77
+ pattern: "dashed"
78
+ }
79
+ };
80
+ const BORDER_WEIGHT_UPPER_PT = {
81
+ hair: .625,
82
+ thin: 1.125,
83
+ medium: 1.875
84
+ };
85
+ function readColorRgb(container, colorTag) {
86
+ const colorEl = childrenWithTag(container, colorTag)[0];
87
+ if (colorEl === void 0) return;
88
+ const raw = attr(colorEl, "rgb");
89
+ if (raw === void 0) return;
90
+ const hex = raw.length >= 6 ? raw.slice(-6) : raw;
91
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return;
92
+ try {
93
+ return rgbHexToColor(hex);
94
+ } catch {
95
+ return;
96
+ }
97
+ }
98
+ function readFillBackground(fill) {
99
+ const patternFill = childrenWithTag(fill, "patternFill")[0];
100
+ if (patternFill === void 0 || attr(patternFill, "patternType") !== "solid") return;
101
+ return readColorRgb(patternFill, "fgColor") ?? readColorRgb(patternFill, "bgColor");
102
+ }
103
+ function readFills(styleSheet) {
104
+ const fillsEl = childrenWithTag(styleSheet, "fills")[0];
105
+ if (fillsEl === void 0) return [];
106
+ return childrenWithTag(fillsEl, "fill").map(readFillBackground);
107
+ }
108
+ function readBorderEdge(border, edge) {
109
+ const edgeEl = childrenWithTag(border, edge)[0];
110
+ if (edgeEl === void 0) return;
111
+ const styleToken = attr(edgeEl, "style");
112
+ if (styleToken === void 0 || styleToken === "none") return;
113
+ const resolved = XLSX_BORDER_STYLE[styleToken];
114
+ if (resolved === void 0) return;
115
+ const color = readColorRgb(edgeEl, "color");
116
+ if (color === void 0) return;
117
+ const result = {
118
+ color,
119
+ widthPt: BORDER_WIDTH_PT[resolved.weight]
120
+ };
121
+ if (resolved.pattern !== "solid") result.style = resolved.pattern;
122
+ return result;
123
+ }
124
+ function readBorders(styleSheet) {
125
+ const bordersEl = childrenWithTag(styleSheet, "borders")[0];
126
+ if (bordersEl === void 0) return [];
127
+ return childrenWithTag(bordersEl, "border").map((border) => {
128
+ const left = readBorderEdge(border, "left");
129
+ const right = readBorderEdge(border, "right");
130
+ const top = readBorderEdge(border, "top");
131
+ const bottom = readBorderEdge(border, "bottom");
132
+ if (left === void 0 && right === void 0 && top === void 0 && bottom === void 0) return;
133
+ const result = {};
134
+ if (left !== void 0) result.left = left;
135
+ if (right !== void 0) result.right = right;
136
+ if (top !== void 0) result.top = top;
137
+ if (bottom !== void 0) result.bottom = bottom;
138
+ return result;
139
+ });
140
+ }
141
+ function readHorizontalAlignment(alignment) {
142
+ const value = attr(alignment, "horizontal");
143
+ if (value === "left" || value === "center" || value === "right" || value === "justify") return value;
144
+ }
145
+ function readVerticalAlignment(alignment) {
146
+ const value = attr(alignment, "vertical");
147
+ if (value === "top") return "top";
148
+ if (value === "center") return "middle";
149
+ }
150
+ function readAlignment(xf) {
151
+ const alignment = childrenWithTag(xf, "alignment")[0];
152
+ if (alignment === void 0) return {};
153
+ return {
154
+ alignment: readHorizontalAlignment(alignment),
155
+ verticalAlignment: readVerticalAlignment(alignment)
156
+ };
157
+ }
158
+ function readCellStyles(pkg) {
20
159
  const styleSheet = rootElement(pkg.parts[STYLES_PATH]);
21
160
  if (styleSheet === void 0) return [];
22
161
  const cellXfsEl = childrenWithTag(styleSheet, "cellXfs")[0];
23
162
  if (cellXfsEl === void 0) return [];
24
163
  const codes = readNumberFormatCodesById(styleSheet);
164
+ const fills = readFills(styleSheet);
165
+ const borders = readBorders(styleSheet);
25
166
  return childrenWithTag(cellXfsEl, "xf").map((xf) => {
26
- const raw = attr(xf, "numFmtId");
27
- const id = raw === void 0 ? 0 : Number.parseInt(raw, 10);
28
- return Number.isInteger(id) ? codes.get(id) : void 0;
167
+ const numFmtRaw = attr(xf, "numFmtId");
168
+ const numFmtId = numFmtRaw === void 0 ? 0 : Number.parseInt(numFmtRaw, 10);
169
+ const entry = {};
170
+ if (Number.isInteger(numFmtId)) {
171
+ const code = codes.get(numFmtId);
172
+ if (code !== void 0) entry.numberFormatCode = code;
173
+ }
174
+ const fillId = parseChildIndex(attr(xf, "fillId"));
175
+ if (fillId !== void 0) entry.background = fills[fillId];
176
+ const borderId = parseChildIndex(attr(xf, "borderId"));
177
+ if (borderId !== void 0) entry.borders = borders[borderId];
178
+ const { alignment, verticalAlignment } = readAlignment(xf);
179
+ if (alignment !== void 0) entry.alignment = alignment;
180
+ if (verticalAlignment !== void 0) entry.verticalAlignment = verticalAlignment;
181
+ return entry;
29
182
  });
30
183
  }
184
+ function parseChildIndex(value) {
185
+ if (value === void 0) return;
186
+ const n = Number.parseInt(value, 10);
187
+ return Number.isInteger(n) ? n : void 0;
188
+ }
189
+ function readCellFormatCodes(pkg) {
190
+ return readCellStyles(pkg).map((entry) => entry.numberFormatCode);
191
+ }
31
192
  const FIRST_CUSTOM_NUM_FMT_ID = 164;
32
193
  const DEFAULT_CELL_FORMAT_INDEX = 0;
33
- function signatureOf(format) {
194
+ const NONE_FILL_INDEX = 0;
195
+ const GRAY125_FILL_INDEX = 1;
196
+ const FIRST_REAL_FILL_INDEX = 2;
197
+ const EMPTY_BORDER_INDEX = 0;
198
+ const FIRST_REAL_BORDER_INDEX = 1;
199
+ function signatureOfNumberFormat(format) {
34
200
  return format.kind === "builtin" ? `builtin:${format.id}` : `custom:${format.code}`;
35
201
  }
202
+ const EMPTY_DECORATION = {};
203
+ function signatureOfDecoration(decoration) {
204
+ let sig = "";
205
+ if (decoration.background !== void 0) sig += `|bg:${colorToRgbHex(decoration.background)}`;
206
+ const borders = decoration.borders;
207
+ if (borders !== void 0) for (const edge of [
208
+ "left",
209
+ "right",
210
+ "top",
211
+ "bottom"
212
+ ]) {
213
+ const border = borders[edge];
214
+ if (border !== void 0) sig += `|${edge}:${border.style ?? "solid"}:${colorToRgbHex(border.color)}:${border.widthPt.toFixed(4)}`;
215
+ }
216
+ if (decoration.alignment !== void 0) sig += `|h:${decoration.alignment}`;
217
+ if (decoration.verticalAlignment !== void 0) sig += `|v:${decoration.verticalAlignment}`;
218
+ return sig;
219
+ }
220
+ function borderToXlsxStyle(border) {
221
+ switch (border.style) {
222
+ case "double": return "double";
223
+ case "dotted": return "dotted";
224
+ case "dashed": return border.widthPt >= BORDER_WEIGHT_UPPER_PT.thin ? "mediumDashed" : "dashed";
225
+ case "solid":
226
+ case void 0:
227
+ if (border.widthPt < BORDER_WEIGHT_UPPER_PT.hair) return "hair";
228
+ if (border.widthPt < BORDER_WEIGHT_UPPER_PT.thin) return "thin";
229
+ if (border.widthPt < BORDER_WEIGHT_UPPER_PT.medium) return "medium";
230
+ return "thick";
231
+ }
232
+ }
36
233
  var CellFormatTable = class {
37
- indexBySignature = /* @__PURE__ */ new Map([[signatureOf({
234
+ indexBySignature = /* @__PURE__ */ new Map([[signatureOfNumberFormat({
38
235
  kind: "builtin",
39
236
  id: 0
40
- }), 0]]);
41
- numFmtIdByIndex = [0];
237
+ }) + signatureOfDecoration(EMPTY_DECORATION), 0]]);
238
+ records = [{
239
+ numFmtId: 0,
240
+ fillId: NONE_FILL_INDEX,
241
+ borderId: EMPTY_BORDER_INDEX
242
+ }];
42
243
  declared = [];
43
- intern(format) {
44
- const signature = signatureOf(format);
244
+ fillIndexByRgb = /* @__PURE__ */ new Map();
245
+ fills = [{ patternType: "none" }, { patternType: "gray125" }];
246
+ borderIndexBySignature = /* @__PURE__ */ new Map();
247
+ borders = [{ edges: {} }];
248
+ intern(format, decoration = EMPTY_DECORATION) {
249
+ const signature = signatureOfNumberFormat(format) + signatureOfDecoration(decoration);
45
250
  const existing = this.indexBySignature.get(signature);
46
251
  if (existing !== void 0) return existing;
47
- const numFmtId = format.kind === "builtin" ? format.id : this.declare(format.code);
48
- const index = this.numFmtIdByIndex.length;
49
- this.numFmtIdByIndex.push(numFmtId);
252
+ const record = {
253
+ numFmtId: format.kind === "builtin" ? format.id : this.declareNumberFormat(format.code),
254
+ fillId: decoration.background === void 0 ? NONE_FILL_INDEX : this.internFill(decoration.background),
255
+ borderId: decoration.borders === void 0 ? EMPTY_BORDER_INDEX : this.internBorder(decoration.borders)
256
+ };
257
+ if (decoration.alignment !== void 0 || decoration.verticalAlignment !== void 0) record.alignment = {
258
+ horizontal: decoration.alignment,
259
+ vertical: decoration.verticalAlignment
260
+ };
261
+ const index = this.records.length;
262
+ this.records.push(record);
50
263
  this.indexBySignature.set(signature, index);
51
264
  return index;
52
265
  }
@@ -54,9 +267,18 @@ var CellFormatTable = class {
54
267
  return this.declared;
55
268
  }
56
269
  cellFormats() {
57
- return this.numFmtIdByIndex;
270
+ return this.records.map((record) => record.numFmtId);
58
271
  }
59
- declare(code) {
272
+ cellFormatRecords() {
273
+ return this.records;
274
+ }
275
+ fillDeclarations() {
276
+ return this.fills;
277
+ }
278
+ borderDeclarations() {
279
+ return this.borders;
280
+ }
281
+ declareNumberFormat(code) {
60
282
  const id = FIRST_CUSTOM_NUM_FMT_ID + this.declared.length;
61
283
  this.declared.push({
62
284
  id,
@@ -64,6 +286,54 @@ var CellFormatTable = class {
64
286
  });
65
287
  return id;
66
288
  }
289
+ internFill(background) {
290
+ const rgb = colorToRgbHex(background);
291
+ const existing = this.fillIndexByRgb.get(rgb);
292
+ if (existing !== void 0) return existing;
293
+ const index = this.fills.length;
294
+ this.fills.push({
295
+ patternType: "solid",
296
+ rgb
297
+ });
298
+ this.fillIndexByRgb.set(rgb, index);
299
+ return index;
300
+ }
301
+ internBorder(borders) {
302
+ const edges = {};
303
+ let signature = "";
304
+ for (const edge of [
305
+ "left",
306
+ "right",
307
+ "top",
308
+ "bottom"
309
+ ]) {
310
+ const border = borders[edge];
311
+ if (border !== void 0) {
312
+ const style = borderToXlsxStyle(border);
313
+ const rgb = colorToRgbHex(border.color);
314
+ edges[edge] = {
315
+ style,
316
+ rgb
317
+ };
318
+ signature += `|${edge}:${style}:${rgb}`;
319
+ }
320
+ }
321
+ const existing = this.borderIndexBySignature.get(signature);
322
+ if (existing !== void 0) return existing;
323
+ const index = this.borders.length;
324
+ this.borders.push({ edges });
325
+ this.borderIndexBySignature.set(signature, index);
326
+ return index;
327
+ }
328
+ };
329
+ const RESERVED_FILL_INDICES = {
330
+ none: NONE_FILL_INDEX,
331
+ gray125: GRAY125_FILL_INDEX,
332
+ firstReal: FIRST_REAL_FILL_INDEX
333
+ };
334
+ const RESERVED_BORDER_INDICES = {
335
+ empty: EMPTY_BORDER_INDEX,
336
+ firstReal: FIRST_REAL_BORDER_INDEX
67
337
  };
68
338
  //#endregion
69
- export { CellFormatTable, DEFAULT_CELL_FORMAT_INDEX, GENERAL_NUM_FMT_ID, readCellFormatCodes };
339
+ export { CellFormatTable, DEFAULT_CELL_FORMAT_INDEX, GENERAL_NUM_FMT_ID, RESERVED_BORDER_INDICES, RESERVED_FILL_INDICES, readCellFormatCodes, readCellStyles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml.js",
3
- "version": "2.6.19",
3
+ "version": "2.8.0",
4
4
  "description": "Type-safe, lossless round-trip conversion between OOXML packages (docx, pptx, xlsx) and JSON, built on Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {