@rowsncolumns/toolkit 24.5.4 → 24.5.5
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/dist/cjs/autofilter-condition.js +6 -2
- package/dist/cjs/excel-parser/helpers.d.ts +9 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +70 -16
- package/dist/cjs/excel-parser/interface.d.ts +9 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.js +61 -6
- package/dist/cjs/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/data-validations.js +13 -2
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +8 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +159 -26
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +17 -2
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/styles.js +25 -14
- package/dist/cjs/excel-parser/types.d.ts +5 -1
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +4 -0
- package/dist/cjs/excel-parser/worker.js +3 -0
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +167 -53
- package/dist/cjs/exporter/xlsx/future-functions.d.ts +6 -7
- package/dist/cjs/exporter/xlsx/future-functions.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/future-functions.js +5 -261
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/style-builder.js +62 -19
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +10 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +404 -158
- package/dist/cjs/helpers/excel-formula-prefixes.d.ts +51 -0
- package/dist/cjs/helpers/excel-formula-prefixes.d.ts.map +1 -0
- package/dist/cjs/helpers/excel-formula-prefixes.js +548 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/autofilter-condition.js +6 -2
- package/dist/esm/excel-parser/helpers.d.ts +9 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +65 -3
- package/dist/esm/excel-parser/interface.d.ts +9 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.js +42 -1
- package/dist/esm/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/data-validations.js +11 -2
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +8 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +109 -4
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +17 -2
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/styles.js +20 -8
- package/dist/esm/excel-parser/types.d.ts +5 -1
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +3 -0
- package/dist/esm/excel-parser/worker.js +3 -0
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +80 -0
- package/dist/esm/exporter/xlsx/future-functions.d.ts +6 -7
- package/dist/esm/exporter/xlsx/future-functions.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/future-functions.js +5 -261
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/style-builder.js +56 -18
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +10 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +338 -106
- package/dist/esm/helpers/excel-formula-prefixes.d.ts +51 -0
- package/dist/esm/helpers/excel-formula-prefixes.d.ts.map +1 -0
- package/dist/esm/helpers/excel-formula-prefixes.js +531 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, isNil, sortSheetsByIndex, } from "@rowsncolumns/utils";
|
|
1
|
+
import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, isNil, sortSheetsByIndex, conditionOperandToFormulaText, } from "@rowsncolumns/utils";
|
|
2
2
|
import { zipSync } from "fflate";
|
|
3
3
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
4
4
|
import { buildAutoFilterCriteriaXml } from "../../autofilter-condition";
|
|
@@ -25,7 +25,7 @@ const isStyleReference = (value) => !!value && typeof value === "object" && "sid
|
|
|
25
25
|
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
26
26
|
const DEFAULT_ROW_HEIGHT_PX = 21;
|
|
27
27
|
const pixelToPoints = (px) => Math.max(1, Number(((px * 72) / 96).toFixed(2)));
|
|
28
|
-
export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], protectedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
|
|
28
|
+
export const createExcelFile = async ({ conditionalFormats: _conditionalFormats, dataValidations: _dataValidations, sheets, sheetData, sharedStrings: sourceSharedStrings, tables: _tables = [], cellXfs, theme, defaultFont, embeds = [], charts = [], slicers = [], pivotTables = [], externalLinks = [], namedRanges = [], protectedRanges = [], namedCellStyles, customTableStyles, iterativeCalculationOptions, workbookProtection, ignoreFormulaPatterns, citations, comments, password, locale, use1904 = false, properties, powerQuery, }) => {
|
|
29
29
|
const sortedSheets = sortSheetsByIndex(sheets, true);
|
|
30
30
|
const sharedStrings = new SharedStrings();
|
|
31
31
|
// Workbook-scoped author registry shared across every sheet's threaded
|
|
@@ -317,13 +317,17 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
317
317
|
commentsXml: result.commentsXml,
|
|
318
318
|
vmlDrawingXml: result.vmlDrawingXml,
|
|
319
319
|
threadedCommentsXml: result.threadedCommentsXml,
|
|
320
|
+
hasDynamicArrays: result.hasDynamicArrays,
|
|
320
321
|
};
|
|
321
322
|
});
|
|
323
|
+
// Dynamic-array anchors (`cm="1"`) index the XLDAPR record of one
|
|
324
|
+
// workbook-level xl/metadata.xml part; without it Excel repairs the file.
|
|
325
|
+
const hasDynamicArrays = worksheetEntries.some((entry) => entry.hasDynamicArrays);
|
|
322
326
|
const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.map((entry) => ({
|
|
323
327
|
chartsheet: entry.isChartsheet,
|
|
324
328
|
target: entry.path.replace(/^xl\//, ""),
|
|
325
|
-
})), slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty());
|
|
326
|
-
const workbookXml = buildWorkbookXml(sortedSheets, sheetRelationshipIds, workbookSheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelationshipIds, use1904, externalLinkRelationshipIds, pivotCacheRelationshipIds);
|
|
329
|
+
})), slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty(), hasDynamicArrays);
|
|
330
|
+
const workbookXml = buildWorkbookXml(sortedSheets, sheetRelationshipIds, workbookSheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelationshipIds, use1904, externalLinkRelationshipIds, pivotCacheRelationshipIds, workbookProtection);
|
|
327
331
|
const sharedStringsXml = buildSharedStringsXml(sharedStrings);
|
|
328
332
|
const rootRelationshipsXml = buildRootRelationshipsXml();
|
|
329
333
|
const appXml = buildAppXml(workbookSheetNames, sortedSheets.map((sheet) => sheet.type === "chartsheet"), properties);
|
|
@@ -350,6 +354,9 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
350
354
|
"xl/styles.xml": encodeXml(stylesXml),
|
|
351
355
|
"xl/theme/theme1.xml": encodeXml(themeXml),
|
|
352
356
|
};
|
|
357
|
+
if (hasDynamicArrays) {
|
|
358
|
+
files["xl/metadata.xml"] = encodeXml(DYNAMIC_ARRAY_METADATA_XML);
|
|
359
|
+
}
|
|
353
360
|
for (const entry of worksheetEntries) {
|
|
354
361
|
files[entry.path] = encodeXml(entry.content);
|
|
355
362
|
if (entry.relationshipsXml) {
|
|
@@ -491,6 +498,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
491
498
|
externalLinkCount: externalLinks.length,
|
|
492
499
|
hasVmlDrawings,
|
|
493
500
|
ctrlPropCount: ctrlProps.parts.length,
|
|
501
|
+
hasSheetMetadata: hasDynamicArrays,
|
|
494
502
|
});
|
|
495
503
|
files["[Content_Types].xml"] = encodeXml(contentTypesXml);
|
|
496
504
|
// Preserved Power Query machinery (DataMashup customXml, connections.xml, queryTable parts) is
|
|
@@ -615,9 +623,13 @@ export const normalizeSqrefMembers = (refs) => {
|
|
|
615
623
|
return out;
|
|
616
624
|
};
|
|
617
625
|
class DataValidationCollector {
|
|
626
|
+
locale;
|
|
618
627
|
entries = new Map();
|
|
628
|
+
constructor(locale) {
|
|
629
|
+
this.locale = locale;
|
|
630
|
+
}
|
|
619
631
|
add(address, rule) {
|
|
620
|
-
const config = convertDataValidationRule(rule);
|
|
632
|
+
const config = convertDataValidationRule(rule, this.locale);
|
|
621
633
|
if (!config)
|
|
622
634
|
return;
|
|
623
635
|
const key = getDataValidationKey(config);
|
|
@@ -756,7 +768,7 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
756
768
|
const defaultRowHeightPx = getDefaultRowHeightPx(sheet);
|
|
757
769
|
const rowXmlChunks = [];
|
|
758
770
|
const collectedImages = [];
|
|
759
|
-
const dataValidationCollector = new DataValidationCollector();
|
|
771
|
+
const dataValidationCollector = new DataValidationCollector(locale);
|
|
760
772
|
const hyperlinkEntries = [];
|
|
761
773
|
const commentsCollector = new CommentsCollector();
|
|
762
774
|
const threadedCommentsCollector = new ThreadedCommentsCollector(comments, personRegistry ?? new PersonRegistry());
|
|
@@ -805,6 +817,8 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
805
817
|
// see `<f t="shared" si="N"/>` with no corresponding leader and read the
|
|
806
818
|
// cell as having no formula at all.
|
|
807
819
|
const validSharedFormulaLeaders = new Set();
|
|
820
|
+
// Any `cm="1"` anchor on this sheet makes the workbook carry xl/metadata.xml.
|
|
821
|
+
let hasDynamicArrays = false;
|
|
808
822
|
for (let rowIndex = 1; rowIndex < maxRows; rowIndex++) {
|
|
809
823
|
const row = rows[rowIndex];
|
|
810
824
|
const rowValues = row?.values;
|
|
@@ -839,6 +853,9 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
839
853
|
sheets,
|
|
840
854
|
locale,
|
|
841
855
|
validSharedFormulaLeaders,
|
|
856
|
+
onDynamicArray: () => {
|
|
857
|
+
hasDynamicArrays = true;
|
|
858
|
+
},
|
|
842
859
|
onImage: (image) => {
|
|
843
860
|
collectedImages.push(image);
|
|
844
861
|
drawingContext.ensureRel();
|
|
@@ -888,6 +905,7 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
888
905
|
sheetId: sheet.sheetId,
|
|
889
906
|
conditionalFormats,
|
|
890
907
|
styleBuilder,
|
|
908
|
+
locale,
|
|
891
909
|
});
|
|
892
910
|
const tablePartsXml = buildTablePartsXml(tables, relationships);
|
|
893
911
|
const drawingXml = drawingContext.hasDrawing()
|
|
@@ -977,6 +995,7 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
977
995
|
commentsXml,
|
|
978
996
|
vmlDrawingXml,
|
|
979
997
|
threadedCommentsXml,
|
|
998
|
+
hasDynamicArrays,
|
|
980
999
|
};
|
|
981
1000
|
};
|
|
982
1001
|
/**
|
|
@@ -998,6 +1017,7 @@ const buildChartsheetXml = ({ sheet, relationships, drawingContext, }) => {
|
|
|
998
1017
|
commentsXml: undefined,
|
|
999
1018
|
vmlDrawingXml: undefined,
|
|
1000
1019
|
threadedCommentsXml: undefined,
|
|
1020
|
+
hasDynamicArrays: false,
|
|
1001
1021
|
};
|
|
1002
1022
|
};
|
|
1003
1023
|
/**
|
|
@@ -1189,7 +1209,7 @@ const normalizeXlsxSourceListFormula1 = (formula1) => {
|
|
|
1189
1209
|
return trimmed;
|
|
1190
1210
|
return `"${escapeFormulaText(trimmed)}"`;
|
|
1191
1211
|
};
|
|
1192
|
-
const convertDataValidationRule = (rule) => {
|
|
1212
|
+
const convertDataValidationRule = (rule, locale) => {
|
|
1193
1213
|
if (!rule)
|
|
1194
1214
|
return undefined;
|
|
1195
1215
|
const allowBlank = rule.allowBlank !== false;
|
|
@@ -1238,7 +1258,7 @@ const convertDataValidationRule = (rule) => {
|
|
|
1238
1258
|
rejectedListSource = normalizeXlsxSourceListFormula1(src.formula1);
|
|
1239
1259
|
}
|
|
1240
1260
|
}
|
|
1241
|
-
const synthesized = convertDataValidationCondition(rule, baseConfig);
|
|
1261
|
+
const synthesized = convertDataValidationCondition(rule, baseConfig, locale);
|
|
1242
1262
|
if (synthesized)
|
|
1243
1263
|
return synthesized;
|
|
1244
1264
|
// A rejected `list` source with no usable condition is still kept — its
|
|
@@ -1257,7 +1277,7 @@ const convertDataValidationRule = (rule) => {
|
|
|
1257
1277
|
// The condition-based synthesizer: the Excel config for a rule that has no
|
|
1258
1278
|
// replayable `xlsxSource` (UI-authored, edited after import, or rejected by
|
|
1259
1279
|
// the round-trip guard above).
|
|
1260
|
-
const convertDataValidationCondition = (rule, baseConfig) => {
|
|
1280
|
+
const convertDataValidationCondition = (rule, baseConfig, locale) => {
|
|
1261
1281
|
if (!rule.condition?.type) {
|
|
1262
1282
|
// Prompt-only DV (no validation rule, just an input-message tooltip
|
|
1263
1283
|
// and/or an error alert). Emit it with no type/formula so Excel still
|
|
@@ -1269,8 +1289,9 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1269
1289
|
}
|
|
1270
1290
|
const { condition } = rule;
|
|
1271
1291
|
const values = condition.values ?? [];
|
|
1292
|
+
// OOXML `<formula1>` carries the formula text WITHOUT a leading `=` — Excel
|
|
1293
|
+
// never writes one (the xlsxSource replay path strips it the same way).
|
|
1272
1294
|
const currentCell = 'INDIRECT("RC",FALSE)';
|
|
1273
|
-
const ensureFormula = (value) => value.startsWith("=") ? value : `=${value}`;
|
|
1274
1295
|
const ifErrorWrap = (formula) => `IFERROR(${formula},TRUE)`;
|
|
1275
1296
|
if (condition.type === "ONE_OF_LIST") {
|
|
1276
1297
|
const entries = values
|
|
@@ -1292,10 +1313,9 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1292
1313
|
.filter((value) => typeof value === "string");
|
|
1293
1314
|
if (!references.length)
|
|
1294
1315
|
return undefined;
|
|
1295
|
-
const formula1 = references.join(",");
|
|
1296
1316
|
return {
|
|
1297
1317
|
type: "list",
|
|
1298
|
-
formula1:
|
|
1318
|
+
formula1: stripLeadingEquals(references.join(",")),
|
|
1299
1319
|
showDropDown: rule.displayStyle === "plain" ? true : undefined,
|
|
1300
1320
|
...baseConfig,
|
|
1301
1321
|
};
|
|
@@ -1306,28 +1326,28 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1306
1326
|
return undefined;
|
|
1307
1327
|
return {
|
|
1308
1328
|
type: "custom",
|
|
1309
|
-
formula1:
|
|
1329
|
+
formula1: stripLeadingEquals(raw),
|
|
1310
1330
|
...baseConfig,
|
|
1311
1331
|
};
|
|
1312
1332
|
}
|
|
1313
1333
|
if (condition.type === "BOOLEAN") {
|
|
1314
1334
|
return {
|
|
1315
1335
|
type: "custom",
|
|
1316
|
-
formula1:
|
|
1336
|
+
formula1: `OR(${currentCell}=TRUE,${currentCell}=FALSE)`,
|
|
1317
1337
|
...baseConfig,
|
|
1318
1338
|
};
|
|
1319
1339
|
}
|
|
1320
1340
|
if (condition.type === "BLANK") {
|
|
1321
1341
|
return {
|
|
1322
1342
|
type: "custom",
|
|
1323
|
-
formula1:
|
|
1343
|
+
formula1: `${currentCell}=""`,
|
|
1324
1344
|
...baseConfig,
|
|
1325
1345
|
};
|
|
1326
1346
|
}
|
|
1327
1347
|
if (condition.type === "NOT_BLANK") {
|
|
1328
1348
|
return {
|
|
1329
1349
|
type: "custom",
|
|
1330
|
-
formula1:
|
|
1350
|
+
formula1: `${currentCell}<>""`,
|
|
1331
1351
|
...baseConfig,
|
|
1332
1352
|
};
|
|
1333
1353
|
}
|
|
@@ -1338,7 +1358,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1338
1358
|
const op = condition.type === "TEXT_EQ" ? "=" : "<>";
|
|
1339
1359
|
return {
|
|
1340
1360
|
type: "custom",
|
|
1341
|
-
formula1:
|
|
1361
|
+
formula1: `${currentCell}${op}"${escapeFormulaText(raw)}"`,
|
|
1342
1362
|
...baseConfig,
|
|
1343
1363
|
};
|
|
1344
1364
|
}
|
|
@@ -1348,7 +1368,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1348
1368
|
return undefined;
|
|
1349
1369
|
return {
|
|
1350
1370
|
type: "custom",
|
|
1351
|
-
formula1:
|
|
1371
|
+
formula1: `ISNUMBER(SEARCH("${escapeFormulaText(raw)}",${currentCell}))`,
|
|
1352
1372
|
...baseConfig,
|
|
1353
1373
|
};
|
|
1354
1374
|
}
|
|
@@ -1358,7 +1378,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1358
1378
|
return undefined;
|
|
1359
1379
|
return {
|
|
1360
1380
|
type: "custom",
|
|
1361
|
-
formula1:
|
|
1381
|
+
formula1: `ISERROR(SEARCH("${escapeFormulaText(raw)}",${currentCell}))`,
|
|
1362
1382
|
...baseConfig,
|
|
1363
1383
|
};
|
|
1364
1384
|
}
|
|
@@ -1369,7 +1389,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1369
1389
|
const text = escapeFormulaText(raw);
|
|
1370
1390
|
return {
|
|
1371
1391
|
type: "custom",
|
|
1372
|
-
formula1:
|
|
1392
|
+
formula1: `LEFT(${currentCell},LEN("${text}"))="${text}"`,
|
|
1373
1393
|
...baseConfig,
|
|
1374
1394
|
};
|
|
1375
1395
|
}
|
|
@@ -1380,21 +1400,21 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1380
1400
|
const text = escapeFormulaText(raw);
|
|
1381
1401
|
return {
|
|
1382
1402
|
type: "custom",
|
|
1383
|
-
formula1:
|
|
1403
|
+
formula1: `RIGHT(${currentCell},LEN("${text}"))="${text}"`,
|
|
1384
1404
|
...baseConfig,
|
|
1385
1405
|
};
|
|
1386
1406
|
}
|
|
1387
1407
|
if (condition.type === "TEXT_IS_EMAIL") {
|
|
1388
1408
|
return {
|
|
1389
1409
|
type: "custom",
|
|
1390
|
-
formula1:
|
|
1410
|
+
formula1: `AND(ISNUMBER(SEARCH("@",${currentCell})),ISNUMBER(SEARCH(".",${currentCell})))`,
|
|
1391
1411
|
...baseConfig,
|
|
1392
1412
|
};
|
|
1393
1413
|
}
|
|
1394
1414
|
if (condition.type === "TEXT_IS_URL") {
|
|
1395
1415
|
return {
|
|
1396
1416
|
type: "custom",
|
|
1397
|
-
formula1:
|
|
1417
|
+
formula1: `OR(LEFT(${currentCell},7)="http://",LEFT(${currentCell},8)="https://")`,
|
|
1398
1418
|
...baseConfig,
|
|
1399
1419
|
};
|
|
1400
1420
|
}
|
|
@@ -1420,13 +1440,16 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1420
1440
|
NUMBER_LESS: "lessThan",
|
|
1421
1441
|
NUMBER_LESS_THAN_EQ: "lessThanOrEqual",
|
|
1422
1442
|
};
|
|
1443
|
+
// Bounds are written the way Excel stores them: typed literals normalized
|
|
1444
|
+
// (`50%` → `0.5`, a typed date → its serial), formulas without their `=`,
|
|
1445
|
+
// references / names / function calls verbatim.
|
|
1423
1446
|
return {
|
|
1424
1447
|
type: "decimal",
|
|
1425
1448
|
operator: operatorMap[condition.type],
|
|
1426
|
-
formula1: first,
|
|
1449
|
+
formula1: conditionOperandToFormulaText(first, locale),
|
|
1427
1450
|
formula2: condition.type === "NUMBER_BETWEEN" ||
|
|
1428
1451
|
condition.type === "NUMBER_NOT_BETWEEN"
|
|
1429
|
-
? second
|
|
1452
|
+
? conditionOperandToFormulaText(second, locale)
|
|
1430
1453
|
: undefined,
|
|
1431
1454
|
...baseConfig,
|
|
1432
1455
|
};
|
|
@@ -1444,7 +1467,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1444
1467
|
const op = opMap[condition.type];
|
|
1445
1468
|
return {
|
|
1446
1469
|
type: "custom",
|
|
1447
|
-
formula1:
|
|
1470
|
+
formula1: `${ifErrorWrap(`${currentCell}${op}INDIRECT("R[-1]C",FALSE)`)}`,
|
|
1448
1471
|
...baseConfig,
|
|
1449
1472
|
};
|
|
1450
1473
|
}
|
|
@@ -1473,10 +1496,10 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1473
1496
|
return {
|
|
1474
1497
|
type: "date",
|
|
1475
1498
|
operator: operatorMap[condition.type],
|
|
1476
|
-
formula1: first,
|
|
1499
|
+
formula1: conditionOperandToFormulaText(first, locale),
|
|
1477
1500
|
formula2: condition.type === "DATE_BETWEEN" ||
|
|
1478
1501
|
condition.type === "DATE_NOT_BETWEEN"
|
|
1479
|
-
? second
|
|
1502
|
+
? conditionOperandToFormulaText(second, locale)
|
|
1480
1503
|
: undefined,
|
|
1481
1504
|
...baseConfig,
|
|
1482
1505
|
};
|
|
@@ -1484,7 +1507,7 @@ const convertDataValidationCondition = (rule, baseConfig) => {
|
|
|
1484
1507
|
if (condition.type === "DATE_IS_VALID") {
|
|
1485
1508
|
return {
|
|
1486
1509
|
type: "custom",
|
|
1487
|
-
formula1:
|
|
1510
|
+
formula1: `ISNUMBER(${currentCell})`,
|
|
1488
1511
|
...baseConfig,
|
|
1489
1512
|
};
|
|
1490
1513
|
}
|
|
@@ -1563,7 +1586,7 @@ const resolveCellDataValidation = (cell, map) => {
|
|
|
1563
1586
|
}
|
|
1564
1587
|
return raw;
|
|
1565
1588
|
};
|
|
1566
|
-
const buildWorksheetRow = ({ rowNumber, row, rowMetadata, defaultRowHeightPx, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, onImage, onHyperlink, dataValidationsById, onDataValidation, tableNameMap, tables, ignoreFormulaPatterns, commentsCollector, onCommentThread, sheetId, sheetData, sheets, locale, validSharedFormulaLeaders, }) => {
|
|
1589
|
+
const buildWorksheetRow = ({ rowNumber, row, rowMetadata, defaultRowHeightPx, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, onImage, onHyperlink, dataValidationsById, onDataValidation, tableNameMap, tables, ignoreFormulaPatterns, commentsCollector, onCommentThread, sheetId, sheetData, sheets, locale, validSharedFormulaLeaders, onDynamicArray, }) => {
|
|
1567
1590
|
const values = row?.values ?? [];
|
|
1568
1591
|
const cellXmlChunks = [];
|
|
1569
1592
|
let minColumn = Number.POSITIVE_INFINITY;
|
|
@@ -1592,6 +1615,7 @@ const buildWorksheetRow = ({ rowNumber, row, rowMetadata, defaultRowHeightPx, sh
|
|
|
1592
1615
|
sheets,
|
|
1593
1616
|
locale,
|
|
1594
1617
|
validSharedFormulaLeaders,
|
|
1618
|
+
onDynamicArray,
|
|
1595
1619
|
});
|
|
1596
1620
|
if (!cellXml)
|
|
1597
1621
|
continue;
|
|
@@ -1662,7 +1686,7 @@ const buildWorksheetRow = ({ rowNumber, row, rowMetadata, defaultRowHeightPx, sh
|
|
|
1662
1686
|
maxColumn: maxColumn || undefined,
|
|
1663
1687
|
};
|
|
1664
1688
|
};
|
|
1665
|
-
const buildCellXml = ({ rowNumber, columnIndex, cell, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, onImage, onHyperlink, dataValidationsById, onDataValidation, tableNameMap, tables, ignoreFormulaPatterns, commentsCollector, onCommentThread, sheetId, sheetData, sheets, locale, validSharedFormulaLeaders, }) => {
|
|
1689
|
+
const buildCellXml = ({ rowNumber, columnIndex, cell, sharedStrings, sourceSharedStrings, cellXfs, styleBuilder, onImage, onHyperlink, dataValidationsById, onDataValidation, tableNameMap, tables, ignoreFormulaPatterns, commentsCollector, onCommentThread, sheetId, sheetData, sheets, locale, validSharedFormulaLeaders, onDynamicArray, }) => {
|
|
1666
1690
|
const normalizedCell = cell;
|
|
1667
1691
|
if (!normalizedCell)
|
|
1668
1692
|
return undefined;
|
|
@@ -1798,12 +1822,26 @@ const buildCellXml = ({ rowNumber, columnIndex, cell, sharedStrings, sourceShare
|
|
|
1798
1822
|
}
|
|
1799
1823
|
const xmlParts = [];
|
|
1800
1824
|
let typeAttr = "";
|
|
1825
|
+
let dynamicArrayAttr = "";
|
|
1801
1826
|
if (exportedFormulaValue) {
|
|
1802
1827
|
// Array formula anchor: emit <f t="array" ref="..."/>. Spilled cells in
|
|
1803
1828
|
// the range carry no `ue` formula (they're populated by the calculator
|
|
1804
1829
|
// as side-effects of the anchor's evaluation), so they never reach this
|
|
1805
|
-
// branch and correctly emit only <v>.
|
|
1806
|
-
|
|
1830
|
+
// branch and correctly emit only <v>. `af` is stamped by the calculator
|
|
1831
|
+
// on every spilling anchor and captured from `<f t="array" ref>` on
|
|
1832
|
+
// import; a bare array constant (`={1,2;3,4}`) that never went through
|
|
1833
|
+
// the calculator still gets its range from the constant's own shape.
|
|
1834
|
+
const arrayRef = normalizedCell.af ??
|
|
1835
|
+
arrayConstantSpillRef(exportedFormulaValue, rowNumber, columnIndex);
|
|
1836
|
+
// Excel distinguishes the two kinds of `<f t="array">`: a dynamic array
|
|
1837
|
+
// carries `cm="1"` on the `<c>` (pointing at the XLDAPR record in
|
|
1838
|
+
// xl/metadata.xml) and re-opens as a spilling formula; without `cm` Excel
|
|
1839
|
+
// reads a legacy Ctrl+Shift+Enter array (`{=…}`). Only an anchor imported
|
|
1840
|
+
// as a CSE array (`cse`) is written back that way.
|
|
1841
|
+
if (arrayRef && !(normalizedCell.af && normalizedCell.cse)) {
|
|
1842
|
+
dynamicArrayAttr = ` cm="1"`;
|
|
1843
|
+
onDynamicArray?.();
|
|
1844
|
+
}
|
|
1807
1845
|
// Shared formula: emit `<f t="shared" si="N" ref="A1:A10">FORMULA</f>`
|
|
1808
1846
|
// on the leader cell (carries both `si` and `ref`) and
|
|
1809
1847
|
// `<f t="shared" si="N"/>` on followers (carries only `si` — no formula
|
|
@@ -1888,7 +1926,7 @@ const buildCellXml = ({ rowNumber, columnIndex, cell, sharedStrings, sourceShare
|
|
|
1888
1926
|
}
|
|
1889
1927
|
return `<c r="${address}"${styleAttr}/>`;
|
|
1890
1928
|
}
|
|
1891
|
-
return `<c r="${address}"${typeAttr}${styleAttr}>${xmlParts.join("")}</c>`;
|
|
1929
|
+
return `<c r="${address}"${typeAttr}${styleAttr}${dynamicArrayAttr}>${xmlParts.join("")}</c>`;
|
|
1892
1930
|
};
|
|
1893
1931
|
const normalizeHyperlinkValue = (hyperlink) => {
|
|
1894
1932
|
if (!hyperlink) {
|
|
@@ -1974,6 +2012,45 @@ function stripFormula(formula) {
|
|
|
1974
2012
|
const trimmed = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
1975
2013
|
return sanitizeFormulaText(trimmed);
|
|
1976
2014
|
}
|
|
2015
|
+
/**
|
|
2016
|
+
* The A1 range an array constant spills over from its anchor: `={1,2;3,4}` at
|
|
2017
|
+
* B3 → `B3:C4`. `undefined` for anything that is not exactly one array
|
|
2018
|
+
* constant (a constant nested in a larger expression, a malformed literal).
|
|
2019
|
+
* Separators inside string elements are skipped.
|
|
2020
|
+
*/
|
|
2021
|
+
function arrayConstantSpillRef(formula, rowNumber, columnIndex) {
|
|
2022
|
+
const body = (formula.startsWith("=") ? formula.slice(1) : formula).trim();
|
|
2023
|
+
if (!body.startsWith("{") || !body.endsWith("}"))
|
|
2024
|
+
return undefined;
|
|
2025
|
+
let rows = 1;
|
|
2026
|
+
let columns = 1;
|
|
2027
|
+
let inString = false;
|
|
2028
|
+
for (const char of body.slice(1, -1)) {
|
|
2029
|
+
if (char === '"') {
|
|
2030
|
+
inString = !inString;
|
|
2031
|
+
}
|
|
2032
|
+
else if (inString) {
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
else if (char === "{" || char === "}") {
|
|
2036
|
+
return undefined;
|
|
2037
|
+
}
|
|
2038
|
+
else if (char === "," && rows === 1) {
|
|
2039
|
+
columns++;
|
|
2040
|
+
}
|
|
2041
|
+
else if (char === ";") {
|
|
2042
|
+
rows++;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (inString)
|
|
2046
|
+
return undefined;
|
|
2047
|
+
return cellRangeToAddress(undefined, {
|
|
2048
|
+
startRowIndex: rowNumber,
|
|
2049
|
+
startColumnIndex: columnIndex,
|
|
2050
|
+
endRowIndex: rowNumber + rows - 1,
|
|
2051
|
+
endColumnIndex: columnIndex + columns - 1,
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
1977
2054
|
function hasPropertyReference(formula) {
|
|
1978
2055
|
if (!formula)
|
|
1979
2056
|
return false;
|
|
@@ -2095,6 +2172,30 @@ function rewriteStructuredReferenceFormula(formula, tableNameMap, tables, rowNum
|
|
|
2095
2172
|
const qualified = prefixUnqualifiedStructuredReferences(normalized, table.name);
|
|
2096
2173
|
return normalizeThisRowStructuredReferences(qualified, table.name);
|
|
2097
2174
|
}
|
|
2175
|
+
/**
|
|
2176
|
+
* `xl/metadata.xml` for a workbook with dynamic-array anchors — one XLDAPR
|
|
2177
|
+
* (dynamic array properties) record every `cm="1"` cell points at, exactly as
|
|
2178
|
+
* Excel 365 writes it. `fDynamic="1"` marks the anchor as a spilling formula;
|
|
2179
|
+
* `fCollapsed="0"` says the spill is shown expanded.
|
|
2180
|
+
*/
|
|
2181
|
+
const DYNAMIC_ARRAY_METADATA_XML = `${XML_DECLARATION}<metadata xmlns="${WORKSHEET_NS}" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray"><metadataTypes count="1"><metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1" pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1" clearComments="1" assign="1" coerce="1" cellMeta="1"/></metadataTypes><futureMetadata name="XLDAPR" count="1"><bk><extLst><ext uri="{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}"><xda:dynamicArrayProperties fDynamic="1" fCollapsed="0"/></ext></extLst></bk></futureMetadata><cellMetadata count="1"><bk><rc t="1" v="0"/></bk></cellMetadata></metadata>`;
|
|
2182
|
+
/**
|
|
2183
|
+
* A defined-name expression that is a formula rather than a constant: a
|
|
2184
|
+
* function call, an operator expression, a reference. Constants (`123`,
|
|
2185
|
+
* `"text"`, `TRUE`) are written verbatim.
|
|
2186
|
+
*/
|
|
2187
|
+
function isAFormulaExpression(expression) {
|
|
2188
|
+
const trimmed = expression.trim();
|
|
2189
|
+
if (!trimmed)
|
|
2190
|
+
return false;
|
|
2191
|
+
if (/^"(?:[^"]|"")*"$/.test(trimmed))
|
|
2192
|
+
return false;
|
|
2193
|
+
if (/^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?%?$/i.test(trimmed))
|
|
2194
|
+
return false;
|
|
2195
|
+
if (/^(?:TRUE|FALSE)$/i.test(trimmed))
|
|
2196
|
+
return false;
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2098
2199
|
function sanitizeFormulaText(formula) {
|
|
2099
2200
|
const normalized = sanitizeXmlText(formula);
|
|
2100
2201
|
// `@x` is persisted as `_xlfn.SINGLE(x)`; encoding first lets the
|
|
@@ -2660,9 +2761,18 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder, customTab
|
|
|
2660
2761
|
const autoFilter = headerRowCount > 0 && table.filterButton !== false
|
|
2661
2762
|
? (() => {
|
|
2662
2763
|
const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder, hiddenButtonColumns);
|
|
2764
|
+
// Excel's table `<autoFilter ref>` spans the header and data rows
|
|
2765
|
+
// only — a ref that reaches into the totals row makes Excel repair
|
|
2766
|
+
// the table (the totals row is never filtered or sorted).
|
|
2767
|
+
const filterRef = totalsRowCount > 0
|
|
2768
|
+
? (gridRangeToRef({
|
|
2769
|
+
...table.range,
|
|
2770
|
+
endRowIndex: table.range.endRowIndex - totalsRowCount,
|
|
2771
|
+
}) ?? rangeRef)
|
|
2772
|
+
: rangeRef;
|
|
2663
2773
|
return filterColumns
|
|
2664
|
-
? `<autoFilter ref="${
|
|
2665
|
-
: `<autoFilter ref="${
|
|
2774
|
+
? `<autoFilter ref="${filterRef}">${filterColumns}</autoFilter>`
|
|
2775
|
+
: `<autoFilter ref="${filterRef}"/>`;
|
|
2666
2776
|
})()
|
|
2667
2777
|
: "";
|
|
2668
2778
|
const sortState = buildTableSortStateXml(table, headerRowCount, totalsRowCount, styleBuilder);
|
|
@@ -2985,53 +3095,86 @@ hiddenButtonColumns) => {
|
|
|
2985
3095
|
.map(([, xml]) => xml)
|
|
2986
3096
|
.join("");
|
|
2987
3097
|
};
|
|
3098
|
+
/**
|
|
3099
|
+
* One `<filterColumn colId=…>`, or `""` when the criteria have no autoFilter
|
|
3100
|
+
* representation. `CT_FilterColumn` is a CHOICE of exactly one of `filters` /
|
|
3101
|
+
* `top10` / `customFilters` / `dynamicFilter` / `colorFilter` / `iconFilter` —
|
|
3102
|
+
* a second child is a schema violation Excel repairs — so the first
|
|
3103
|
+
* representable criterion wins: colorFilter, iconFilter, condition(s), then
|
|
3104
|
+
* the value list (`<filters>`: the checked values, `blank="1"` for the
|
|
3105
|
+
* "(Blanks)" entry, and Excel's `<dateGroupItem>` date-tree entries). The
|
|
3106
|
+
* Rust exporter (`rnc-export` `filter_column_xml`) uses the same precedence.
|
|
3107
|
+
*/
|
|
2988
3108
|
const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
backgroundColor: colorFilter.color,
|
|
2997
|
-
})
|
|
2998
|
-
: styleBuilder.registerDifferentialFormat({
|
|
2999
|
-
textFormat: { color: colorFilter.color },
|
|
3000
|
-
});
|
|
3001
|
-
if (typeof dxfId === "number") {
|
|
3002
|
-
const cellColorAttr = colorFilter.mode === "cellColor" ? "1" : "0";
|
|
3003
|
-
parts.push(`<colorFilter dxfId="${dxfId}" cellColor="${cellColorAttr}"/>`);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
// <iconFilter iconSet="…" iconId="…"/>
|
|
3007
|
-
const iconFilter = spec.filterCriteria?.iconFilter;
|
|
3008
|
-
if (iconFilter?.iconSet) {
|
|
3009
|
-
parts.push(`<iconFilter iconSet="${escapeXmlAttr(iconFilter.iconSet)}" iconId="${iconFilter.iconId ?? 0}"/>`);
|
|
3010
|
-
}
|
|
3011
|
-
// A condition filter (greater-than / contains / between / relative-date /
|
|
3012
|
-
// above-average …) takes precedence over a value-list and is emitted as
|
|
3013
|
-
// <customFilters> or <dynamicFilter>. Falls back to the value list when the
|
|
3014
|
-
// condition has no AutoFilter representation.
|
|
3015
|
-
const conditionXml = spec.filterCriteria
|
|
3016
|
-
? buildAutoFilterCriteriaXml(spec.filterCriteria)
|
|
3017
|
-
: null;
|
|
3018
|
-
if (conditionXml) {
|
|
3019
|
-
parts.push(conditionXml);
|
|
3020
|
-
}
|
|
3021
|
-
else {
|
|
3022
|
-
// <filters><filter val=…/></filters>
|
|
3023
|
-
const values = spec.filterCriteria?.visibleValues ??
|
|
3024
|
-
getFilterVisibleValues(spec, range, rows, headerRow, totalRow);
|
|
3025
|
-
if (values?.length) {
|
|
3026
|
-
const filters = values
|
|
3027
|
-
.map((value) => `<filter val="${escapeXmlAttr(value)}"/>`)
|
|
3028
|
-
.join("");
|
|
3029
|
-
parts.push(`<filters>${filters}</filters>`);
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
if (!parts.length)
|
|
3109
|
+
const child = buildColorFilterXml(spec.filterCriteria?.colorFilter, styleBuilder) ??
|
|
3110
|
+
buildIconFilterXml(spec.filterCriteria?.iconFilter) ??
|
|
3111
|
+
(spec.filterCriteria
|
|
3112
|
+
? buildAutoFilterCriteriaXml(spec.filterCriteria)
|
|
3113
|
+
: null) ??
|
|
3114
|
+
buildFilterValueListXml(spec, range, rows, headerRow, totalRow);
|
|
3115
|
+
if (!child)
|
|
3033
3116
|
return "";
|
|
3034
|
-
return `<filterColumn colId="${spec.columnIndex}">${
|
|
3117
|
+
return `<filterColumn colId="${spec.columnIndex}">${child}</filterColumn>`;
|
|
3118
|
+
};
|
|
3119
|
+
/**
|
|
3120
|
+
* `<colorFilter dxfId="N" cellColor="1|0"/>` — Excel matches against the dxf
|
|
3121
|
+
* table; the color is registered as a dxf and referenced by id.
|
|
3122
|
+
*/
|
|
3123
|
+
const buildColorFilterXml = (colorFilter, styleBuilder) => {
|
|
3124
|
+
if (!colorFilter || !styleBuilder)
|
|
3125
|
+
return null;
|
|
3126
|
+
const dxfId = colorFilter.mode === "cellColor"
|
|
3127
|
+
? styleBuilder.registerDifferentialFormat({
|
|
3128
|
+
backgroundColor: colorFilter.color,
|
|
3129
|
+
})
|
|
3130
|
+
: styleBuilder.registerDifferentialFormat({
|
|
3131
|
+
textFormat: { color: colorFilter.color },
|
|
3132
|
+
});
|
|
3133
|
+
if (typeof dxfId !== "number")
|
|
3134
|
+
return null;
|
|
3135
|
+
const cellColorAttr = colorFilter.mode === "cellColor" ? "1" : "0";
|
|
3136
|
+
return `<colorFilter dxfId="${dxfId}" cellColor="${cellColorAttr}"/>`;
|
|
3137
|
+
};
|
|
3138
|
+
/** `<iconFilter iconSet="…" iconId="…"/>`. */
|
|
3139
|
+
const buildIconFilterXml = (iconFilter) => {
|
|
3140
|
+
if (!iconFilter?.iconSet)
|
|
3141
|
+
return null;
|
|
3142
|
+
return `<iconFilter iconSet="${escapeXmlAttr(iconFilter.iconSet)}" iconId="${iconFilter.iconId ?? 0}"/>`;
|
|
3143
|
+
};
|
|
3144
|
+
/**
|
|
3145
|
+
* `<filters>` — the checked values (`blank="1"` for the engine's "(Blanks)"
|
|
3146
|
+
* sentinel; Excel never writes it as a `<filter val>`) plus the date-grouped
|
|
3147
|
+
* `<dateGroupItem>` entries. Without explicit `visibleValues` the list is
|
|
3148
|
+
* derived from the column's cells (every value visible).
|
|
3149
|
+
*/
|
|
3150
|
+
const buildFilterValueListXml = (spec, range, rows, headerRow, totalRow) => {
|
|
3151
|
+
const dateGroups = spec.filterCriteria?.dateGroups ?? [];
|
|
3152
|
+
const values = spec.filterCriteria?.visibleValues ??
|
|
3153
|
+
(dateGroups.length
|
|
3154
|
+
? []
|
|
3155
|
+
: getFilterVisibleValues(spec, range, rows, headerRow, totalRow));
|
|
3156
|
+
if (!values.length && !dateGroups.length)
|
|
3157
|
+
return null;
|
|
3158
|
+
const showBlanks = values.includes(BLANK_LABEL);
|
|
3159
|
+
const filters = values
|
|
3160
|
+
.filter((value) => value !== BLANK_LABEL)
|
|
3161
|
+
.map((value) => `<filter val="${escapeXmlAttr(value)}"/>`)
|
|
3162
|
+
.join("");
|
|
3163
|
+
const groups = dateGroups
|
|
3164
|
+
.map((group) => {
|
|
3165
|
+
const attrs = [`year="${group.year}"`];
|
|
3166
|
+
for (const name of ["month", "day", "hour", "minute", "second"]) {
|
|
3167
|
+
const value = group[name];
|
|
3168
|
+
if (value !== undefined && value !== null) {
|
|
3169
|
+
attrs.push(`${name}="${value}"`);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
attrs.push(`dateTimeGrouping="${escapeXmlAttr(group.dateTimeGrouping ?? "day")}"`);
|
|
3173
|
+
return `<dateGroupItem ${attrs.join(" ")}/>`;
|
|
3174
|
+
})
|
|
3175
|
+
.join("");
|
|
3176
|
+
const blankAttr = showBlanks ? ` blank="1"` : "";
|
|
3177
|
+
return `<filters${blankAttr}>${filters}${groups}</filters>`;
|
|
3035
3178
|
};
|
|
3036
3179
|
const gridRangeToRef = (range) => {
|
|
3037
3180
|
if (!range)
|
|
@@ -3312,7 +3455,7 @@ const groupSlicersBySheet = (slicers) => {
|
|
|
3312
3455
|
}
|
|
3313
3456
|
return result;
|
|
3314
3457
|
};
|
|
3315
|
-
const buildConditionalFormattingXml = ({ conditionalFormats, sheetId, styleBuilder, }) => {
|
|
3458
|
+
const buildConditionalFormattingXml = ({ conditionalFormats, sheetId, styleBuilder, locale, }) => {
|
|
3316
3459
|
if (!conditionalFormats?.length) {
|
|
3317
3460
|
return "";
|
|
3318
3461
|
}
|
|
@@ -3359,6 +3502,7 @@ const buildConditionalFormattingXml = ({ conditionalFormats, sheetId, styleBuild
|
|
|
3359
3502
|
styleBuilder,
|
|
3360
3503
|
priority: basePriority,
|
|
3361
3504
|
baseCellAddress,
|
|
3505
|
+
locale,
|
|
3362
3506
|
});
|
|
3363
3507
|
if (!cfRulesXml?.length)
|
|
3364
3508
|
continue;
|
|
@@ -3467,7 +3611,7 @@ const buildXlsxSourceCfRuleXml = (rule, styleBuilder, priority) => {
|
|
|
3467
3611
|
}
|
|
3468
3612
|
return `<cfRule ${attrs.join(" ")}${stopIfTrueAttr}>${formulasXml}${childXmlParts.join("")}</cfRule>`;
|
|
3469
3613
|
};
|
|
3470
|
-
const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseCellAddress, }) => {
|
|
3614
|
+
const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseCellAddress, locale, }) => {
|
|
3471
3615
|
// Round-trip via xlsxSource when present (imported rules). Falls through
|
|
3472
3616
|
// to synthesizer for rules without source metadata (UI-created).
|
|
3473
3617
|
if (rule.xlsxSource?.type) {
|
|
@@ -3602,8 +3746,11 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
|
|
|
3602
3746
|
NUMBER_LESS_THAN_EQ: "lessThanOrEqual",
|
|
3603
3747
|
};
|
|
3604
3748
|
const operator = operatorMap[condition.type];
|
|
3605
|
-
|
|
3606
|
-
|
|
3749
|
+
// Typed literals are written the way Excel stores them (`50%` → `0.5`,
|
|
3750
|
+
// a typed date → its serial); formulas lose their `=`; references, names
|
|
3751
|
+
// and function calls are written verbatim.
|
|
3752
|
+
const first = conditionOperandToFormulaText(stripFormula(firstRaw), locale);
|
|
3753
|
+
const second = conditionOperandToFormulaText(stripFormula(secondRaw), locale);
|
|
3607
3754
|
if ((operator === "between" || operator === "notBetween") && !secondRaw) {
|
|
3608
3755
|
return undefined;
|
|
3609
3756
|
}
|
|
@@ -3633,18 +3780,11 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
|
|
|
3633
3780
|
DATE_BETWEEN: "between",
|
|
3634
3781
|
DATE_NOT_BETWEEN: "notBetween",
|
|
3635
3782
|
};
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
return stripFormula(trimmed);
|
|
3642
|
-
}
|
|
3643
|
-
if (!isNaN(Number(trimmed))) {
|
|
3644
|
-
return trimmed;
|
|
3645
|
-
}
|
|
3646
|
-
return `DATEVALUE("${escapeFormulaText(trimmed)}")`;
|
|
3647
|
-
};
|
|
3783
|
+
// A typed date (`2024-01-05`, `1/5/2024` in the document locale) is
|
|
3784
|
+
// exported as its serial — what Excel itself stores; the bare text would
|
|
3785
|
+
// be the arithmetic 2024-01-05 = 2022. Formulas lose their `=`;
|
|
3786
|
+
// references and function calls are written verbatim.
|
|
3787
|
+
const normalizeDateValue = (raw) => conditionOperandToFormulaText(stripFormula(raw), locale);
|
|
3648
3788
|
const first = normalizeDateValue(firstRaw);
|
|
3649
3789
|
const second = normalizeDateValue(secondRaw);
|
|
3650
3790
|
const operator = operatorMap[condition.type];
|
|
@@ -4778,7 +4918,7 @@ const buildCalcPrXml = (iterativeCalculationOptions) => {
|
|
|
4778
4918
|
}
|
|
4779
4919
|
return `<calcPr ${attrs.join(" ")}/>`;
|
|
4780
4920
|
};
|
|
4781
|
-
const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelIds = [], use1904 = false, externalLinkRelIds = [], pivotCacheRelIds = []) => {
|
|
4921
|
+
const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelIds = [], use1904 = false, externalLinkRelIds = [], pivotCacheRelIds = [], workbookProtection) => {
|
|
4782
4922
|
const sheetEntries = sheets
|
|
4783
4923
|
.map((sheet, index) => {
|
|
4784
4924
|
const relId = sheetRelIds[index];
|
|
@@ -4836,7 +4976,54 @@ const buildWorkbookXml = (sheets, sheetRelIds, sheetNames, namedRanges, iterativ
|
|
|
4836
4976
|
const workbookPrXml = use1904
|
|
4837
4977
|
? `<workbookPr date1904="1"/>`
|
|
4838
4978
|
: `<workbookPr/>`;
|
|
4839
|
-
|
|
4979
|
+
// `CT_Workbook` puts `<workbookProtection>` right after `<workbookPr>`.
|
|
4980
|
+
const workbookProtectionXml = buildWorkbookProtectionXml(workbookProtection);
|
|
4981
|
+
return `${XML_DECLARATION}<workbook xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${workbookPrXml}${workbookProtectionXml}${calcPrXml}<bookViews><workbookView activeTab="0" firstSheet="0"/></bookViews><sheets>${sheetEntries}</sheets>${definedNamesXml}${externalReferencesXml}${pivotCachesXml}${slicerCachesXml}</workbook>`;
|
|
4982
|
+
};
|
|
4983
|
+
/**
|
|
4984
|
+
* `<workbookProtection …/>` from the `workbookProtection` export prop: the
|
|
4985
|
+
* agile hash quartets first, then the legacy password hashes, then the lock
|
|
4986
|
+
* flags — Excel's own attribute order — writing only what the model carries
|
|
4987
|
+
* (flags as `"1"` / `"0"`), so an imported element round-trips verbatim. An
|
|
4988
|
+
* absent model or one with no attribute at all writes nothing. Twin of
|
|
4989
|
+
* `rnc-export`'s `workbook_protection_xml`.
|
|
4990
|
+
*/
|
|
4991
|
+
const buildWorkbookProtectionXml = (protection) => {
|
|
4992
|
+
if (!protection)
|
|
4993
|
+
return "";
|
|
4994
|
+
const attrs = [];
|
|
4995
|
+
const text = (name) => {
|
|
4996
|
+
const value = protection[name];
|
|
4997
|
+
if (typeof value === "string" && value) {
|
|
4998
|
+
attrs.push(`${name}="${escapeXmlAttr(value)}"`);
|
|
4999
|
+
}
|
|
5000
|
+
};
|
|
5001
|
+
const count = (name) => {
|
|
5002
|
+
const value = protection[name];
|
|
5003
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
5004
|
+
attrs.push(`${name}="${Math.trunc(value)}"`);
|
|
5005
|
+
}
|
|
5006
|
+
};
|
|
5007
|
+
const flag = (name) => {
|
|
5008
|
+
const value = protection[name];
|
|
5009
|
+
if (typeof value === "boolean") {
|
|
5010
|
+
attrs.push(`${name}="${value ? 1 : 0}"`);
|
|
5011
|
+
}
|
|
5012
|
+
};
|
|
5013
|
+
text("workbookAlgorithmName");
|
|
5014
|
+
text("workbookHashValue");
|
|
5015
|
+
text("workbookSaltValue");
|
|
5016
|
+
count("workbookSpinCount");
|
|
5017
|
+
text("revisionsAlgorithmName");
|
|
5018
|
+
text("revisionsHashValue");
|
|
5019
|
+
text("revisionsSaltValue");
|
|
5020
|
+
count("revisionsSpinCount");
|
|
5021
|
+
text("workbookPassword");
|
|
5022
|
+
text("revisionsPassword");
|
|
5023
|
+
flag("lockStructure");
|
|
5024
|
+
flag("lockWindows");
|
|
5025
|
+
flag("lockRevision");
|
|
5026
|
+
return attrs.length ? `<workbookProtection ${attrs.join(" ")}/>` : "";
|
|
4840
5027
|
};
|
|
4841
5028
|
const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
|
|
4842
5029
|
const entries = [];
|
|
@@ -4878,10 +5065,33 @@ const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
|
|
|
4878
5065
|
entries.push(`<definedName name="${PRINT_TITLES_NAME}" localSheetId="${index}">${escapeXmlText(titles)}</definedName>`);
|
|
4879
5066
|
}
|
|
4880
5067
|
});
|
|
5068
|
+
// A sheet-scoped name (`scopeSheetId`) is written with `localSheetId` = the
|
|
5069
|
+
// sheet's position in `<sheets>`; a scope that names no exported sheet
|
|
5070
|
+
// falls back to workbook scope. Excel allows one entry per (name, scope)
|
|
5071
|
+
// pair and repairs the workbook when a pair repeats, so a duplicate (two
|
|
5072
|
+
// scoped `LocalRange`s that both lost their scope, or a multi-area name
|
|
5073
|
+
// imported as one range per area) is written once.
|
|
5074
|
+
const localSheetIdAttr = (namedRange) => {
|
|
5075
|
+
const scope = namedRange.scopeSheetId;
|
|
5076
|
+
if (scope === undefined || scope === null)
|
|
5077
|
+
return "";
|
|
5078
|
+
const sheetName = sheetNameById.get(scope);
|
|
5079
|
+
const index = sheetName ? sheetIndexByName.get(sheetName) : undefined;
|
|
5080
|
+
return index == null ? "" : ` localSheetId="${index}"`;
|
|
5081
|
+
};
|
|
5082
|
+
const seenNameScopes = new Set();
|
|
5083
|
+
const claimNameScope = (name, localSheetId) => {
|
|
5084
|
+
const key = `${name.toLowerCase()}|${localSheetId}`;
|
|
5085
|
+
if (seenNameScopes.has(key))
|
|
5086
|
+
return false;
|
|
5087
|
+
seenNameScopes.add(key);
|
|
5088
|
+
return true;
|
|
5089
|
+
};
|
|
4881
5090
|
for (const namedRange of namedRanges ?? []) {
|
|
4882
5091
|
if (!namedRange?.name || isBuiltinPrintName(namedRange.name))
|
|
4883
5092
|
continue;
|
|
4884
5093
|
const hiddenAttr = namedRange.hidden ? ` hidden="1"` : "";
|
|
5094
|
+
const scopeAttr = localSheetIdAttr(namedRange);
|
|
4885
5095
|
if (namedRange.range?.sheetId) {
|
|
4886
5096
|
const sheetName = sheetNameById.get(namedRange.range.sheetId);
|
|
4887
5097
|
if (!sheetName)
|
|
@@ -4889,18 +5099,34 @@ const buildDefinedNamesXml = (namedRanges, sheetNameById, printScopes = []) => {
|
|
|
4889
5099
|
const ref = cellRangeToAddress(undefined, namedRange.range);
|
|
4890
5100
|
if (!ref)
|
|
4891
5101
|
continue;
|
|
5102
|
+
if (!claimNameScope(namedRange.name, scopeAttr))
|
|
5103
|
+
continue;
|
|
4892
5104
|
const address = `${escapeSheetName(sheetName)}!${toAbsoluteRef(ref)}`;
|
|
4893
|
-
entries.push(`<definedName name="${escapeXmlAttr(namedRange.name)}"${hiddenAttr}>${escapeXmlText(address)}</definedName>`);
|
|
5105
|
+
entries.push(`<definedName name="${escapeXmlAttr(namedRange.name)}"${scopeAttr}${hiddenAttr}>${escapeXmlText(address)}</definedName>`);
|
|
4894
5106
|
continue;
|
|
4895
5107
|
}
|
|
5108
|
+
// A constant name reaches us as text (`"Hello"`, `0.2`) or, when the
|
|
5109
|
+
// XML layer coerced it, as a number / boolean — both are the same
|
|
5110
|
+
// `<definedName>` body in Excel.
|
|
4896
5111
|
const rawValue = namedRange.value;
|
|
4897
|
-
|
|
5112
|
+
const valueText = typeof rawValue === "string"
|
|
5113
|
+
? rawValue
|
|
5114
|
+
: typeof rawValue === "number" || typeof rawValue === "boolean"
|
|
5115
|
+
? String(rawValue)
|
|
5116
|
+
: undefined;
|
|
5117
|
+
if (valueText === undefined)
|
|
4898
5118
|
continue;
|
|
4899
|
-
const trimmed =
|
|
5119
|
+
const trimmed = valueText.trim();
|
|
4900
5120
|
if (!trimmed)
|
|
4901
5121
|
continue;
|
|
4902
5122
|
const expression = trimmed.startsWith("=") ? trimmed.slice(1) : trimmed;
|
|
4903
|
-
|
|
5123
|
+
if (!claimNameScope(namedRange.name, scopeAttr))
|
|
5124
|
+
continue;
|
|
5125
|
+
// A formula-valued name (`Dbl` = `LAMBDA(x,x*2)`) is stored with the same
|
|
5126
|
+
// markers as a cell formula: `_xlfn.LAMBDA(_xlpm.x,_xlpm.x*2)`.
|
|
5127
|
+
entries.push(`<definedName name="${escapeXmlAttr(namedRange.name)}"${scopeAttr}${hiddenAttr}>${escapeXmlText(isAFormulaExpression(expression)
|
|
5128
|
+
? sanitizeFormulaText(expression)
|
|
5129
|
+
: expression)}</definedName>`);
|
|
4904
5130
|
}
|
|
4905
5131
|
if (!entries.length)
|
|
4906
5132
|
return "";
|
|
@@ -4931,7 +5157,7 @@ const buildWorkbookSlicerCachesXml = (slicerCacheRelIds) => {
|
|
|
4931
5157
|
};
|
|
4932
5158
|
const buildWorkbookRelationships = (
|
|
4933
5159
|
/** One entry per sheet in `<sheets>` order; `target` is relative to `xl/`. */
|
|
4934
|
-
sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false) => {
|
|
5160
|
+
sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false, hasSheetMetadata = false) => {
|
|
4935
5161
|
const relationships = [];
|
|
4936
5162
|
const sheetRelIds = [];
|
|
4937
5163
|
const slicerCacheRelIds = [];
|
|
@@ -4948,6 +5174,9 @@ sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, ha
|
|
|
4948
5174
|
relationships.push(`<Relationship Id="${themeId}" Type="${REL_NS}/theme" Target="theme/theme1.xml"/>`);
|
|
4949
5175
|
relationships.push(`<Relationship Id="${sharedStringsId}" Type="${REL_NS}/sharedStrings" Target="sharedStrings.xml"/>`);
|
|
4950
5176
|
let nextRelId = sheetCount + 4;
|
|
5177
|
+
if (hasSheetMetadata) {
|
|
5178
|
+
relationships.push(`<Relationship Id="rId${nextRelId++}" Type="${REL_NS}/sheetMetadata" Target="metadata.xml"/>`);
|
|
5179
|
+
}
|
|
4951
5180
|
for (let i = 0; i < slicerCacheCount; i++) {
|
|
4952
5181
|
const relId = `rId${nextRelId++}`;
|
|
4953
5182
|
slicerCacheRelIds.push(relId);
|
|
@@ -4984,7 +5213,10 @@ sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, ha
|
|
|
4984
5213
|
slicerCacheRelationshipIds: slicerCacheRelIds,
|
|
4985
5214
|
};
|
|
4986
5215
|
};
|
|
4987
|
-
const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
|
|
5216
|
+
const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, hasSheetMetadata = false, }) => {
|
|
5217
|
+
const metadataOverride = hasSheetMetadata
|
|
5218
|
+
? `<Override PartName="/xl/metadata.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"/>`
|
|
5219
|
+
: "";
|
|
4988
5220
|
const sheetOverrides = Array.from({ length: worksheetCount }, (_, index) => {
|
|
4989
5221
|
return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
|
4990
5222
|
}).join("");
|
|
@@ -5029,7 +5261,7 @@ const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount,
|
|
|
5029
5261
|
? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
|
|
5030
5262
|
: "";
|
|
5031
5263
|
const ctrlPropOverrides = Array.from({ length: ctrlPropCount }, (_, index) => `<Override PartName="/xl/ctrlProps/ctrlProp${index + 1}.xml" ContentType="${CTRL_PROP_CONTENT_TYPE}"/>`).join("");
|
|
5032
|
-
return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${chartsheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
5264
|
+
return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${chartsheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}${metadataOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
5033
5265
|
};
|
|
5034
5266
|
const buildRootRelationshipsXml = () => {
|
|
5035
5267
|
return `${XML_DECLARATION}<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="${REL_NS}/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId3" Type="${REL_NS}/core-properties" Target="docProps/core.xml"/></Relationships>`;
|