@lotics/xlsx 0.1.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/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Types
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
export interface ThemeData {
|
|
8
|
+
colors: string[]; // 12 entries: lt1, dk1, lt2, dk2, accent1-6, hlink, folHlink
|
|
9
|
+
majorFont: string;
|
|
10
|
+
minorFont: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OoxmlColor {
|
|
14
|
+
rgb?: string;
|
|
15
|
+
theme?: number;
|
|
16
|
+
indexed?: number;
|
|
17
|
+
tint?: number;
|
|
18
|
+
auto?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Default indexed color palette (64 colors, indices 0-63)
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_INDEXED_COLORS: string[] = [
|
|
26
|
+
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", // 0-7
|
|
27
|
+
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", // 8-15
|
|
28
|
+
"800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080", // 16-23
|
|
29
|
+
"9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF", // 24-31
|
|
30
|
+
"000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF", // 32-39
|
|
31
|
+
"00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99", // 40-47
|
|
32
|
+
"3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696", // 48-55
|
|
33
|
+
"003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333", // 56-63
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Theme parsing
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
const xmlParser = new XMLParser({
|
|
41
|
+
ignoreAttributes: false,
|
|
42
|
+
attributeNamePrefix: "@_",
|
|
43
|
+
processEntities: false,
|
|
44
|
+
isArray: (tagName) => tagName === "a:srgbClr" || tagName === "a:sysClr",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function parseTheme(themeXml: string): ThemeData {
|
|
48
|
+
const doc = xmlParser.parse(themeXml);
|
|
49
|
+
const themeElements = doc?.["a:theme"]?.["a:themeElements"];
|
|
50
|
+
if (!themeElements) {
|
|
51
|
+
return { colors: defaultThemeColors(), majorFont: "Calibri", minorFont: "Calibri" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Color scheme
|
|
55
|
+
const clrScheme = themeElements["a:clrScheme"];
|
|
56
|
+
const colors = parseColorScheme(clrScheme);
|
|
57
|
+
|
|
58
|
+
// Font scheme
|
|
59
|
+
const fontScheme = themeElements["a:fontScheme"];
|
|
60
|
+
const majorFont = extractFontName(fontScheme?.["a:majorFont"]) ?? "Calibri";
|
|
61
|
+
const minorFont = extractFontName(fontScheme?.["a:minorFont"]) ?? "Calibri";
|
|
62
|
+
|
|
63
|
+
return { colors, majorFont, minorFont };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** XML color scheme order: dk1, lt1, dk2, lt2, accent1-6, hlink, folHlink */
|
|
67
|
+
const COLOR_SCHEME_KEYS = [
|
|
68
|
+
"a:dk1", "a:lt1", "a:dk2", "a:lt2",
|
|
69
|
+
"a:accent1", "a:accent2", "a:accent3", "a:accent4", "a:accent5", "a:accent6",
|
|
70
|
+
"a:hlink", "a:folHlink",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function parseColorScheme(clrScheme: Record<string, unknown> | undefined): string[] {
|
|
74
|
+
if (!clrScheme) return defaultThemeColors();
|
|
75
|
+
|
|
76
|
+
const colors: string[] = [];
|
|
77
|
+
for (const key of COLOR_SCHEME_KEYS) {
|
|
78
|
+
const el = clrScheme[key] as Record<string, unknown> | undefined;
|
|
79
|
+
colors.push(extractSchemeColor(el));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reorder: XML is [dk1, lt1, dk2, lt2, ...] but theme index mapping needs
|
|
83
|
+
// [lt1, dk1, lt2, dk2, ...] — swap 0↔1 and 2↔3
|
|
84
|
+
const reordered = [...colors];
|
|
85
|
+
reordered[0] = colors[1]; // lt1
|
|
86
|
+
reordered[1] = colors[0]; // dk1
|
|
87
|
+
reordered[2] = colors[3]; // lt2
|
|
88
|
+
reordered[3] = colors[2]; // dk2
|
|
89
|
+
return reordered;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractSchemeColor(el: Record<string, unknown> | undefined): string {
|
|
93
|
+
if (!el) return "000000";
|
|
94
|
+
|
|
95
|
+
// srgbClr has @_val attribute directly
|
|
96
|
+
const srgb = el["a:srgbClr"];
|
|
97
|
+
if (srgb) {
|
|
98
|
+
const val = Array.isArray(srgb) ? srgb[0] : srgb;
|
|
99
|
+
const hex = typeof val === "object" && val !== null ? (val as Record<string, string>)["@_val"] : undefined;
|
|
100
|
+
if (hex) return hex;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// sysClr has @_lastClr attribute
|
|
104
|
+
const sys = el["a:sysClr"];
|
|
105
|
+
if (sys) {
|
|
106
|
+
const val = Array.isArray(sys) ? sys[0] : sys;
|
|
107
|
+
const lastClr = typeof val === "object" && val !== null ? (val as Record<string, string>)["@_lastClr"] : undefined;
|
|
108
|
+
if (lastClr) return lastClr;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return "000000";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractFontName(fontEl: Record<string, unknown> | undefined): string | undefined {
|
|
115
|
+
if (!fontEl) return undefined;
|
|
116
|
+
const latin = fontEl["a:latin"] as Record<string, string> | undefined;
|
|
117
|
+
return latin?.["@_typeface"];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function defaultThemeColors(): string[] {
|
|
121
|
+
return [
|
|
122
|
+
"FFFFFF", "000000", "E7E6E6", "44546A",
|
|
123
|
+
"4472C4", "ED7D31", "A5A5A5", "FFC000",
|
|
124
|
+
"5B9BD5", "70AD47", "0563C1", "954F72",
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Color resolution
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
/** Parse a color element's attributes into an OoxmlColor. */
|
|
133
|
+
export function parseColorElement(attrs: Record<string, string>): OoxmlColor {
|
|
134
|
+
const color: OoxmlColor = {};
|
|
135
|
+
if (attrs["@_rgb"] !== undefined) color.rgb = attrs["@_rgb"];
|
|
136
|
+
if (attrs["@_theme"] !== undefined) color.theme = parseInt(attrs["@_theme"], 10);
|
|
137
|
+
if (attrs["@_indexed"] !== undefined) color.indexed = parseInt(attrs["@_indexed"], 10);
|
|
138
|
+
if (attrs["@_tint"] !== undefined) color.tint = parseFloat(attrs["@_tint"]);
|
|
139
|
+
if (attrs["@_auto"] !== undefined) color.auto = attrs["@_auto"] === "1" || attrs["@_auto"] === "true";
|
|
140
|
+
return color;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve an OOXML color to a CSS hex string (6-char, no #).
|
|
145
|
+
* Returns undefined if the color is auto/unresolvable.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveColor(
|
|
148
|
+
color: OoxmlColor | undefined,
|
|
149
|
+
theme: ThemeData,
|
|
150
|
+
indexedColors: string[],
|
|
151
|
+
context: "font" | "fill" | "border",
|
|
152
|
+
): string | undefined {
|
|
153
|
+
if (!color) return undefined;
|
|
154
|
+
|
|
155
|
+
// Auto color: black for font, undefined for fill/border
|
|
156
|
+
if (color.auto) {
|
|
157
|
+
return context === "font" ? "000000" : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let hex: string | undefined;
|
|
161
|
+
|
|
162
|
+
if (color.rgb !== undefined) {
|
|
163
|
+
// RGB can be AARRGGBB (8 chars) or RRGGBB (6 chars)
|
|
164
|
+
hex = color.rgb.length === 8 ? color.rgb.slice(2) : color.rgb;
|
|
165
|
+
} else if (color.theme !== undefined) {
|
|
166
|
+
const idx = color.theme;
|
|
167
|
+
if (idx >= 0 && idx < theme.colors.length) {
|
|
168
|
+
hex = theme.colors[idx];
|
|
169
|
+
}
|
|
170
|
+
} else if (color.indexed !== undefined) {
|
|
171
|
+
const idx = color.indexed;
|
|
172
|
+
if (idx >= 0 && idx < indexedColors.length) {
|
|
173
|
+
hex = indexedColors[idx];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!hex) return undefined;
|
|
178
|
+
|
|
179
|
+
// Apply tint
|
|
180
|
+
if (color.tint !== undefined && color.tint !== 0) {
|
|
181
|
+
hex = applyTint(hex, color.tint);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return hex;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// Canonical color format
|
|
189
|
+
// =============================================================================
|
|
190
|
+
//
|
|
191
|
+
// Internal canonical color format throughout @lotics/xlsx is `#RRGGBB`
|
|
192
|
+
// (uppercase, hash-prefixed). Every consumer reads/writes that format.
|
|
193
|
+
// Inputs from outside the package (parser, tools, frontend pickers) flow
|
|
194
|
+
// through `normalizeColor()` at ingress; OOXML writers convert via
|
|
195
|
+
// `hexToArgb()` (8-char ARGB) or `hexToOoxmlRgb()` (6-char RGB for chart XML)
|
|
196
|
+
// at egress.
|
|
197
|
+
|
|
198
|
+
/** Prefix a 6-char internal hex (`RRGGBB`) with `#`. */
|
|
199
|
+
export function prefixHex(hex6: string): string {
|
|
200
|
+
if (hex6.length !== 6) return "#000000";
|
|
201
|
+
return `#${hex6.toUpperCase()}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Coerce a color string in any supported input format to canonical `#RRGGBB`.
|
|
206
|
+
* Accepts `#RRGGBB`, `RRGGBB`, `#RGB`, `rgb(r, g, b)`, `rgba(r, g, b, a)`.
|
|
207
|
+
* Returns undefined for empty or unparseable input.
|
|
208
|
+
*/
|
|
209
|
+
export function normalizeColor(input: string | undefined): string | undefined {
|
|
210
|
+
if (input === undefined) return undefined;
|
|
211
|
+
const s = input.trim();
|
|
212
|
+
if (s === "") return undefined;
|
|
213
|
+
|
|
214
|
+
// rgb()/rgba()
|
|
215
|
+
const rgbMatch = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
216
|
+
if (rgbMatch) {
|
|
217
|
+
return `#${toHex2(parseInt(rgbMatch[1], 10))}${toHex2(parseInt(rgbMatch[2], 10))}${toHex2(parseInt(rgbMatch[3], 10))}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// #RRGGBB
|
|
221
|
+
const hex6Match = s.match(/^#([0-9a-fA-F]{6})$/);
|
|
222
|
+
if (hex6Match) return `#${hex6Match[1].toUpperCase()}`;
|
|
223
|
+
|
|
224
|
+
// RRGGBB (no hash)
|
|
225
|
+
const bareHex6Match = s.match(/^([0-9a-fA-F]{6})$/);
|
|
226
|
+
if (bareHex6Match) return `#${bareHex6Match[1].toUpperCase()}`;
|
|
227
|
+
|
|
228
|
+
// #RGB shorthand
|
|
229
|
+
const hex3Match = s.match(/^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/);
|
|
230
|
+
if (hex3Match) {
|
|
231
|
+
const r = hex3Match[1];
|
|
232
|
+
const g = hex3Match[2];
|
|
233
|
+
const b = hex3Match[3];
|
|
234
|
+
return `#${(r + r + g + g + b + b).toUpperCase()}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Convert a normalized `#RRGGBB` color to OOXML 8-char ARGB (`FFRRGGBB`).
|
|
242
|
+
* Accepts unnormalized input as a courtesy — runs `normalizeColor` first.
|
|
243
|
+
*/
|
|
244
|
+
export function hexToArgb(input: string | undefined): string {
|
|
245
|
+
const norm = normalizeColor(input);
|
|
246
|
+
if (!norm) return "FF000000";
|
|
247
|
+
return `FF${norm.slice(1)}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert a normalized `#RRGGBB` color to OOXML 6-char hex (`RRGGBB`),
|
|
252
|
+
* used by drawing/chart XML where `<a:srgbClr val="…"/>` requires no alpha.
|
|
253
|
+
*/
|
|
254
|
+
export function hexToOoxmlRgb(input: string | undefined): string {
|
|
255
|
+
const norm = normalizeColor(input);
|
|
256
|
+
if (!norm) return "000000";
|
|
257
|
+
return norm.slice(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// =============================================================================
|
|
261
|
+
// Tint algorithm
|
|
262
|
+
// =============================================================================
|
|
263
|
+
|
|
264
|
+
/** Apply Excel tint to a 6-char hex color. Returns 6-char hex. */
|
|
265
|
+
export function applyTint(hex: string, tint: number): string {
|
|
266
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
267
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
268
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
269
|
+
|
|
270
|
+
const { h, s, l } = rgbToHsl(r, g, b);
|
|
271
|
+
|
|
272
|
+
let newL: number;
|
|
273
|
+
if (tint < 0) {
|
|
274
|
+
newL = l * (1 + tint);
|
|
275
|
+
} else {
|
|
276
|
+
newL = l + (1 - l) * tint;
|
|
277
|
+
}
|
|
278
|
+
newL = Math.max(0, Math.min(1, newL));
|
|
279
|
+
|
|
280
|
+
const { r: nr, g: ng, b: nb } = hslToRgb(h, s, newL);
|
|
281
|
+
return toHex2(nr) + toHex2(ng) + toHex2(nb);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function toHex2(n: number): string {
|
|
285
|
+
const clamped = Math.max(0, Math.min(255, Math.round(n)));
|
|
286
|
+
return clamped.toString(16).padStart(2, "0").toUpperCase();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
|
290
|
+
const rf = r / 255;
|
|
291
|
+
const gf = g / 255;
|
|
292
|
+
const bf = b / 255;
|
|
293
|
+
|
|
294
|
+
const max = Math.max(rf, gf, bf);
|
|
295
|
+
const min = Math.min(rf, gf, bf);
|
|
296
|
+
const l = (max + min) / 2;
|
|
297
|
+
|
|
298
|
+
if (max === min) return { h: 0, s: 0, l };
|
|
299
|
+
|
|
300
|
+
const d = max - min;
|
|
301
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
302
|
+
|
|
303
|
+
let h: number;
|
|
304
|
+
if (max === rf) {
|
|
305
|
+
h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
|
|
306
|
+
} else if (max === gf) {
|
|
307
|
+
h = ((bf - rf) / d + 2) / 6;
|
|
308
|
+
} else {
|
|
309
|
+
h = ((rf - gf) / d + 4) / 6;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { h, s, l };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
|
|
316
|
+
if (s === 0) {
|
|
317
|
+
const v = Math.round(l * 255);
|
|
318
|
+
return { r: v, g: v, b: v };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
322
|
+
const p = 2 * l - q;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
r: Math.round(hueToRgb(p, q, h + 1 / 3) * 255),
|
|
326
|
+
g: Math.round(hueToRgb(p, q, h) * 255),
|
|
327
|
+
b: Math.round(hueToRgb(p, q, h - 1 / 3) * 255),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function hueToRgb(p: number, q: number, t: number): number {
|
|
332
|
+
let tt = t;
|
|
333
|
+
if (tt < 0) tt += 1;
|
|
334
|
+
if (tt > 1) tt -= 1;
|
|
335
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
336
|
+
if (tt < 1 / 2) return q;
|
|
337
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
338
|
+
return p;
|
|
339
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
import { parseDrawingXml } from "./ooxml_drawing";
|
|
4
|
+
|
|
5
|
+
const parser = new XMLParser({
|
|
6
|
+
ignoreAttributes: false,
|
|
7
|
+
attributeNamePrefix: "@_",
|
|
8
|
+
isArray: (tagName) =>
|
|
9
|
+
tagName === "xdr:twoCellAnchor" || tagName === "xdr:oneCellAnchor",
|
|
10
|
+
});
|
|
11
|
+
const parseXml = (xml: string) => parser.parse(xml) as Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
const CHART_DRAWING_XML = `<?xml version="1.0"?>
|
|
14
|
+
<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">
|
|
15
|
+
<xdr:twoCellAnchor>
|
|
16
|
+
<xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>2</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
|
|
17
|
+
<xdr:to><xdr:col>5</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>10</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
|
|
18
|
+
<xdr:graphicFrame>
|
|
19
|
+
<a:graphic><a:graphicData><c:chart r:id="rId1"/></a:graphicData></a:graphic>
|
|
20
|
+
</xdr:graphicFrame>
|
|
21
|
+
</xdr:twoCellAnchor>
|
|
22
|
+
</xdr:wsDr>`;
|
|
23
|
+
|
|
24
|
+
const SHAPE_DRAWING_XML = `<?xml version="1.0"?>
|
|
25
|
+
<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
26
|
+
<xdr:twoCellAnchor>
|
|
27
|
+
<xdr:from><xdr:col>0</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
|
|
28
|
+
<xdr:to><xdr:col>3</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>4</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
|
|
29
|
+
<xdr:sp>
|
|
30
|
+
<xdr:spPr>
|
|
31
|
+
<a:prstGeom prst="rect"/>
|
|
32
|
+
<a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>
|
|
33
|
+
</xdr:spPr>
|
|
34
|
+
<xdr:txBody><a:p><a:r><a:t>Hello</a:t></a:r></a:p></xdr:txBody>
|
|
35
|
+
</xdr:sp>
|
|
36
|
+
</xdr:twoCellAnchor>
|
|
37
|
+
</xdr:wsDr>`;
|
|
38
|
+
|
|
39
|
+
const CONNECTOR_DRAWING_XML = `<?xml version="1.0"?>
|
|
40
|
+
<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
41
|
+
<xdr:twoCellAnchor>
|
|
42
|
+
<xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>1</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
|
|
43
|
+
<xdr:to><xdr:col>4</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>4</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
|
|
44
|
+
<xdr:cxnSp>
|
|
45
|
+
<xdr:spPr>
|
|
46
|
+
<a:ln><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:ln>
|
|
47
|
+
</xdr:spPr>
|
|
48
|
+
</xdr:cxnSp>
|
|
49
|
+
</xdr:twoCellAnchor>
|
|
50
|
+
</xdr:wsDr>`;
|
|
51
|
+
|
|
52
|
+
describe("parseDrawingXml", () => {
|
|
53
|
+
it("parses two-cell anchor coordinates", () => {
|
|
54
|
+
const results = parseDrawingXml(CHART_DRAWING_XML, parseXml);
|
|
55
|
+
expect(results).toHaveLength(1);
|
|
56
|
+
const anchor = results[0].anchor;
|
|
57
|
+
expect(anchor.from).toEqual({ row: 2, col: 1, rowOffset: 0, colOffset: 0 });
|
|
58
|
+
expect(anchor.to).toEqual({ row: 10, col: 5, rowOffset: 0, colOffset: 0 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("identifies chart graphic frame and extracts rId", () => {
|
|
62
|
+
const results = parseDrawingXml(CHART_DRAWING_XML, parseXml);
|
|
63
|
+
expect(results[0].type).toBe("chart");
|
|
64
|
+
expect(results[0].rId).toBe("rId1");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("parses shape with text and fill", () => {
|
|
68
|
+
const results = parseDrawingXml(SHAPE_DRAWING_XML, parseXml);
|
|
69
|
+
expect(results).toHaveLength(1);
|
|
70
|
+
expect(results[0].type).toBe("shape");
|
|
71
|
+
expect(results[0].drawing).toBeDefined();
|
|
72
|
+
expect(results[0].drawing!.type).toBe("textbox");
|
|
73
|
+
expect(results[0].drawing!.text).toBe("Hello");
|
|
74
|
+
expect(results[0].drawing!.fillColor).toBe("#FF0000");
|
|
75
|
+
expect(results[0].drawing!.geometry).toBe("rect");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("parses connector with outline color", () => {
|
|
79
|
+
const results = parseDrawingXml(CONNECTOR_DRAWING_XML, parseXml);
|
|
80
|
+
expect(results).toHaveLength(1);
|
|
81
|
+
expect(results[0].type).toBe("shape");
|
|
82
|
+
expect(results[0].drawing).toBeDefined();
|
|
83
|
+
expect(results[0].drawing!.type).toBe("connector");
|
|
84
|
+
expect(results[0].drawing!.geometry).toBe("line");
|
|
85
|
+
expect(results[0].drawing!.outlineColor).toBe("#0000FF");
|
|
86
|
+
});
|
|
87
|
+
});
|