@particle-academy/holy-sheet 2.10.2 → 2.11.1

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/index.cjs CHANGED
@@ -521,9 +521,10 @@ var ChartRange = class _ChartRange {
521
521
 
522
522
  // src/workbook/workbook.ts
523
523
  var Workbook = class {
524
- constructor(sheets, meta = {}) {
524
+ constructor(sheets, meta = {}, sourceKind2 = "document") {
525
525
  this.sheets = sheets;
526
526
  this.meta = meta;
527
+ this.sourceKind = sourceKind2;
527
528
  }
528
529
  };
529
530
 
@@ -568,6 +569,23 @@ var ArchiveRefused = class _ArchiveRefused extends Error {
568
569
  Object.setPrototypeOf(this, _ArchiveRefused.prototype);
569
570
  }
570
571
  };
572
+ var TEMPLATE_MESSAGES = {
573
+ macro_enabled: "Macro-enabled templates (.xltm) are refused: re-save it as an .xltx in Excel.",
574
+ family_mismatch: "This package declares a template of a different kind than its contents.",
575
+ already_template: "This package is already a template.",
576
+ already_document: "This package is already a document.",
577
+ not_convertible: "This package has no single content-type declaration that can be rewritten.",
578
+ odf_mimetype: "An OpenDocument package must store its mimetype entry first and uncompressed.",
579
+ bad_target: 'The conversion target must be "document" or "template".'
580
+ };
581
+ var OfficeTemplateRefused = class _OfficeTemplateRefused extends Error {
582
+ constructor(reason) {
583
+ super(`Template refused (${reason}): ${TEMPLATE_MESSAGES[reason] ?? reason}`);
584
+ this.reason = reason;
585
+ this.name = "OfficeTemplateRefused";
586
+ Object.setPrototypeOf(this, _OfficeTemplateRefused.prototype);
587
+ }
588
+ };
571
589
  var SchemaException = class _SchemaException extends Error {
572
590
  constructor(errors, message) {
573
591
  super(message ?? summarize(errors));
@@ -769,7 +787,7 @@ var Normalizer = class {
769
787
  address,
770
788
  new Cell(
771
789
  address,
772
- this.coerceValue(rawValue, format, address, sheetName2),
790
+ this.coerceValue(rawValue, format, address, sheetName2, true),
773
791
  cellData?.formula ?? null,
774
792
  format,
775
793
  comment,
@@ -862,7 +880,7 @@ var Normalizer = class {
862
880
  const rawValue = value.value ?? null;
863
881
  return new Cell(
864
882
  address,
865
- this.coerceValue(rawValue, merged, address, sheetName2),
883
+ this.coerceValue(rawValue, merged, address, sheetName2, true),
866
884
  value.formula ?? null,
867
885
  merged,
868
886
  comment,
@@ -877,7 +895,15 @@ var Normalizer = class {
877
895
  columnFormat
878
896
  );
879
897
  }
880
- coerceValue(value, format, address = "?", sheetName2) {
898
+ /**
899
+ * `explicit`: the value came from an object cell's `value` key. An explicit
900
+ * string is the caller saying "this text, exactly", so it is never coerced
901
+ * to a number: `{value: "+1"}` stays "+1" and `{value: "007"}` stays "007".
902
+ * Only a date/datetime display format still turns a string into a serial,
903
+ * because that conversion is what the format asks for. Bare values keep
904
+ * numeric-string coercion. Mirrors PHP `Normalizer::coerceValue`.
905
+ */
906
+ coerceValue(value, format, address = "?", sheetName2, explicit = false) {
881
907
  if (value === null || value === void 0) return null;
882
908
  const df = format?.displayFormat;
883
909
  if (df === "date" || df === "datetime") {
@@ -885,6 +911,9 @@ var Normalizer = class {
885
911
  return DateConverter.toSerial(value, df === "datetime");
886
912
  }
887
913
  }
914
+ if (explicit && typeof value === "string") {
915
+ return value;
916
+ }
888
917
  if (typeof value === "string" && isNumericString(value)) {
889
918
  return numericStringToNumber(value);
890
919
  }
@@ -2624,9 +2653,11 @@ var corrupt = () => {
2624
2653
  throw new ArchiveRefused("archive_corrupt");
2625
2654
  };
2626
2655
  var SourceArchive = class _SourceArchive {
2627
- constructor(original, parts) {
2656
+ constructor(original, parts, records = [], archiveComment = new Uint8Array()) {
2628
2657
  this.original = original;
2629
2658
  this.parts = parts;
2659
+ this.records = records;
2660
+ this.archiveComment = archiveComment;
2630
2661
  }
2631
2662
  static open(input, options = {}) {
2632
2663
  for (const [key, value] of Object.entries(options)) {
@@ -2635,7 +2666,7 @@ var SourceArchive = class _SourceArchive {
2635
2666
  const limits = { ...DEFAULT_ARCHIVE_LIMITS, ...options };
2636
2667
  if (input.length > limits.maxArchiveBytes) throw new ArchiveRefused("archive_limit");
2637
2668
  const bytes = input.slice();
2638
- const records = index(bytes, limits);
2669
+ const [records, comment] = index(bytes, limits);
2639
2670
  const parts = /* @__PURE__ */ new Map();
2640
2671
  for (const record of records) {
2641
2672
  const compressed = bytes.subarray(record.start, record.start + record.compressed);
@@ -2648,7 +2679,8 @@ var SourceArchive = class _SourceArchive {
2648
2679
  if (payload.length !== record.size || crc32(payload) !== record.crc) corrupt();
2649
2680
  parts.set(record.name, payload);
2650
2681
  }
2651
- return new _SourceArchive(bytes, parts);
2682
+ const entries = records.map(({ name, size, crc, local, finish, central, centralLength, flags, method, localExtra }) => ({ name, size, crc, local, finish, central, centralLength, flags, method, localExtra }));
2683
+ return new _SourceArchive(bytes, parts, entries, comment);
2652
2684
  }
2653
2685
  names() {
2654
2686
  return [...this.parts.keys()];
@@ -2659,6 +2691,14 @@ var SourceArchive = class _SourceArchive {
2659
2691
  toBytes() {
2660
2692
  return this.original.slice();
2661
2693
  }
2694
+ /** Verified member records in central-directory order. */
2695
+ entries() {
2696
+ return this.records.map((entry) => ({ ...entry }));
2697
+ }
2698
+ /** The archive comment, verbatim. */
2699
+ comment() {
2700
+ return this.archiveComment.slice();
2701
+ }
2662
2702
  };
2663
2703
  function index(bytes, limits) {
2664
2704
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
@@ -2737,7 +2777,20 @@ function index(bytes, limits) {
2737
2777
  }
2738
2778
  if (!found) corrupt();
2739
2779
  }
2740
- records.push({ name, method, crc, size, compressed, start });
2780
+ records.push({
2781
+ name,
2782
+ method,
2783
+ crc,
2784
+ size,
2785
+ compressed,
2786
+ start,
2787
+ local,
2788
+ finish,
2789
+ central: at2,
2790
+ centralLength: 46 + nameLength + extraLength + commentLength,
2791
+ flags,
2792
+ localExtra
2793
+ });
2741
2794
  regions.push([local, finish]);
2742
2795
  at2 += 46 + nameLength + extraLength + commentLength;
2743
2796
  }
@@ -2748,7 +2801,7 @@ function index(bytes, limits) {
2748
2801
  if (start < previous) corrupt();
2749
2802
  previous = finish;
2750
2803
  }
2751
- return records;
2804
+ return [records, bytes.slice(end + 22, end + 22 + u16(end + 20))];
2752
2805
  }
2753
2806
  function partName(bytes, flags) {
2754
2807
  let name;
@@ -2837,6 +2890,472 @@ var FormatSniffer = {
2837
2890
  }
2838
2891
  };
2839
2892
 
2893
+ // src/reader/xml.ts
2894
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
2895
+ function localName(name) {
2896
+ const idx = name.indexOf(":");
2897
+ return idx >= 0 ? name.slice(idx + 1) : name;
2898
+ }
2899
+ function unescapeXml(s) {
2900
+ if (s.indexOf("&") < 0) return s;
2901
+ return s.replace(/&(#x?[0-9a-fA-F]+|\w+);/g, (m, ent) => {
2902
+ switch (ent) {
2903
+ case "amp":
2904
+ return "&";
2905
+ case "lt":
2906
+ return "<";
2907
+ case "gt":
2908
+ return ">";
2909
+ case "quot":
2910
+ return '"';
2911
+ case "apos":
2912
+ return "'";
2913
+ default:
2914
+ if (ent[0] === "#") {
2915
+ const code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
2916
+ return Number.isFinite(code) ? String.fromCodePoint(code) : m;
2917
+ }
2918
+ return m;
2919
+ }
2920
+ });
2921
+ }
2922
+ function findTagEnd(src, from) {
2923
+ let quote2 = "";
2924
+ for (let i = from; i < src.length; i++) {
2925
+ const ch = src[i];
2926
+ if (quote2) {
2927
+ if (ch === quote2) quote2 = "";
2928
+ } else if (ch === '"' || ch === "'") {
2929
+ quote2 = ch;
2930
+ } else if (ch === ">") {
2931
+ return i;
2932
+ }
2933
+ }
2934
+ return src.length;
2935
+ }
2936
+ var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
2937
+ function parseTag(inner) {
2938
+ const trimmed = inner.trim();
2939
+ let i = 0;
2940
+ while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
2941
+ const name = trimmed.slice(0, i);
2942
+ const attrs = {};
2943
+ const raw = [];
2944
+ const rest = trimmed.slice(i);
2945
+ for (const m of rest.matchAll(ATTR_RE)) {
2946
+ const rawValue = m[3] ?? m[4] ?? "";
2947
+ attrs[localName(m[1])] = unescapeXml(rawValue);
2948
+ raw.push([m[1], rawValue]);
2949
+ }
2950
+ return { name, attrs, raw };
2951
+ }
2952
+ function parseXml(src, options = {}) {
2953
+ const namespaces = options.namespaces === true;
2954
+ let i = 0;
2955
+ const n = src.length;
2956
+ const root = { name: "#root", attrs: {}, children: [], text: "" };
2957
+ if (namespaces) root.content = [];
2958
+ const stack = [root];
2959
+ const scopes = [{ xml: XML_NS }];
2960
+ const appendText = (text2) => {
2961
+ const top = stack[stack.length - 1];
2962
+ top.text += text2;
2963
+ if (namespaces && text2 !== "") top.content.push(text2);
2964
+ };
2965
+ while (i < n) {
2966
+ if (src[i] === "<") {
2967
+ if (src.startsWith("<!--", i)) {
2968
+ const end2 = src.indexOf("-->", i);
2969
+ i = end2 < 0 ? n : end2 + 3;
2970
+ continue;
2971
+ }
2972
+ if (src.startsWith("<![CDATA[", i)) {
2973
+ const end2 = src.indexOf("]]>", i);
2974
+ appendText(src.slice(i + 9, end2 < 0 ? n : end2));
2975
+ i = end2 < 0 ? n : end2 + 3;
2976
+ continue;
2977
+ }
2978
+ if (src.startsWith("<?", i)) {
2979
+ const end2 = src.indexOf("?>", i);
2980
+ i = end2 < 0 ? n : end2 + 2;
2981
+ continue;
2982
+ }
2983
+ if (src.startsWith("<!", i)) {
2984
+ const end2 = src.indexOf(">", i);
2985
+ i = end2 < 0 ? n : end2 + 1;
2986
+ continue;
2987
+ }
2988
+ if (src[i + 1] === "/") {
2989
+ const end2 = src.indexOf(">", i);
2990
+ if (stack.length > 1) {
2991
+ stack.pop();
2992
+ if (namespaces) scopes.pop();
2993
+ }
2994
+ i = end2 < 0 ? n : end2 + 1;
2995
+ continue;
2996
+ }
2997
+ const end = findTagEnd(src, i + 1);
2998
+ const tagContent = src.slice(i + 1, end);
2999
+ const selfClosing = tagContent.endsWith("/");
3000
+ const { name, attrs, raw } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
3001
+ const node = { name: localName(name), attrs, children: [], text: "" };
3002
+ const parent = stack[stack.length - 1];
3003
+ parent.children.push(node);
3004
+ let scope;
3005
+ if (namespaces) {
3006
+ scope = resolveNamespaces(node, name, raw, scopes[scopes.length - 1]);
3007
+ parent.content.push(node);
3008
+ }
3009
+ if (!selfClosing) {
3010
+ stack.push(node);
3011
+ if (scope) scopes.push(scope);
3012
+ }
3013
+ i = end + 1;
3014
+ } else {
3015
+ const next = src.indexOf("<", i);
3016
+ const stop = next < 0 ? n : next;
3017
+ appendText(unescapeXml(src.slice(i, stop)));
3018
+ i = stop;
3019
+ }
3020
+ }
3021
+ return root.children[0] ?? null;
3022
+ }
3023
+ function resolveNamespaces(node, qname, raw, parentScope) {
3024
+ let scope = parentScope;
3025
+ for (const [key, value] of raw) {
3026
+ if (key === "xmlns" || key.startsWith("xmlns:")) {
3027
+ if (scope === parentScope) scope = { ...parentScope };
3028
+ scope[key === "xmlns" ? "" : key.slice(6)] = unescapeXml(value);
3029
+ }
3030
+ }
3031
+ const colon = qname.indexOf(":");
3032
+ node.ns = scope[colon >= 0 ? qname.slice(0, colon) : ""] ?? "";
3033
+ node.content = [];
3034
+ const nsAttrs = {};
3035
+ for (const [key, value] of raw) {
3036
+ if (key === "xmlns" || key.startsWith("xmlns:")) continue;
3037
+ const idx = key.indexOf(":");
3038
+ const normalised = unescapeXml(value.replace(/[\t\n\r]/g, " "));
3039
+ if (idx < 0) {
3040
+ nsAttrs[key] = normalised;
3041
+ } else {
3042
+ nsAttrs[`{${scope[key.slice(0, idx)] ?? ""}}${key.slice(idx + 1)}`] = normalised;
3043
+ }
3044
+ }
3045
+ node.nsAttrs = nsAttrs;
3046
+ return scope;
3047
+ }
3048
+ function el(node, name) {
3049
+ return node?.children.find((c) => c.name === name);
3050
+ }
3051
+ function els(node, name) {
3052
+ return node ? node.children.filter((c) => c.name === name) : [];
3053
+ }
3054
+ function at(node, name) {
3055
+ return node?.attrs[name];
3056
+ }
3057
+
3058
+ // src/zip/office-template.ts
3059
+ var DOCUMENT = "document";
3060
+ var TEMPLATE = "template";
3061
+ var FAMILY = "spreadsheet";
3062
+ var VBA = "application/vnd.ms-office.vbaProject";
3063
+ var FAMILIES = {
3064
+ word: {
3065
+ part: "/word/document.xml",
3066
+ document: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
3067
+ template: "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml",
3068
+ macroDocument: "application/vnd.ms-word.document.macroEnabled.main+xml",
3069
+ macroTemplate: "application/vnd.ms-word.template.macroEnabledTemplate.main+xml",
3070
+ odfDocument: "application/vnd.oasis.opendocument.text",
3071
+ odfTemplate: "application/vnd.oasis.opendocument.text-template",
3072
+ odfBody: "text"
3073
+ },
3074
+ presentation: {
3075
+ part: "/ppt/presentation.xml",
3076
+ document: "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
3077
+ template: "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml",
3078
+ macroDocument: "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml",
3079
+ macroTemplate: "application/vnd.ms-powerpoint.template.macroEnabled.main+xml",
3080
+ odfDocument: "application/vnd.oasis.opendocument.presentation",
3081
+ odfTemplate: "application/vnd.oasis.opendocument.presentation-template",
3082
+ odfBody: "presentation"
3083
+ },
3084
+ spreadsheet: {
3085
+ part: "/xl/workbook.xml",
3086
+ document: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
3087
+ template: "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml",
3088
+ macroDocument: "application/vnd.ms-excel.sheet.macroEnabled.main+xml",
3089
+ macroTemplate: "application/vnd.ms-excel.template.macroEnabled.main+xml",
3090
+ odfDocument: "application/vnd.oasis.opendocument.spreadsheet",
3091
+ odfTemplate: "application/vnd.oasis.opendocument.spreadsheet-template",
3092
+ odfBody: "spreadsheet"
3093
+ }
3094
+ };
3095
+ var OWN = FAMILIES[FAMILY];
3096
+ var UTF8 = new TextDecoder("utf-8");
3097
+ function filesView(files) {
3098
+ return {
3099
+ read: (name) => Object.prototype.hasOwnProperty.call(files, name) ? files[name] : null,
3100
+ names: () => Object.keys(files)
3101
+ };
3102
+ }
3103
+ function sourceKind(zip) {
3104
+ const types = zip.read("[Content_Types].xml");
3105
+ if (types !== null) return ooxmlKind(zip, types);
3106
+ const mimetype = zip.read("mimetype");
3107
+ if (mimetype !== null) return odfKind(zip, latin1(mimetype));
3108
+ return DOCUMENT;
3109
+ }
3110
+ function convert(bytes, to = null) {
3111
+ if (to !== null && to !== DOCUMENT && to !== TEMPLATE) throw new OfficeTemplateRefused("bad_target");
3112
+ const zip = SourceArchive.open(new Uint8Array(bytes));
3113
+ const kind = sourceKind(zip);
3114
+ const target = to ?? (kind === TEMPLATE ? DOCUMENT : TEMPLATE);
3115
+ if (target === kind) throw new OfficeTemplateRefused(`already_${kind}`);
3116
+ const replacements = /* @__PURE__ */ new Map();
3117
+ const types = zip.read("[Content_Types].xml");
3118
+ const mimetypeBytes = zip.read("mimetype");
3119
+ if (types !== null) {
3120
+ const declared = declarations(types);
3121
+ if (declared === null) throw new OfficeTemplateRefused("not_convertible");
3122
+ if (hasVba(zip, declared) || typesOf(declared, OWN.part).includes(OWN.macroDocument)) {
3123
+ throw new OfficeTemplateRefused("macro_enabled");
3124
+ }
3125
+ const [from, value] = target === TEMPLATE ? [OWN.document, OWN.template] : [OWN.template, OWN.document];
3126
+ replacements.set("[Content_Types].xml", fromLatin1(rewriteOverride(latin1(types), OWN.part, from, value)));
3127
+ } else if (mimetypeBytes !== null) {
3128
+ const entries = zip.entries();
3129
+ const first2 = entries[0];
3130
+ const byLocal = [...entries].sort((a, b) => a.local - b.local);
3131
+ if (first2 === void 0 || first2.name !== "mimetype" || byLocal[0].name !== "mimetype" || first2.method !== 0 || first2.localExtra !== 0) {
3132
+ throw new OfficeTemplateRefused("odf_mimetype");
3133
+ }
3134
+ const [from, value] = target === TEMPLATE ? [OWN.odfDocument, OWN.odfTemplate] : [OWN.odfTemplate, OWN.odfDocument];
3135
+ if (latin1(mimetypeBytes) !== from) throw new OfficeTemplateRefused("not_convertible");
3136
+ replacements.set("mimetype", fromLatin1(value));
3137
+ const manifest = zip.read("META-INF/manifest.xml");
3138
+ if (manifest !== null) {
3139
+ const rewritten = rewriteManifestRoot(latin1(manifest), from, value);
3140
+ if (rewritten !== null) replacements.set("META-INF/manifest.xml", fromLatin1(rewritten));
3141
+ }
3142
+ } else {
3143
+ throw new OfficeTemplateRefused("not_convertible");
3144
+ }
3145
+ const out = rebuild(zip, replacements);
3146
+ if (sourceKind(SourceArchive.open(out)) !== target) {
3147
+ throw new Error("Template conversion did not produce the requested kind.");
3148
+ }
3149
+ return out;
3150
+ }
3151
+ function ooxmlKind(zip, types) {
3152
+ const declared = declarations(types);
3153
+ if (declared === null) return DOCUMENT;
3154
+ let kind = DOCUMENT;
3155
+ for (const [part2, type] of declared) {
3156
+ for (const [name, family] of Object.entries(FAMILIES)) {
3157
+ if (type !== family.template && type !== family.macroTemplate) continue;
3158
+ if (name !== FAMILY || part2 !== asciiLower2(OWN.part)) throw new OfficeTemplateRefused("family_mismatch");
3159
+ if (type === family.macroTemplate) throw new OfficeTemplateRefused("macro_enabled");
3160
+ kind = TEMPLATE;
3161
+ }
3162
+ }
3163
+ if (kind === TEMPLATE && hasVba(zip, declared)) throw new OfficeTemplateRefused("macro_enabled");
3164
+ return kind;
3165
+ }
3166
+ function odfKind(zip, mimetype) {
3167
+ if (mimetype !== OWN.odfTemplate) {
3168
+ if (mimetype.startsWith("application/vnd.oasis.opendocument.") && mimetype.endsWith("-template")) {
3169
+ throw new OfficeTemplateRefused("family_mismatch");
3170
+ }
3171
+ return DOCUMENT;
3172
+ }
3173
+ const content = dom(zip.read("content.xml"));
3174
+ const body = content !== null ? firstByLocalName(content, "body") : null;
3175
+ const first2 = body?.children[0] ?? null;
3176
+ if (first2 === null || first2.name !== OWN.odfBody) throw new OfficeTemplateRefused("family_mismatch");
3177
+ const root = manifestRootType(dom(zip.read("META-INF/manifest.xml")));
3178
+ if (root !== null && root !== OWN.odfTemplate && root !== OWN.odfDocument) {
3179
+ throw new OfficeTemplateRefused("family_mismatch");
3180
+ }
3181
+ return TEMPLATE;
3182
+ }
3183
+ function declarations(types) {
3184
+ const root = dom(types);
3185
+ if (root === null) return null;
3186
+ const out = [];
3187
+ for (const el2 of elements(root)) {
3188
+ if (el2.name === "Override") out.push([asciiLower2(el2.attrs["PartName"] ?? ""), el2.attrs["ContentType"] ?? ""]);
3189
+ else if (el2.name === "Default") out.push([`*.${asciiLower2(el2.attrs["Extension"] ?? "")}`, el2.attrs["ContentType"] ?? ""]);
3190
+ }
3191
+ return out;
3192
+ }
3193
+ function typesOf(declared, part2) {
3194
+ return declared.filter(([name]) => name === asciiLower2(part2)).map(([, type]) => type);
3195
+ }
3196
+ function hasVba(zip, declared) {
3197
+ if (declared.some(([, type]) => asciiLower2(type) === asciiLower2(VBA))) return true;
3198
+ return zip.names().some((name) => /(^|\/)vbaProject\.bin$/i.test(name));
3199
+ }
3200
+ function manifestRootType(root) {
3201
+ if (root === null) return null;
3202
+ for (const el2 of elements(root)) {
3203
+ if (el2.name === "file-entry" && el2.attrs["full-path"] === "/") return el2.attrs["media-type"] ?? null;
3204
+ }
3205
+ return null;
3206
+ }
3207
+ function rewriteOverride(xml, part2, from, to) {
3208
+ const hits = [];
3209
+ for (const tag of xml.matchAll(/<(?:[A-Za-z_][\w.\-]*:)?Override\b[^>]*>/g)) {
3210
+ const name = /\sPartName\s*=\s*(["'])(.*?)\1/s.exec(tag[0]);
3211
+ if (name === null || asciiLower2(name[2]) !== asciiLower2(part2)) continue;
3212
+ const type = /\sContentType\s*=\s*(["'])(.*?)\1/s.exec(tag[0]);
3213
+ if (type !== null && type[2] === from) hits.push(tag.index + type.index + type[0].indexOf(type[1]) + 1);
3214
+ }
3215
+ if (hits.length !== 1) throw new OfficeTemplateRefused("not_convertible");
3216
+ const out = xml.slice(0, hits[0]) + to + xml.slice(hits[0] + from.length);
3217
+ const declared = declarations(out);
3218
+ const after = declared === null ? [] : typesOf(declared, part2);
3219
+ if (after.length !== 1 || after[0] !== to) throw new OfficeTemplateRefused("not_convertible");
3220
+ return out;
3221
+ }
3222
+ function rewriteManifestRoot(xml, from, to) {
3223
+ const current = manifestRootType(dom(xml));
3224
+ if (current === null) return null;
3225
+ if (current !== from) throw new OfficeTemplateRefused("not_convertible");
3226
+ const hits = [];
3227
+ for (const tag of xml.matchAll(/<(?:[A-Za-z_][\w.\-]*:)?file-entry\b[^>]*>/g)) {
3228
+ if (!/\s(?:[A-Za-z_][\w.\-]*:)?full-path\s*=\s*(["'])\/\1/.test(tag[0])) continue;
3229
+ const type = /\s(?:[A-Za-z_][\w.\-]*:)?media-type\s*=\s*(["'])(.*?)\1/s.exec(tag[0]);
3230
+ if (type !== null && type[2] === from) hits.push(tag.index + type.index + type[0].indexOf(type[1]) + 1);
3231
+ }
3232
+ if (hits.length !== 1) throw new OfficeTemplateRefused("not_convertible");
3233
+ const out = xml.slice(0, hits[0]) + to + xml.slice(hits[0] + from.length);
3234
+ if (manifestRootType(dom(out)) !== to) throw new OfficeTemplateRefused("not_convertible");
3235
+ return out;
3236
+ }
3237
+ function rebuild(zip, replacements) {
3238
+ const original = zip.toBytes();
3239
+ const view = new DataView(original.buffer, original.byteOffset, original.byteLength);
3240
+ const u16 = (at2) => view.getUint16(at2, true);
3241
+ const u32 = (at2) => view.getUint32(at2, true);
3242
+ const entries = zip.entries();
3243
+ const byLocal = [...entries].sort((a, b) => a.local - b.local);
3244
+ const nameBytes = (entry) => original.subarray(entry.central + 46, entry.central + 46 + u16(entry.central + 28));
3245
+ const chunks = [];
3246
+ const offsets = /* @__PURE__ */ new Map();
3247
+ let length = 0;
3248
+ const push = (chunk) => {
3249
+ chunks.push(chunk);
3250
+ length += chunk.length;
3251
+ };
3252
+ for (const entry of byLocal) {
3253
+ offsets.set(entry.name, length);
3254
+ const data = replacements.get(entry.name);
3255
+ if (data === void 0) {
3256
+ push(original.subarray(entry.local, entry.finish));
3257
+ continue;
3258
+ }
3259
+ const name = nameBytes(entry);
3260
+ const header = new Uint8Array(30 + name.length + data.length);
3261
+ const hv = new DataView(header.buffer);
3262
+ hv.setUint32(0, 67324752, true);
3263
+ hv.setUint16(4, 10, true);
3264
+ hv.setUint16(6, entry.flags & 2048, true);
3265
+ hv.setUint16(8, 0, true);
3266
+ hv.setUint16(10, u16(entry.local + 10), true);
3267
+ hv.setUint16(12, u16(entry.local + 12), true);
3268
+ hv.setUint32(14, crc32(data) >>> 0, true);
3269
+ hv.setUint32(18, data.length, true);
3270
+ hv.setUint32(22, data.length, true);
3271
+ hv.setUint16(26, name.length, true);
3272
+ hv.setUint16(28, 0, true);
3273
+ header.set(name, 30);
3274
+ header.set(data, 30 + name.length);
3275
+ push(header);
3276
+ }
3277
+ const localsLength = length;
3278
+ for (const entry of entries) {
3279
+ const data = replacements.get(entry.name);
3280
+ if (data === void 0) {
3281
+ const record2 = new Uint8Array(entry.centralLength);
3282
+ record2.set(original.subarray(entry.central, entry.central + entry.centralLength));
3283
+ new DataView(record2.buffer, record2.byteOffset, record2.byteLength).setUint32(42, offsets.get(entry.name), true);
3284
+ push(record2);
3285
+ continue;
3286
+ }
3287
+ const name = nameBytes(entry);
3288
+ const at2 = entry.central;
3289
+ const record = new Uint8Array(46 + name.length);
3290
+ const rv = new DataView(record.buffer);
3291
+ rv.setUint32(0, 33639248, true);
3292
+ rv.setUint16(4, u16(at2 + 4), true);
3293
+ rv.setUint16(6, 10, true);
3294
+ rv.setUint16(8, entry.flags & 2048, true);
3295
+ rv.setUint16(10, 0, true);
3296
+ rv.setUint16(12, u16(at2 + 12), true);
3297
+ rv.setUint16(14, u16(at2 + 14), true);
3298
+ rv.setUint32(16, crc32(data) >>> 0, true);
3299
+ rv.setUint32(20, data.length, true);
3300
+ rv.setUint32(24, data.length, true);
3301
+ rv.setUint16(28, name.length, true);
3302
+ rv.setUint16(36, u16(at2 + 36), true);
3303
+ rv.setUint32(38, u32(at2 + 38), true);
3304
+ rv.setUint32(42, offsets.get(entry.name), true);
3305
+ record.set(name, 46);
3306
+ push(record);
3307
+ }
3308
+ const comment = zip.comment();
3309
+ const end = new Uint8Array(22 + comment.length);
3310
+ const ev = new DataView(end.buffer);
3311
+ ev.setUint32(0, 101010256, true);
3312
+ ev.setUint16(8, entries.length, true);
3313
+ ev.setUint16(10, entries.length, true);
3314
+ ev.setUint32(12, length - localsLength, true);
3315
+ ev.setUint32(16, localsLength, true);
3316
+ ev.setUint16(20, comment.length, true);
3317
+ end.set(comment, 22);
3318
+ push(end);
3319
+ const out = new Uint8Array(length);
3320
+ let pos = 0;
3321
+ for (const chunk of chunks) {
3322
+ out.set(chunk, pos);
3323
+ pos += chunk.length;
3324
+ }
3325
+ return out;
3326
+ }
3327
+ function dom(xml) {
3328
+ if (xml === null) return null;
3329
+ const text2 = typeof xml === "string" ? UTF8.decode(fromLatin1(xml)) : UTF8.decode(xml);
3330
+ if (text2 === "" || /<!DOCTYPE/i.test(text2)) return null;
3331
+ return parseXml(text2);
3332
+ }
3333
+ function* elements(root) {
3334
+ const stack = [root];
3335
+ while (stack.length > 0) {
3336
+ const el2 = stack.pop();
3337
+ yield el2;
3338
+ for (let i = el2.children.length - 1; i >= 0; i--) stack.push(el2.children[i]);
3339
+ }
3340
+ }
3341
+ function firstByLocalName(root, local) {
3342
+ for (const el2 of elements(root)) if (el2.name === local) return el2;
3343
+ return null;
3344
+ }
3345
+ function asciiLower2(s) {
3346
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
3347
+ }
3348
+ function latin1(bytes) {
3349
+ let out = "";
3350
+ for (let i = 0; i < bytes.length; i += 32768) out += String.fromCharCode(...bytes.subarray(i, i + 32768));
3351
+ return out;
3352
+ }
3353
+ function fromLatin1(s) {
3354
+ const out = new Uint8Array(s.length);
3355
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
3356
+ return out;
3357
+ }
3358
+
2840
3359
  // src/reader/file-input.ts
2841
3360
  async function readFileBounded(path) {
2842
3361
  const fs = await import('fs');
@@ -3377,6 +3896,7 @@ var WorkbookSchema = {
3377
3896
  fromWorkbook(workbook) {
3378
3897
  const schema = { sheets: workbook.sheets.map(sheetToSchema) };
3379
3898
  if (Object.keys(workbook.meta).length > 0) schema["meta"] = workbook.meta;
3899
+ if (workbook.sourceKind === "template") schema["sourceKind"] = "template";
3380
3900
  return schema;
3381
3901
  }
3382
3902
  };
@@ -3479,171 +3999,6 @@ function commentToObject(c) {
3479
3999
  return out;
3480
4000
  }
3481
4001
 
3482
- // src/reader/xml.ts
3483
- var XML_NS = "http://www.w3.org/XML/1998/namespace";
3484
- function localName(name) {
3485
- const idx = name.indexOf(":");
3486
- return idx >= 0 ? name.slice(idx + 1) : name;
3487
- }
3488
- function unescapeXml(s) {
3489
- if (s.indexOf("&") < 0) return s;
3490
- return s.replace(/&(#x?[0-9a-fA-F]+|\w+);/g, (m, ent) => {
3491
- switch (ent) {
3492
- case "amp":
3493
- return "&";
3494
- case "lt":
3495
- return "<";
3496
- case "gt":
3497
- return ">";
3498
- case "quot":
3499
- return '"';
3500
- case "apos":
3501
- return "'";
3502
- default:
3503
- if (ent[0] === "#") {
3504
- const code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
3505
- return Number.isFinite(code) ? String.fromCodePoint(code) : m;
3506
- }
3507
- return m;
3508
- }
3509
- });
3510
- }
3511
- function findTagEnd(src, from) {
3512
- let quote2 = "";
3513
- for (let i = from; i < src.length; i++) {
3514
- const ch = src[i];
3515
- if (quote2) {
3516
- if (ch === quote2) quote2 = "";
3517
- } else if (ch === '"' || ch === "'") {
3518
- quote2 = ch;
3519
- } else if (ch === ">") {
3520
- return i;
3521
- }
3522
- }
3523
- return src.length;
3524
- }
3525
- var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
3526
- function parseTag(inner) {
3527
- const trimmed = inner.trim();
3528
- let i = 0;
3529
- while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
3530
- const name = trimmed.slice(0, i);
3531
- const attrs = {};
3532
- const raw = [];
3533
- const rest = trimmed.slice(i);
3534
- for (const m of rest.matchAll(ATTR_RE)) {
3535
- const rawValue = m[3] ?? m[4] ?? "";
3536
- attrs[localName(m[1])] = unescapeXml(rawValue);
3537
- raw.push([m[1], rawValue]);
3538
- }
3539
- return { name, attrs, raw };
3540
- }
3541
- function parseXml(src, options = {}) {
3542
- const namespaces = options.namespaces === true;
3543
- let i = 0;
3544
- const n = src.length;
3545
- const root = { name: "#root", attrs: {}, children: [], text: "" };
3546
- if (namespaces) root.content = [];
3547
- const stack = [root];
3548
- const scopes = [{ xml: XML_NS }];
3549
- const appendText = (text2) => {
3550
- const top = stack[stack.length - 1];
3551
- top.text += text2;
3552
- if (namespaces && text2 !== "") top.content.push(text2);
3553
- };
3554
- while (i < n) {
3555
- if (src[i] === "<") {
3556
- if (src.startsWith("<!--", i)) {
3557
- const end2 = src.indexOf("-->", i);
3558
- i = end2 < 0 ? n : end2 + 3;
3559
- continue;
3560
- }
3561
- if (src.startsWith("<![CDATA[", i)) {
3562
- const end2 = src.indexOf("]]>", i);
3563
- appendText(src.slice(i + 9, end2 < 0 ? n : end2));
3564
- i = end2 < 0 ? n : end2 + 3;
3565
- continue;
3566
- }
3567
- if (src.startsWith("<?", i)) {
3568
- const end2 = src.indexOf("?>", i);
3569
- i = end2 < 0 ? n : end2 + 2;
3570
- continue;
3571
- }
3572
- if (src.startsWith("<!", i)) {
3573
- const end2 = src.indexOf(">", i);
3574
- i = end2 < 0 ? n : end2 + 1;
3575
- continue;
3576
- }
3577
- if (src[i + 1] === "/") {
3578
- const end2 = src.indexOf(">", i);
3579
- if (stack.length > 1) {
3580
- stack.pop();
3581
- if (namespaces) scopes.pop();
3582
- }
3583
- i = end2 < 0 ? n : end2 + 1;
3584
- continue;
3585
- }
3586
- const end = findTagEnd(src, i + 1);
3587
- const tagContent = src.slice(i + 1, end);
3588
- const selfClosing = tagContent.endsWith("/");
3589
- const { name, attrs, raw } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
3590
- const node = { name: localName(name), attrs, children: [], text: "" };
3591
- const parent = stack[stack.length - 1];
3592
- parent.children.push(node);
3593
- let scope;
3594
- if (namespaces) {
3595
- scope = resolveNamespaces(node, name, raw, scopes[scopes.length - 1]);
3596
- parent.content.push(node);
3597
- }
3598
- if (!selfClosing) {
3599
- stack.push(node);
3600
- if (scope) scopes.push(scope);
3601
- }
3602
- i = end + 1;
3603
- } else {
3604
- const next = src.indexOf("<", i);
3605
- const stop = next < 0 ? n : next;
3606
- appendText(unescapeXml(src.slice(i, stop)));
3607
- i = stop;
3608
- }
3609
- }
3610
- return root.children[0] ?? null;
3611
- }
3612
- function resolveNamespaces(node, qname, raw, parentScope) {
3613
- let scope = parentScope;
3614
- for (const [key, value] of raw) {
3615
- if (key === "xmlns" || key.startsWith("xmlns:")) {
3616
- if (scope === parentScope) scope = { ...parentScope };
3617
- scope[key === "xmlns" ? "" : key.slice(6)] = unescapeXml(value);
3618
- }
3619
- }
3620
- const colon = qname.indexOf(":");
3621
- node.ns = scope[colon >= 0 ? qname.slice(0, colon) : ""] ?? "";
3622
- node.content = [];
3623
- const nsAttrs = {};
3624
- for (const [key, value] of raw) {
3625
- if (key === "xmlns" || key.startsWith("xmlns:")) continue;
3626
- const idx = key.indexOf(":");
3627
- const normalised = unescapeXml(value.replace(/[\t\n\r]/g, " "));
3628
- if (idx < 0) {
3629
- nsAttrs[key] = normalised;
3630
- } else {
3631
- nsAttrs[`{${scope[key.slice(0, idx)] ?? ""}}${key.slice(idx + 1)}`] = normalised;
3632
- }
3633
- }
3634
- node.nsAttrs = nsAttrs;
3635
- return scope;
3636
- }
3637
- function el(node, name) {
3638
- return node?.children.find((c) => c.name === name);
3639
- }
3640
- function els(node, name) {
3641
- return node ? node.children.filter((c) => c.name === name) : [];
3642
- }
3643
- function at(node, name) {
3644
- return node?.attrs[name];
3645
- }
3646
-
3647
4002
  // src/reader/ods-reader.ts
3648
4003
  var MAX_ROWS = 1048576;
3649
4004
  var MAX_COLUMNS = 16384;
@@ -3658,6 +4013,7 @@ var OdsReader = class {
3658
4013
  }
3659
4014
  /** Read an already-unzipped package (name -> bytes). */
3660
4015
  readFiles(files) {
4016
+ const kind = sourceKind(filesView(files));
3661
4017
  const getText = (name) => files[name] ? decoder2.decode(files[name]) : null;
3662
4018
  const content = getText("content.xml");
3663
4019
  if (content === null) throw new Error("[holy-sheet] missing content.xml");
@@ -3672,7 +4028,7 @@ var OdsReader = class {
3672
4028
  if (table.ns === NS.TABLE && table.name === "table") sheets.push(this.readSheet(table, styles));
3673
4029
  }
3674
4030
  const meta = getText("meta.xml");
3675
- return new Workbook(sheets, meta === null ? {} : readMeta(meta));
4031
+ return new Workbook(sheets, meta === null ? {} : readMeta(meta), kind);
3676
4032
  }
3677
4033
  readSheet(table, styles) {
3678
4034
  const columns = [];
@@ -4338,6 +4694,7 @@ var XlsxReader = class {
4338
4694
  }
4339
4695
  /** Read an already-unzipped package (name -> bytes), so a caller that sniffed it does not unzip twice. */
4340
4696
  readFiles(files) {
4697
+ const kind = sourceKind(filesView(files));
4341
4698
  const getText = (name) => files[name] ? decoder3.decode(files[name]) : null;
4342
4699
  const stylesXml = getText("xl/styles.xml");
4343
4700
  const stylesIndex = stylesXml ? StylesParser.parse(stylesXml) : [];
@@ -4393,7 +4750,7 @@ var XlsxReader = class {
4393
4750
  i++;
4394
4751
  }
4395
4752
  const meta = this.parseDocProps(getText("docProps/core.xml"));
4396
- return new Workbook(sheets, meta);
4753
+ return new Workbook(sheets, meta, kind);
4397
4754
  }
4398
4755
  parseDocProps(coreXml) {
4399
4756
  if (coreXml === null) return {};
@@ -6331,7 +6688,7 @@ var holy_sheet_schema_default = {
6331
6688
  };
6332
6689
 
6333
6690
  // src/agent.ts
6334
- var VERSION = "2.10.2";
6691
+ var VERSION = "2.11.1";
6335
6692
  function toU8(input) {
6336
6693
  return input instanceof Uint8Array ? input : new Uint8Array(input);
6337
6694
  }
@@ -6372,6 +6729,32 @@ var Agent = {
6372
6729
  read(input) {
6373
6730
  return readSpreadsheet(toU8(input), null);
6374
6731
  },
6732
+ /**
6733
+ * Turn an Excel `.xltx` / LibreOffice `.ots` template into its `.xlsx` /
6734
+ * `.ods` twin, or back. `to` is `"document"`, `"template"`, or null to flip.
6735
+ * Universal and synchronous: bytes in, bytes out. Mirrors PHP
6736
+ * `Agent::convertTemplate()`.
6737
+ *
6738
+ * Only the declaration moves: the workbook part's content type in
6739
+ * `[Content_Types].xml`, or the ODF `mimetype` entry together with the
6740
+ * manifest's root media-type (ODF 1.3 requires them to agree). Every other
6741
+ * member is copied as its original zip record, byte for byte.
6742
+ *
6743
+ * Throws `OfficeTemplateRefused` (macro-enabled, mismatched, already that
6744
+ * kind), `ArchiveRefused` (archive guard), or `UnsupportedFormatException`
6745
+ * for a package that is not a spreadsheet.
6746
+ */
6747
+ convertTemplate(input, to = null) {
6748
+ const bytes = toU8(input);
6749
+ const zip = SourceArchive.open(bytes);
6750
+ const raw = zip.read("mimetype");
6751
+ const mimetype = raw === null ? null : new TextDecoder().decode(raw).trim();
6752
+ const odf = mimetype !== null && mimetype.startsWith("application/vnd.oasis.opendocument.spreadsheet");
6753
+ if (!odf && (mimetype !== null || zip.read("xl/workbook.xml") === null)) {
6754
+ throw UnsupportedFormatException.unknownPackage(null, mimetype);
6755
+ }
6756
+ return convert(bytes, to);
6757
+ },
6375
6758
  /** Round-trip an xlsx or ods file on disk back to a schema (Node only). */
6376
6759
  async describe(path) {
6377
6760
  const fs = await import('fs');
@@ -6473,6 +6856,10 @@ var HolySheet = class {
6473
6856
  describe(path) {
6474
6857
  return Agent.describe(path);
6475
6858
  }
6859
+ /** {@link Agent.convertTemplate}: .xltx/.ots to .xlsx/.ods and back. */
6860
+ convertTemplate(input, to = null) {
6861
+ return Agent.convertTemplate(input, to);
6862
+ }
6476
6863
  validateAndRepair(schema) {
6477
6864
  return Agent.validateAndRepair(schema);
6478
6865
  }
@@ -6505,6 +6892,7 @@ exports.HolySheet = HolySheet;
6505
6892
  exports.Inference = Inference;
6506
6893
  exports.Normalizer = Normalizer;
6507
6894
  exports.OdsReader = OdsReader;
6895
+ exports.OfficeTemplateRefused = OfficeTemplateRefused;
6508
6896
  exports.Repairer = Repairer;
6509
6897
  exports.SchemaException = SchemaException;
6510
6898
  exports.SheetDiff = SheetDiff;