@mdzip/editor-ng 1.5.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6723,10 +6723,360 @@ try {
6723
6723
  } catch (e) {
6724
6724
  }
6725
6725
 
6726
+ // ../../node_modules/@mdzip/core-js/dist/spec.js
6727
+ var MDZ_SPEC_VERSION = "1.2.0";
6728
+ var MDZ_SUPPORTED_MAJOR = 1;
6729
+ var MDZ_MODES = ["document", "project"];
6730
+ var MDZ_SUPPORT_FILE_NAMES = ["readme", "agents", "license", "changelog", "contributing"];
6731
+ function mdzError(code, message) {
6732
+ return new Error(`${code}: ${message}`);
6733
+ }
6734
+ var CONTROL_RE = /[\u0000-\u001f\u007f]/;
6735
+ var RESERVED_RE = /[\\:*?"<>|]/;
6736
+ var MARKDOWN_EXT_RE = /\.(md|markdown)$/;
6737
+ function nfc(path) {
6738
+ return String(path).normalize("NFC");
6739
+ }
6740
+ function asciiLower(text) {
6741
+ return String(text).replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
6742
+ }
6743
+ function samePath(a, b) {
6744
+ return nfc(a) === nfc(b);
6745
+ }
6746
+ function isMarkdownPath(path) {
6747
+ return MARKDOWN_EXT_RE.test(asciiLower(path));
6748
+ }
6749
+ function isSupportFile(path) {
6750
+ if (path.includes("/") || !isMarkdownPath(path))
6751
+ return false;
6752
+ return MDZ_SUPPORT_FILE_NAMES.includes(asciiLower(path).replace(MARKDOWN_EXT_RE, ""));
6753
+ }
6754
+ function pathProblem(path) {
6755
+ const raw = String(path ?? "");
6756
+ if (raw === "")
6757
+ return { code: "ERR_PATH_EMPTY", message: "path is empty" };
6758
+ if (raw.startsWith("/"))
6759
+ return { code: "ERR_PATH_LEADING_SLASH", message: `'${raw}' starts with '/'` };
6760
+ if (CONTROL_RE.test(raw))
6761
+ return { code: "ERR_PATH_CONTROL_CHAR", message: `${JSON.stringify(raw)} contains a control character` };
6762
+ if (RESERVED_RE.test(raw))
6763
+ return { code: "ERR_PATH_RESERVED_CHAR", message: `'${raw}' contains a reserved character (\\ : * ? " < > |)` };
6764
+ const segments = (raw.endsWith("/") ? raw.slice(0, -1) : raw).split("/");
6765
+ if (segments.includes(".."))
6766
+ return { code: "ERR_PATH_TRAVERSAL", message: `'${raw}' contains a '..' segment` };
6767
+ if (segments.some((s) => s === "" || s === "."))
6768
+ return { code: "ERR_PATH_INVALID", message: `'${raw}' contains an empty or '.' segment` };
6769
+ return null;
6770
+ }
6771
+ function entrySetProblem(names) {
6772
+ const files = /* @__PURE__ */ new Map();
6773
+ const directories = /* @__PURE__ */ new Map();
6774
+ for (const name of names) {
6775
+ const isDir = name.endsWith("/");
6776
+ const key = nfc(isDir ? name.slice(0, -1) : name);
6777
+ if (!isDir) {
6778
+ const other = files.get(key);
6779
+ if (other !== void 0) {
6780
+ return { code: "ERR_DUPLICATE_PATH", message: `'${name}' and '${other}' are the same path (\xA75.4)` };
6781
+ }
6782
+ files.set(key, name);
6783
+ } else if (!directories.has(key)) {
6784
+ directories.set(key, name);
6785
+ }
6786
+ const parts = key.split("/");
6787
+ for (let i = 1; i < parts.length; i += 1) {
6788
+ const dir2 = parts.slice(0, i).join("/");
6789
+ if (!directories.has(dir2))
6790
+ directories.set(dir2, name);
6791
+ }
6792
+ }
6793
+ for (const [key, name] of files) {
6794
+ const dir2 = directories.get(key);
6795
+ if (dir2 !== void 0) {
6796
+ return { code: "ERR_PATH_CONFLICT", message: `'${name}' is a file and also a directory of '${dir2}' (\xA75.4)` };
6797
+ }
6798
+ }
6799
+ return null;
6800
+ }
6801
+ function entryPointProblem(value) {
6802
+ if (typeof value !== "string" || value === "")
6803
+ return "entryPoint must be a non-empty string";
6804
+ const path = value.startsWith("./") ? value.slice(2) : value;
6805
+ if (path.endsWith("/"))
6806
+ return `entryPoint '${value}' names a directory`;
6807
+ const problem = pathProblem(path);
6808
+ if (problem)
6809
+ return `entryPoint '${value}' is not a valid archive path (${problem.message})`;
6810
+ if (!isMarkdownPath(path))
6811
+ return `entryPoint '${value}' is not a .md or .markdown file`;
6812
+ return null;
6813
+ }
6814
+ function coverProblem(value) {
6815
+ if (typeof value !== "string" || value === "")
6816
+ return "cover must be a non-empty string";
6817
+ const path = value.startsWith("./") ? value.slice(2) : value;
6818
+ if (path.endsWith("/"))
6819
+ return `cover '${value}' names a directory`;
6820
+ const problem = pathProblem(path);
6821
+ return problem ? `cover '${value}' is not a valid archive path (${problem.message})` : null;
6822
+ }
6823
+ var SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
6824
+ var DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
6825
+ function isSemver(value) {
6826
+ return typeof value === "string" && SEMVER_RE.test(value);
6827
+ }
6828
+ function isDateTime(value) {
6829
+ return typeof value === "string" && DATE_TIME_RE.test(value);
6830
+ }
6831
+ function isObject(value) {
6832
+ return !!value && typeof value === "object" && !Array.isArray(value);
6833
+ }
6834
+ function isTimestamp(value) {
6835
+ return isDateTime(value) || isObject(value) && isDateTime(value.when);
6836
+ }
6837
+ var DESCRIPTIVE_FIELDS = {
6838
+ title: (v) => typeof v === "string" && v !== "",
6839
+ language: (v) => typeof v === "string",
6840
+ description: (v) => typeof v === "string",
6841
+ version: (v) => typeof v === "string",
6842
+ license: (v) => typeof v === "string",
6843
+ keywords: (v) => Array.isArray(v) && v.every((k) => typeof k === "string"),
6844
+ author: isObject,
6845
+ producer: isObject,
6846
+ created: isTimestamp,
6847
+ modified: isTimestamp,
6848
+ cover: (v) => coverProblem(v) === null
6849
+ };
6850
+ function findDuplicateMembers(text) {
6851
+ const duplicates = [];
6852
+ const stack = [];
6853
+ for (let i = 0; i < text.length; i += 1) {
6854
+ const c = text[i];
6855
+ if (c === '"') {
6856
+ let j = i + 1;
6857
+ while (j < text.length && text[j] !== '"')
6858
+ j += text[j] === "\\" ? 2 : 1;
6859
+ const top = stack[stack.length - 1];
6860
+ if (top?.object && top.expectKey) {
6861
+ const key = JSON.parse(text.slice(i, j + 1));
6862
+ if (top.keys.has(key))
6863
+ duplicates.push(key);
6864
+ top.keys.add(key);
6865
+ top.expectKey = false;
6866
+ }
6867
+ i = j;
6868
+ } else if (c === "{" || c === "[") {
6869
+ stack.push({ object: c === "{", keys: /* @__PURE__ */ new Set(), expectKey: c === "{" });
6870
+ } else if (c === "}" || c === "]") {
6871
+ stack.pop();
6872
+ } else if (c === ",") {
6873
+ const top = stack[stack.length - 1];
6874
+ if (top?.object)
6875
+ top.expectKey = true;
6876
+ }
6877
+ }
6878
+ return duplicates;
6879
+ }
6880
+ function parseManifestText(text) {
6881
+ const source = String(text).replace(/^/, "");
6882
+ let value;
6883
+ try {
6884
+ value = JSON.parse(source);
6885
+ } catch {
6886
+ throw mdzError("ERR_MANIFEST_INVALID", "manifest.json is not valid JSON");
6887
+ }
6888
+ if (!isObject(value))
6889
+ throw mdzError("ERR_MANIFEST_INVALID", "manifest.json must be a JSON object");
6890
+ return { manifest: value, duplicateMembers: findDuplicateMembers(source) };
6891
+ }
6892
+ function checkManifest(manifest, options) {
6893
+ if ("spec" in manifest) {
6894
+ const spec = manifest.spec;
6895
+ if (!isObject(spec))
6896
+ throw mdzError("ERR_MANIFEST_INVALID", `spec must be an object, not ${JSON.stringify(spec)}`);
6897
+ if ("version" in spec) {
6898
+ const version = spec.version;
6899
+ if (typeof version !== "string")
6900
+ throw mdzError("ERR_MANIFEST_INVALID", `spec.version must be a string, not ${JSON.stringify(version)}`);
6901
+ if (!SEMVER_RE.test(version))
6902
+ throw mdzError("ERR_MANIFEST_INVALID", `spec.version '${version}' is not a semantic version`);
6903
+ if (Number.parseInt(version, 10) > MDZ_SUPPORTED_MAJOR) {
6904
+ throw mdzError("ERR_VERSION_UNSUPPORTED", `archive targets spec ${version}; this library supports ${MDZ_SUPPORTED_MAJOR}.x`);
6905
+ }
6906
+ }
6907
+ }
6908
+ if ("mode" in manifest) {
6909
+ const mode = manifest.mode;
6910
+ if (typeof mode !== "string")
6911
+ throw mdzError("ERR_MANIFEST_INVALID", `mode must be a string, not ${JSON.stringify(mode)}`);
6912
+ if (!MDZ_MODES.includes(mode))
6913
+ throw mdzError("ERR_MODE_UNSUPPORTED", `unrecognized mode '${mode}'; supported: ${MDZ_MODES.join(", ")}`);
6914
+ }
6915
+ if (options?.entryPoint !== false && "entryPoint" in manifest) {
6916
+ const problem = entryPointProblem(manifest.entryPoint);
6917
+ if (problem)
6918
+ throw mdzError("ERR_ENTRYPOINT_INVALID", problem);
6919
+ }
6920
+ return Object.entries(DESCRIPTIVE_FIELDS).filter(([name, ok]) => name in manifest && !ok(manifest[name])).map(([name]) => name);
6921
+ }
6922
+ var SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
6923
+ var BAD_SEGMENT_RE = /[/\\\u0000-\u001f\u007f]/;
6924
+ var BAD_PERCENT_RE = /%(?![0-9A-Fa-f]{2})/;
6925
+ function isMdzName(name) {
6926
+ return asciiLower(name).endsWith(".mdz");
6927
+ }
6928
+ function dirSegments(path) {
6929
+ return path.split("/").slice(0, -1).filter(Boolean);
6930
+ }
6931
+ function decodeSegments(dest) {
6932
+ if (dest.startsWith("/") || dest.includes("\\"))
6933
+ return null;
6934
+ const segments = [];
6935
+ for (const raw of dest.split("/")) {
6936
+ let segment = raw;
6937
+ if (raw.includes("%")) {
6938
+ if (BAD_PERCENT_RE.test(raw))
6939
+ return null;
6940
+ try {
6941
+ segment = decodeURIComponent(raw);
6942
+ } catch {
6943
+ return null;
6944
+ }
6945
+ }
6946
+ if (BAD_SEGMENT_RE.test(segment))
6947
+ return null;
6948
+ segments.push(segment);
6949
+ }
6950
+ return segments;
6951
+ }
6952
+ function permitted(host, path) {
6953
+ if (host.allowedPaths?.includes(path))
6954
+ return true;
6955
+ const root = host.trustRoot === void 0 ? "" : host.trustRoot;
6956
+ if (root === null)
6957
+ return false;
6958
+ return root === "" || path === root || path.startsWith(`${root.replace(/\/+$/, "")}/`);
6959
+ }
6960
+ function unresolved(reason) {
6961
+ return { kind: "unresolved", reason };
6962
+ }
6963
+ function resolveLink(source, link, host, use = "navigate") {
6964
+ const hostLocation = host.hostLocation ?? true;
6965
+ const outside = host.outsideLinks ?? true;
6966
+ const into = host.intoArchiveLinks ?? true;
6967
+ const loadOutside = host.loadOutsideResources ?? false;
6968
+ if (SCHEME_RE.test(link))
6969
+ return { kind: "url" };
6970
+ const hash = link.indexOf("#");
6971
+ let dest = hash >= 0 ? link.slice(0, hash) : link;
6972
+ const fragment = hash >= 0 ? link.slice(hash + 1) : "";
6973
+ const query = dest.indexOf("?");
6974
+ if (query >= 0)
6975
+ dest = dest.slice(0, query);
6976
+ const directory = dest.endsWith("/");
6977
+ const withFragment = (target) => fragment ? { ...target, fragment } : target;
6978
+ if (dest === "") {
6979
+ return withFragment("archive" in source ? { kind: "internal", entry: source.path } : { kind: "host", path: source.host });
6980
+ }
6981
+ const segments = decodeSegments(dest);
6982
+ if (!segments)
6983
+ return unresolved("invalid");
6984
+ let i = 0;
6985
+ let stack;
6986
+ if ("archive" in source) {
6987
+ stack = dirSegments(source.path);
6988
+ let left = false;
6989
+ while (i < segments.length) {
6990
+ const s = segments[i++];
6991
+ if (s === "" || s === ".")
6992
+ continue;
6993
+ if (s === "..") {
6994
+ if (!stack.length) {
6995
+ left = true;
6996
+ break;
6997
+ }
6998
+ stack.pop();
6999
+ } else {
7000
+ stack.push(s);
7001
+ }
7002
+ }
7003
+ if (!left) {
7004
+ if (directory)
7005
+ return unresolved("not-found");
7006
+ const entry = host.findEntry(source.archive, stack.join("/"));
7007
+ return entry ? withFragment({ kind: "internal", entry }) : unresolved("not-found");
7008
+ }
7009
+ if (!outside)
7010
+ return unresolved("unsupported");
7011
+ if (!hostLocation)
7012
+ return unresolved("no-host-location");
7013
+ if (use === "embed" && !loadOutside)
7014
+ return unresolved("denied");
7015
+ stack = dirSegments(source.archive);
7016
+ } else {
7017
+ stack = dirSegments(source.host);
7018
+ }
7019
+ while (i < segments.length) {
7020
+ const s = segments[i++];
7021
+ if (s === "" || s === ".")
7022
+ continue;
7023
+ if (s === "..") {
7024
+ if (!stack.length)
7025
+ return unresolved("not-found");
7026
+ stack.pop();
7027
+ continue;
7028
+ }
7029
+ stack.push(s);
7030
+ const hostPath2 = stack.join("/");
7031
+ if (isMdzName(s) && permitted(host, hostPath2) && host.kind(hostPath2) === "archive") {
7032
+ if (!into)
7033
+ return unresolved("unsupported");
7034
+ if ("archive" in source && hostPath2 !== source.archive && use === "embed" && !loadOutside)
7035
+ return unresolved("denied");
7036
+ return insideArchive(hostPath2, segments.slice(i), host, withFragment, directory);
7037
+ }
7038
+ }
7039
+ const hostPath = stack.join("/");
7040
+ if (!permitted(host, hostPath))
7041
+ return unresolved("denied");
7042
+ if (directory)
7043
+ return unresolved("not-found");
7044
+ return host.kind(hostPath) === "file" ? withFragment({ kind: "host", path: hostPath }) : unresolved("not-found");
7045
+ }
7046
+ function insideArchive(archive, segments, host, withFragment, directory) {
7047
+ const inner = [];
7048
+ for (let j = 0; j < segments.length; j += 1) {
7049
+ const s = segments[j];
7050
+ if (s === "" || s === ".")
7051
+ continue;
7052
+ if (s === "..") {
7053
+ if (!inner.length)
7054
+ return unresolved("invalid");
7055
+ inner.pop();
7056
+ continue;
7057
+ }
7058
+ inner.push(s);
7059
+ if (isMdzName(s) && j < segments.length - 1)
7060
+ return unresolved("nested-archive");
7061
+ }
7062
+ if (!inner.length) {
7063
+ const entryPoint = host.entryPoint(archive);
7064
+ return entryPoint ? withFragment({ kind: "archive", archive, entry: entryPoint }) : unresolved("not-found");
7065
+ }
7066
+ if (directory)
7067
+ return unresolved("not-found");
7068
+ const entry = host.findEntry(archive, inner.join("/"));
7069
+ return entry ? withFragment({ kind: "archive", archive, entry }) : unresolved("not-found");
7070
+ }
7071
+
6726
7072
  // ../../node_modules/@mdzip/core-js/dist/mdz-core.js
6727
- var PRODUCER_SPEC_VERSION = "1.1.0";
6728
- var CORE_LIBRARY_VERSION = "1.5.0";
7073
+ var PRODUCER_SPEC_VERSION = MDZ_SPEC_VERSION;
7074
+ var CORE_LIBRARY_VERSION = "2.0.1";
6729
7075
  var CORE_LIBRARY_URL = "https://github.com/mdzip-project/mdzip-core-js";
7076
+ function isSymlinkEntry(entry) {
7077
+ const mode = entry?.unixPermissions;
7078
+ return typeof mode === "number" && (mode & 61440) === 40960;
7079
+ }
6730
7080
  var MDZ_IMAGE_MIME_TYPES = {
6731
7081
  png: "image/png",
6732
7082
  jpg: "image/jpeg",
@@ -6746,11 +7096,11 @@ var MdzArchiveCore = class _MdzArchiveCore {
6746
7096
  static IMAGE_MIME_TYPES = MDZ_IMAGE_MIME_TYPES;
6747
7097
  static SUPPORTED_MDZ_MAJOR = 1;
6748
7098
  static SUPPORTED_MODES = ["document", "project"];
6749
- static SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
6750
7099
  static MARKDOWN_IMAGE_REF_RE = /!\[[^\]]*\]\(([^)\n]+)\)/g;
6751
7100
  static HTML_IMG_SRC_RE = /<img\b[^>]*\ssrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))[^>]*>/gi;
6752
7101
  static entriesCache = /* @__PURE__ */ new WeakMap();
6753
7102
  static manifestCache = /* @__PURE__ */ new WeakMap();
7103
+ static manifestIssuesCache = /* @__PURE__ */ new WeakMap();
6754
7104
  entryByteSizes;
6755
7105
  /**
6756
7106
  * Creates an archive wrapper around a previously loaded ZIP object.
@@ -6773,12 +7123,45 @@ var MdzArchiveCore = class _MdzArchiveCore {
6773
7123
  static async open(input, zipFactory) {
6774
7124
  const factory = zipFactory ?? _MdzArchiveCore.getDefaultZipFactory();
6775
7125
  if (zipFactory) {
6776
- return new _MdzArchiveCore(await factory.loadAsync(input));
7126
+ const zip2 = await _MdzArchiveCore.loadOrThrowZipInvalid(() => factory.loadAsync(input));
7127
+ _MdzArchiveCore.checkEntryNames(Object.keys(zip2.files));
7128
+ return new _MdzArchiveCore(zip2);
6777
7129
  }
6778
7130
  const bytes = await _MdzArchiveCore.readArchiveInputBytes(input);
6779
- const zip = await factory.loadAsync(bytes);
7131
+ const zip = await _MdzArchiveCore.loadOrThrowZipInvalid(() => factory.loadAsync(bytes));
7132
+ let names = Object.keys(zip.files);
7133
+ try {
7134
+ const raw = _MdzArchiveCore.parseRawZipEntries(bytes);
7135
+ for (const entry of raw) {
7136
+ if (entry.method !== 0 && entry.method !== 8) {
7137
+ throw mdzError("ERR_ZIP_INVALID", `entry '${entry.name}' uses compression method ${entry.method}; MDZip allows 0 (stored) and 8 (DEFLATE) (\xA74)`);
7138
+ }
7139
+ }
7140
+ names = raw.map((entry) => entry.name);
7141
+ } catch (error2) {
7142
+ if (!(error2 instanceof Error) || !error2.message.startsWith("ERR_ZIP_UNSUPPORTED"))
7143
+ throw error2;
7144
+ }
7145
+ _MdzArchiveCore.checkEntryNames(names);
6780
7146
  return new _MdzArchiveCore(zip, bytes);
6781
7147
  }
7148
+ /** Runs a ZIP load, reporting a container JSZip can't read as `ERR_ZIP_INVALID` (§4, §16.5). */
7149
+ static async loadOrThrowZipInvalid(load4) {
7150
+ try {
7151
+ return await load4();
7152
+ } catch (error2) {
7153
+ if (/^ERR_[A-Z_]+:/.test(error2 instanceof Error ? error2.message : ""))
7154
+ throw error2;
7155
+ const message = error2 instanceof Error ? error2.message : String(error2);
7156
+ throw mdzError("ERR_ZIP_INVALID", `not a readable ZIP archive (${message})`);
7157
+ }
7158
+ }
7159
+ /** Rejects NFC-duplicate entries and files that are also directories (§5.4, §13.1): never picks one. */
7160
+ static checkEntryNames(names) {
7161
+ const problem = entrySetProblem(names);
7162
+ if (problem)
7163
+ throw mdzError(problem.code, problem.message);
7164
+ }
6782
7165
  /**
6783
7166
  * Lazily derives per-entry uncompressed sizes from the raw archive bytes,
6784
7167
  * memoizing the result so the central-directory scan runs at most once.
@@ -6791,7 +7174,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
6791
7174
  if (this.rawBytes) {
6792
7175
  try {
6793
7176
  for (const entry of _MdzArchiveCore.parseRawZipEntries(this.rawBytes)) {
6794
- entryByteSizes.set(entry.name.toLowerCase(), entry.uncompressedSize);
7177
+ entryByteSizes.set(nfc(entry.name), entry.uncompressedSize);
6795
7178
  }
6796
7179
  } catch (error2) {
6797
7180
  if (!(error2 instanceof Error) || !error2.message.startsWith("ERR_ZIP_UNSUPPORTED")) {
@@ -6869,13 +7252,17 @@ var MdzArchiveCore = class _MdzArchiveCore {
6869
7252
  static async updateFiles(input, writes, removals = [], options) {
6870
7253
  const normalizedWrites = /* @__PURE__ */ new Map();
6871
7254
  for (const write of writes) {
6872
- const path = MdzPackagerCore.normalizePath(write.path);
7255
+ const path = nfc(MdzPackagerCore.normalizePath(write.path));
6873
7256
  const pathError = _MdzArchiveCore.validateArchivePath(path);
6874
7257
  if (pathError) {
6875
7258
  throw new Error(`ERR_PATH_INVALID: ${pathError}`);
6876
7259
  }
6877
- normalizedWrites.set(path.toLowerCase(), { path, content: write.content });
7260
+ normalizedWrites.set(path, { path, content: write.content });
6878
7261
  }
7262
+ const sourceBytes = await _MdzArchiveCore.readArchiveInputBytes(input);
7263
+ const archive = await _MdzArchiveCore.open(sourceBytes);
7264
+ const existingPaths = archive.listPaths();
7265
+ const removable = [...existingPaths, ...archive.listSpecialFiles()];
6879
7266
  const normalizedRemovals = /* @__PURE__ */ new Map();
6880
7267
  for (const removal of removals) {
6881
7268
  const path = MdzPackagerCore.normalizePath(removal);
@@ -6883,45 +7270,63 @@ var MdzArchiveCore = class _MdzArchiveCore {
6883
7270
  if (pathError) {
6884
7271
  throw new Error(`ERR_PATH_INVALID: ${pathError}`);
6885
7272
  }
6886
- normalizedRemovals.delete(path.toLowerCase());
6887
- normalizedRemovals.set(path.toLowerCase(), path);
7273
+ const stored = removable.find((p) => samePath(p, path));
7274
+ if (stored === void 0) {
7275
+ throw new Error(`ERR_NOT_FOUND: Entry "${path}" was not found in archive.`);
7276
+ }
7277
+ normalizedRemovals.set(nfc(stored), stored);
6888
7278
  }
6889
- for (const lower of normalizedRemovals.keys()) {
6890
- normalizedWrites.delete(lower);
7279
+ if (options?.dropSpecialFiles) {
7280
+ for (const stored of archive.listSpecialFiles())
7281
+ normalizedRemovals.set(nfc(stored), stored);
6891
7282
  }
6892
- const sourceBytes = await _MdzArchiveCore.readArchiveInputBytes(input);
6893
- const archive = await _MdzArchiveCore.open(sourceBytes);
6894
- const existingPaths = archive.listPaths();
6895
- const existingLower = new Set(existingPaths.map((p) => p.toLowerCase()));
6896
- for (const [lower, path] of normalizedRemovals) {
6897
- if (!existingLower.has(lower)) {
6898
- throw new Error(`ERR_NOT_FOUND: Entry "${path.toLowerCase()}" was not found in archive.`);
6899
- }
7283
+ for (const key of normalizedRemovals.keys()) {
7284
+ normalizedWrites.delete(key);
6900
7285
  }
7286
+ const changing = normalizedWrites.size > 0 || normalizedRemovals.size > 0;
7287
+ const existingManifest = await _MdzArchiveCore.readExistingManifestObject(archive.zip, changing);
7288
+ const entryPointBefore = MdzPackagerCore.resolveEntryPoint(existingPaths, existingManifest);
6901
7289
  const manifestWrite = normalizedWrites.get("manifest.json");
6902
7290
  let manifestObject = null;
7291
+ let explicitManifest = false;
6903
7292
  if (manifestWrite) {
7293
+ explicitManifest = true;
6904
7294
  manifestObject = _MdzArchiveCore.stampManifestObject(await _MdzArchiveCore.parseManifestObjectContent(manifestWrite.content, true), true);
7295
+ _MdzArchiveCore.validateManifest(manifestObject);
6905
7296
  } else if (options?.manifest) {
7297
+ explicitManifest = true;
6906
7298
  manifestObject = _MdzArchiveCore.stampManifestObject(options.manifest, false);
6907
- } else if ((normalizedWrites.size > 0 || normalizedRemovals.size > 0) && !normalizedRemovals.has("manifest.json") && existingLower.has("manifest.json")) {
6908
- const existing = await archive.readManifest();
6909
- if (existing) {
6910
- manifestObject = _MdzArchiveCore.stampManifestObject(existing, false);
7299
+ _MdzArchiveCore.validateManifest(manifestObject);
7300
+ } else if (changing && !normalizedRemovals.has("manifest.json")) {
7301
+ if (existingManifest) {
7302
+ manifestObject = _MdzArchiveCore.stampManifestObject(existingManifest, false);
7303
+ checkManifest(manifestObject);
7304
+ } else {
7305
+ manifestObject = _MdzArchiveCore.stampManifestObject({
7306
+ spec: { name: "mdzip-spec", version: PRODUCER_SPEC_VERSION },
7307
+ producer: { core: { name: "mdzip-core-js", version: CORE_LIBRARY_VERSION, url: CORE_LIBRARY_URL } },
7308
+ ...entryPointBefore && !normalizedRemovals.has(nfc(entryPointBefore)) ? { entryPoint: entryPointBefore } : {}
7309
+ }, true);
6911
7310
  }
6912
7311
  }
7312
+ if (manifestObject && !("entryPoint" in manifestObject) && entryPointBefore && !normalizedRemovals.has(nfc(entryPointBefore))) {
7313
+ manifestObject.entryPoint = entryPointBefore;
7314
+ }
6913
7315
  if (manifestObject) {
6914
- _MdzArchiveCore.validateManifest(manifestObject);
6915
7316
  normalizedWrites.set("manifest.json", {
6916
7317
  path: "manifest.json",
6917
7318
  content: _MdzArchiveCore.normaliseLf(JSON.stringify(manifestObject, null, 2))
6918
7319
  });
6919
7320
  }
6920
7321
  const nextPaths = [
6921
- ...existingPaths.filter((p) => !normalizedRemovals.has(p.toLowerCase()) && !normalizedWrites.has(p.toLowerCase())),
7322
+ ...existingPaths.filter((p) => !normalizedRemovals.has(nfc(p)) && !normalizedWrites.has(nfc(p))),
6922
7323
  ...[...normalizedWrites.values()].map((w) => w.path)
6923
7324
  ];
6924
- _MdzArchiveCore.ensureCreatableEntryPoint(nextPaths, manifestObject);
7325
+ const nextManifest = manifestObject ?? (normalizedRemovals.has("manifest.json") ? null : existingManifest);
7326
+ const entryPointAfter = _MdzArchiveCore.resolveEntryPointStrict(nextPaths, nextManifest);
7327
+ if (entryPointBefore && !explicitManifest && !samePath(entryPointBefore, entryPointAfter)) {
7328
+ throw new Error(`ERR_ENTRYPOINT_CHANGED: this change would move the entry point from "${entryPointBefore}" to "${entryPointAfter}"; set manifest entryPoint to change it.`);
7329
+ }
6925
7330
  let patchedBytes;
6926
7331
  try {
6927
7332
  patchedBytes = await _MdzArchiveCore.patchArchiveBytes(sourceBytes, normalizedWrites, normalizedRemovals);
@@ -6931,10 +7336,10 @@ var MdzArchiveCore = class _MdzArchiveCore {
6931
7336
  }
6932
7337
  const zip = await _MdzArchiveCore.loadZip(sourceBytes);
6933
7338
  for (const path of normalizedRemovals.values()) {
6934
- _MdzArchiveCore.removeEntryIgnoreCase(zip, path);
7339
+ _MdzArchiveCore.removeEntrySamePath(zip, path);
6935
7340
  }
6936
7341
  for (const write of normalizedWrites.values()) {
6937
- _MdzArchiveCore.removeEntryIgnoreCase(zip, write.path);
7342
+ _MdzArchiveCore.removeEntrySamePath(zip, write.path);
6938
7343
  await _MdzArchiveCore.writeZipEntry(zip, write.path, write.content);
6939
7344
  }
6940
7345
  return _MdzArchiveCore.finalizeMutation(zip);
@@ -6970,8 +7375,8 @@ var MdzArchiveCore = class _MdzArchiveCore {
6970
7375
  const view = new DataView(source.buffer, source.byteOffset, source.byteLength);
6971
7376
  const output = [];
6972
7377
  for (const entry of entries) {
6973
- const lower = entry.name.toLowerCase();
6974
- if (removals.has(lower) || writes.has(lower)) {
7378
+ const key = nfc(entry.name);
7379
+ if (removals.has(key) || writes.has(key)) {
6975
7380
  continue;
6976
7381
  }
6977
7382
  if (entry.localOffset + 30 > source.length || view.getUint32(entry.localOffset, true) !== 67324752) {
@@ -7258,7 +7663,74 @@ var MdzArchiveCore = class _MdzArchiveCore {
7258
7663
  * @param path - Archive-relative path.
7259
7664
  */
7260
7665
  static isMarkdownFile(path) {
7261
- return /\.(md|markdown)$/i.test(path);
7666
+ return isMarkdownPath(path);
7667
+ }
7668
+ /** Support file names (spec 1.2.0 §5.5), compared ASCII case-insensitively, with .md or .markdown. */
7669
+ static SUPPORT_FILE_NAMES = ["readme", "agents", "license", "changelog", "contributing"];
7670
+ /** Lowercases A–Z only — never locale-aware (spec 1.2.0 §5.4.1; Turkish rules turn LICENSE into lıcense). */
7671
+ static asciiLower(text) {
7672
+ return asciiLower(text);
7673
+ }
7674
+ /**
7675
+ * Returns true for a root-level support file (spec 1.2.0 §5.5): README, AGENTS, LICENSE,
7676
+ * CHANGELOG or CONTRIBUTING, with a .md or .markdown extension. Support files are set aside
7677
+ * when choosing the entry point from root-level Markdown files.
7678
+ */
7679
+ static isSupportFile(path) {
7680
+ return isSupportFile(path);
7681
+ }
7682
+ /** Path equality per spec 1.2.0 §5.4.1: case-sensitive, after Unicode NFC normalization. */
7683
+ static samePath(a, b) {
7684
+ return samePath(a, b);
7685
+ }
7686
+ /**
7687
+ * Why a manifest `entryPoint` value isn't a valid entry point path (spec 1.2.0 §5.5), or null.
7688
+ * Valid: a non-empty string, a relative archive path (§5.4), naming a file (no trailing `/`),
7689
+ * with a .md/.markdown extension. A single leading `./` is allowed.
7690
+ */
7691
+ static entryPointError(value) {
7692
+ return entryPointProblem(value);
7693
+ }
7694
+ /**
7695
+ * Resolves the entry point per spec 1.2.0 §5.5, throwing a coded error when there is none:
7696
+ *
7697
+ * 1. A manifest `entryPoint`, matched case-sensitively after NFC normalization. An invalid
7698
+ * value throws `ERR_ENTRYPOINT_INVALID`; a valid one that matches no entry throws
7699
+ * `ERR_ENTRYPOINT_MISSING`. Neither falls back to the rules below.
7700
+ * 2. `index.md` at the root.
7701
+ * 3. The single root-level Markdown file once support files are set aside; or, if every
7702
+ * root-level Markdown file is a support file and there is exactly one, that file.
7703
+ * 4. Otherwise `ERR_ENTRYPOINT_UNRESOLVED`.
7704
+ *
7705
+ * @param archivePaths - File paths in the archive (directories are ignored).
7706
+ * @param manifest - Parsed manifest, or null.
7707
+ * @returns The matching path as stored in the archive.
7708
+ */
7709
+ static resolveEntryPointStrict(archivePaths, manifest) {
7710
+ const files = archivePaths.filter((p) => !p.endsWith("/"));
7711
+ const find = (wanted) => files.find((p) => _MdzArchiveCore.samePath(p, wanted)) ?? null;
7712
+ if (manifest && "entryPoint" in manifest && manifest.entryPoint !== void 0) {
7713
+ const problem = _MdzArchiveCore.entryPointError(manifest.entryPoint);
7714
+ if (problem)
7715
+ throw new Error(`ERR_ENTRYPOINT_INVALID: ${problem}`);
7716
+ const value = manifest.entryPoint;
7717
+ const found = find(value.startsWith("./") ? value.slice(2) : value);
7718
+ if (!found)
7719
+ throw new Error(`ERR_ENTRYPOINT_MISSING: manifest.json references "${value}" which is not in the archive`);
7720
+ return found;
7721
+ }
7722
+ const index = find("index.md");
7723
+ if (index)
7724
+ return index;
7725
+ const rootMarkdown = files.filter((p) => !p.includes("/") && _MdzArchiveCore.isMarkdownFile(p));
7726
+ const content = rootMarkdown.filter((p) => !_MdzArchiveCore.isSupportFile(p));
7727
+ if (content.length === 1)
7728
+ return content[0];
7729
+ if (content.length === 0 && rootMarkdown.length === 1)
7730
+ return rootMarkdown[0];
7731
+ if (rootMarkdown.length === 0)
7732
+ throw new Error("ERR_ENTRYPOINT_UNRESOLVED: No Markdown file found at the archive root.");
7733
+ throw new Error("ERR_ENTRYPOINT_UNRESOLVED: Multiple Markdown files at the archive root and no manifest.json entryPoint. Add an index.md or manifest.json entryPoint.");
7262
7734
  }
7263
7735
  /**
7264
7736
  * Infers a MIME type from an archive path.
@@ -7368,7 +7840,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7368
7840
  static buildPathTree(paths) {
7369
7841
  const roots = [];
7370
7842
  const ensureNode = (siblings, name, path, isDirectory) => {
7371
- let node = siblings.find((item) => item.name.toLowerCase() === name.toLowerCase() && item.isDirectory === isDirectory);
7843
+ let node = siblings.find((item) => samePath(item.name, name) && item.isDirectory === isDirectory);
7372
7844
  if (!node) {
7373
7845
  node = { name, path, isDirectory, children: [] };
7374
7846
  siblings.push(node);
@@ -7406,24 +7878,24 @@ var MdzArchiveCore = class _MdzArchiveCore {
7406
7878
  * @returns `null` when valid, otherwise a short reason string.
7407
7879
  */
7408
7880
  static validateArchivePath(path) {
7409
- const raw = String(path || "");
7410
- if (!raw)
7411
- return "empty path";
7412
- if (raw.startsWith("/"))
7413
- return "leading slash";
7414
- if (raw.includes("\\"))
7415
- return "reserved character";
7416
- const normalized = raw;
7417
- if (normalized.split("/").includes(".."))
7418
- return "path traversal";
7419
- for (const c of normalized) {
7420
- const code = c.charCodeAt(0);
7421
- if (c === "\0" || code >= 1 && code <= 31 || code === 127)
7422
- return "control char";
7423
- }
7424
- if (/[\\:*?"<>|]/.test(normalized))
7425
- return "reserved character";
7426
- return null;
7881
+ const problem = pathProblem(path);
7882
+ return problem ? _MdzArchiveCore.PATH_PROBLEM_REASONS[problem.code] ?? problem.message : null;
7883
+ }
7884
+ /** Short reasons returned by {@link validateArchivePath}, by spec error code. */
7885
+ static PATH_PROBLEM_REASONS = {
7886
+ ERR_PATH_EMPTY: "empty path",
7887
+ ERR_PATH_LEADING_SLASH: "leading slash",
7888
+ ERR_PATH_CONTROL_CHAR: "control char",
7889
+ ERR_PATH_RESERVED_CHAR: "reserved character",
7890
+ ERR_PATH_TRAVERSAL: "path traversal",
7891
+ ERR_PATH_INVALID: "empty or dot segment"
7892
+ };
7893
+ /**
7894
+ * Why `path` isn't a valid archive entry path (§5.4) as a spec error code and message, or null.
7895
+ * A trailing `/` marks a directory entry.
7896
+ */
7897
+ static pathProblem(path) {
7898
+ return pathProblem(path);
7427
7899
  }
7428
7900
  static dirOf(filePath) {
7429
7901
  const i = filePath.lastIndexOf("/");
@@ -7471,18 +7943,59 @@ var MdzArchiveCore = class _MdzArchiveCore {
7471
7943
  return out.join("/");
7472
7944
  }
7473
7945
  /**
7474
- * Finds an archive entry by path (case-insensitive fallback).
7946
+ * Finds an archive entry by path: case-sensitive, after Unicode NFC normalization (§5.4.1).
7947
+ * Symbolic link entries are never returned (§13.1); see {@link listSpecialFiles}.
7475
7948
  *
7476
7949
  * @param path - Archive-relative path.
7477
7950
  */
7478
7951
  findEntry(path) {
7479
- const normalized = _MdzArchiveCore.normalizePath(path);
7480
- if (this.zip.files[normalized])
7481
- return this.zip.files[normalized];
7482
7952
  if (!_MdzArchiveCore.entriesCache.has(this.zip)) {
7483
- _MdzArchiveCore.entriesCache.set(this.zip, Object.fromEntries(Object.entries(this.zip.files).map(([k, v]) => [_MdzArchiveCore.normalizePath(k).toLowerCase(), v])));
7953
+ _MdzArchiveCore.entriesCache.set(this.zip, Object.fromEntries(Object.entries(this.zip.files).filter(([, v]) => !isSymlinkEntry(v)).map(([k, v]) => [nfc(_MdzArchiveCore.normalizePath(k)), v])));
7484
7954
  }
7485
- return _MdzArchiveCore.entriesCache.get(this.zip)?.[normalized.toLowerCase()] ?? null;
7955
+ return _MdzArchiveCore.entriesCache.get(this.zip)?.[nfc(_MdzArchiveCore.normalizePath(path))] ?? null;
7956
+ }
7957
+ /**
7958
+ * The stored path of an entry whose name differs from `path` only in case, for reporting a
7959
+ * failed lookup ("did you mean ...?", §5.4.1). Never use it to open the entry.
7960
+ *
7961
+ * @param path - Archive-relative path that wasn't found.
7962
+ */
7963
+ findCaseOnlyMatch(path) {
7964
+ const normalized = _MdzArchiveCore.normalizePath(path);
7965
+ const wanted = nfc(normalized).toLowerCase();
7966
+ return this.listPaths().find((p) => nfc(p).toLowerCase() === wanted && !samePath(p, normalized)) ?? null;
7967
+ }
7968
+ /**
7969
+ * Symbolic link entries (and other special files) in the archive. They are not listed by
7970
+ * {@link listPaths}, can't be read and are never followed (§4, §13.1); `validate()` reports
7971
+ * them. Mutations keep them unless `dropSpecialFiles` is set.
7972
+ */
7973
+ listSpecialFiles() {
7974
+ return Object.entries(this.zip.files).filter(([, entry]) => isSymlinkEntry(entry)).map(([path]) => _MdzArchiveCore.normalizePath(path));
7975
+ }
7976
+ /**
7977
+ * Resolves a Markdown link written in `document` (§9.3–9.6). Without a `host`, only links inside
7978
+ * this archive resolve; links that leave it are `unresolved` with reason `no-host-location`
7979
+ * (pass a host that knows the archive's location and its neighbours to follow them).
7980
+ *
7981
+ * @param document - Archive path of the document containing the link.
7982
+ * @param link - The link destination as written.
7983
+ * @param host - Optional host model; this archive is addressed as `host` path `archivePath`.
7984
+ * @param options - `archivePath` (this archive's host path, default `archive.mdz`) and `use`.
7985
+ */
7986
+ resolveLink(document, link, host, options) {
7987
+ const archivePath = options?.archivePath ?? "archive.mdz";
7988
+ const self2 = this;
7989
+ const linkHost = host ?? {
7990
+ kind: () => null,
7991
+ findEntry: (_archive, entry) => {
7992
+ const found = self2.findEntry(entry);
7993
+ return found && !found.dir ? _MdzArchiveCore.normalizePath(found.name) : null;
7994
+ },
7995
+ entryPoint: () => null,
7996
+ hostLocation: false
7997
+ };
7998
+ return resolveLink({ archive: archivePath, path: document }, link, linkHost, options?.use ?? "navigate");
7486
7999
  }
7487
8000
  /**
7488
8001
  * Lists archive paths.
@@ -7502,7 +8015,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7502
8015
  const entryByteSizes = this.getEntryByteSizes();
7503
8016
  return entries.map((entry) => ({
7504
8017
  path: entry.path,
7505
- byteSize: entryByteSizes.get(entry.path.toLowerCase()),
8018
+ byteSize: entryByteSizes.get(nfc(entry.path)),
7506
8019
  isMarkdown: !entry.isDirectory && _MdzArchiveCore.isMarkdownFile(entry.path),
7507
8020
  isImage: !entry.isDirectory && _MdzArchiveCore.isImagePath(entry.path),
7508
8021
  isDirectory: entry.isDirectory
@@ -7565,7 +8078,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7565
8078
  async findOrphanedAssets(options) {
7566
8079
  const allEntries = this.listEntries();
7567
8080
  const assetPaths = allEntries.filter((entry) => entry.isImage).map((entry) => entry.path).sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
7568
- const assetPathSet = new Set(assetPaths.map((p) => p.toLowerCase()));
8081
+ const assetPathSet = new Set(assetPaths.map((p) => nfc(p)));
7569
8082
  const scanMode = options?.scanMode ?? "entrypoint";
7570
8083
  const scannedMarkdownPaths = scanMode === "all-markdown" ? allEntries.filter((entry) => entry.isMarkdown).map((entry) => entry.path) : [options?.entryPoint ? _MdzArchiveCore.normalizePath(options.entryPoint) : await this.resolveEntryPoint()];
7571
8084
  const referencedAssets = /* @__PURE__ */ new Set();
@@ -7590,7 +8103,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7590
8103
  unresolvedReferences.push({ sourcePath: markdownPath, reference: ref, reason: "not-found" });
7591
8104
  continue;
7592
8105
  }
7593
- if (!_MdzArchiveCore.isImagePath(resolved) || !assetPathSet.has(resolved.toLowerCase())) {
8106
+ if (!_MdzArchiveCore.isImagePath(resolved) || !assetPathSet.has(nfc(resolved))) {
7594
8107
  unresolvedReferences.push({ sourcePath: markdownPath, reference: ref, reason: "not-asset" });
7595
8108
  continue;
7596
8109
  }
@@ -7600,13 +8113,13 @@ var MdzArchiveCore = class _MdzArchiveCore {
7600
8113
  const manifest = await this.readManifest();
7601
8114
  if (manifest?.cover) {
7602
8115
  const cover = _MdzArchiveCore.normalizePath(manifest.cover);
7603
- if (assetPathSet.has(cover.toLowerCase())) {
7604
- referencedAssets.add(_MdzArchiveCore.resolveAssetPathCase(assetPaths, cover));
8116
+ if (assetPathSet.has(nfc(cover.replace(/^\.\//, "")))) {
8117
+ referencedAssets.add(_MdzArchiveCore.resolveAssetPathCase(assetPaths, cover.replace(/^\.\//, "")));
7605
8118
  }
7606
8119
  }
7607
8120
  const referencedAssetPaths = Array.from(referencedAssets).sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
7608
- const referencedSet = new Set(referencedAssetPaths.map((p) => p.toLowerCase()));
7609
- const orphanedAssetPaths = assetPaths.filter((p) => !referencedSet.has(p.toLowerCase()));
8121
+ const referencedSet = new Set(referencedAssetPaths.map((p) => nfc(p)));
8122
+ const orphanedAssetPaths = assetPaths.filter((p) => !referencedSet.has(nfc(p)));
7610
8123
  return {
7611
8124
  scannedMarkdownPaths,
7612
8125
  assetPaths,
@@ -7630,7 +8143,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7630
8143
  const includeLazyDocReaders = options?.includeLazyDocumentReaders === true;
7631
8144
  const documents = [];
7632
8145
  for (const entry of entries.filter((item) => item.isMarkdown)) {
7633
- const isEntryPt = !!entryPoint && entry.path.toLowerCase() === entryPoint.toLowerCase();
8146
+ const isEntryPt = !!entryPoint && samePath(entry.path, entryPoint);
7634
8147
  const useLazy = includeLazyDocReaders && !isEntryPt;
7635
8148
  const doc = {
7636
8149
  path: entry.path,
@@ -7645,7 +8158,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7645
8158
  documents.push(doc);
7646
8159
  }
7647
8160
  const assets = [];
7648
- for (const entry of entries.filter((item) => !item.isMarkdown && !item.isDirectory && item.path.toLowerCase() !== "manifest.json")) {
8161
+ for (const entry of entries.filter((item) => !item.isMarkdown && !item.isDirectory && item.path !== "manifest.json")) {
7649
8162
  const mimeType = _MdzArchiveCore.inferMimeType(entry.path);
7650
8163
  const asset = {
7651
8164
  path: entry.path,
@@ -7675,163 +8188,60 @@ var MdzArchiveCore = class _MdzArchiveCore {
7675
8188
  }
7676
8189
  return workspace;
7677
8190
  }
8191
+ /**
8192
+ * Checks a manifest a producer is about to write (§6.4, §14.1). Critical fields must be valid
8193
+ * (see `checkManifest`), and so must every descriptive field present: a producer must not
8194
+ * write values a consumer would ignore. Unknown fields are never checked. Throws `ERR_*` errors.
8195
+ */
7678
8196
  static validateManifest(manifest) {
7679
8197
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
7680
8198
  throw new Error("ERR_MANIFEST_INVALID: manifest.json must be a JSON object.");
7681
8199
  }
7682
- const candidate = manifest;
7683
- const validateMetadataNode = (value, path) => {
7684
- if (value == null)
7685
- return;
7686
- if (typeof value !== "object" || Array.isArray(value)) {
7687
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}" must be an object when provided.`);
7688
- }
7689
- const node = value;
7690
- for (const key of ["name", "version", "url"]) {
7691
- const v = node[key];
7692
- if (v != null && typeof v !== "string") {
7693
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}.${key}" must be a string when provided.`);
7694
- }
7695
- }
7696
- };
7697
- const validateByNode = (value, path) => {
7698
- if (value == null)
7699
- return;
7700
- if (typeof value !== "object" || Array.isArray(value)) {
7701
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}" must be an object when provided.`);
7702
- }
7703
- const node = value;
7704
- for (const key of ["name", "email", "url"]) {
7705
- const v = node[key];
7706
- if (v != null && typeof v !== "string") {
7707
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}.${key}" must be a string when provided.`);
7708
- }
7709
- }
7710
- };
7711
- const validateTimestamp = (value, path) => {
7712
- if (value == null)
7713
- return;
7714
- if (typeof value === "string")
7715
- return;
7716
- if (typeof value !== "object" || Array.isArray(value)) {
7717
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}" must be a string or object when provided.`);
7718
- }
7719
- const node = value;
7720
- if (typeof node.when !== "string" || node.when.trim() === "") {
7721
- throw new Error(`ERR_MANIFEST_INVALID: manifest.json "${path}.when" must be a non-empty string when "${path}" is an object.`);
7722
- }
7723
- validateByNode(node.by, `${path}.by`);
7724
- };
7725
- if (candidate.title != null && (typeof candidate.title !== "string" || candidate.title.trim() === "")) {
7726
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "title" must be a non-empty string when provided.');
7727
- }
7728
- if (candidate.mode != null) {
7729
- if (typeof candidate.mode !== "string") {
7730
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "mode" must be a string when provided.');
7731
- }
7732
- if (!_MdzArchiveCore.SUPPORTED_MODES.includes(candidate.mode)) {
7733
- throw new Error(`ERR_MODE_UNSUPPORTED: manifest.json mode "${candidate.mode}" is not supported.`);
7734
- }
7735
- }
7736
- if (candidate.description != null && typeof candidate.description !== "string") {
7737
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "description" must be a string when provided.');
7738
- }
7739
- if (candidate.version != null && typeof candidate.version !== "string") {
7740
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "version" must be a string when provided.');
7741
- }
7742
- if (candidate.language != null && typeof candidate.language !== "string") {
7743
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "language" must be a string when provided.');
7744
- }
7745
- if (candidate.license != null && typeof candidate.license !== "string") {
7746
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "license" must be a string when provided.');
7747
- }
7748
- if (candidate.keywords != null) {
7749
- if (!Array.isArray(candidate.keywords) || candidate.keywords.some((k) => typeof k !== "string")) {
7750
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "keywords" must be an array of strings when provided.');
7751
- }
7752
- }
7753
- if (candidate.cover != null && typeof candidate.cover !== "string") {
7754
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "cover" must be a string when provided.');
7755
- }
7756
- if (candidate.entryPoint != null && typeof candidate.entryPoint !== "string") {
7757
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "entryPoint" must be a string when provided.');
7758
- }
7759
- if (candidate.entryPoint != null && _MdzArchiveCore.validateArchivePath(candidate.entryPoint) != null) {
7760
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "entryPoint" must be a valid archive-relative path.');
7761
- }
7762
- if (candidate.spec != null) {
7763
- if (typeof candidate.spec !== "object" || Array.isArray(candidate.spec)) {
7764
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "spec" must be an object when provided.');
7765
- }
7766
- const spec = candidate.spec;
7767
- if (spec.name != null && typeof spec.name !== "string") {
7768
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "spec.name" must be a string when provided.');
7769
- }
7770
- if (spec.version != null) {
7771
- if (typeof spec.version !== "string" || !_MdzArchiveCore.SEMVER_RE.test(spec.version)) {
7772
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "spec.version" must be a valid semver string when provided.');
7773
- }
7774
- const specMajor = Number.parseInt(spec.version.split(".")[0] ?? "0", 10);
7775
- if (specMajor > _MdzArchiveCore.SUPPORTED_MDZ_MAJOR) {
7776
- throw new Error(`ERR_VERSION_UNSUPPORTED: manifest.json spec.version ${spec.version} is not supported; this viewer supports major ${_MdzArchiveCore.SUPPORTED_MDZ_MAJOR}.x only.`);
7777
- }
7778
- }
7779
- }
7780
- if (candidate.mdz != null) {
7781
- if (typeof candidate.mdz !== "string" || !_MdzArchiveCore.SEMVER_RE.test(candidate.mdz)) {
7782
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "mdz" must be a valid semver string when provided.');
7783
- }
7784
- const major = Number.parseInt(candidate.mdz.split(".")[0] ?? "0", 10);
7785
- if (major > _MdzArchiveCore.SUPPORTED_MDZ_MAJOR) {
7786
- throw new Error(`ERR_VERSION_UNSUPPORTED: manifest.json targets mdz ${candidate.mdz}, but this viewer supports major ${_MdzArchiveCore.SUPPORTED_MDZ_MAJOR}.x only.`);
7787
- }
7788
- }
7789
- if (candidate.producer != null) {
7790
- if (typeof candidate.producer !== "object" || Array.isArray(candidate.producer)) {
7791
- throw new Error('ERR_MANIFEST_INVALID: manifest.json "producer" must be an object when provided.');
7792
- }
7793
- const producer = candidate.producer;
7794
- validateMetadataNode(producer.application, "producer.application");
7795
- validateMetadataNode(producer.core, "producer.core");
8200
+ const ignored = checkManifest(manifest);
8201
+ _MdzArchiveCore.checkLegacyMdzField(manifest);
8202
+ if (ignored.length) {
8203
+ throw mdzError("ERR_MANIFEST_INVALID", `manifest.json has invalid values for ${ignored.map((field) => `"${field}"`).join(", ")} (\xA76.4)`);
7796
8204
  }
7797
- if (candidate.author != null) {
7798
- validateByNode(candidate.author, "author");
8205
+ }
8206
+ /** The pre-1.0 `mdz` version field: a newer major is still refused; anything else is left alone. */
8207
+ static checkLegacyMdzField(manifest) {
8208
+ const mdz = manifest.mdz;
8209
+ if (isSemver(mdz) && Number.parseInt(mdz, 10) > _MdzArchiveCore.SUPPORTED_MDZ_MAJOR) {
8210
+ throw mdzError("ERR_VERSION_UNSUPPORTED", `manifest.json targets mdz ${mdz}, but this library supports major ${_MdzArchiveCore.SUPPORTED_MDZ_MAJOR}.x only.`);
7799
8211
  }
7800
- validateTimestamp(candidate.created, "created");
7801
- validateTimestamp(candidate.modified, "modified");
7802
8212
  }
7803
8213
  /**
7804
- * Parses and validates `manifest.json` if present.
8214
+ * Parses and checks `manifest.json` if present (§6.4).
7805
8215
  *
7806
- * Missing or invalid cover references are normalized away in returned data.
8216
+ * Throws for an unreadable manifest or an invalid critical field (`spec`, `spec.version`,
8217
+ * `mode`, `entryPoint`). Descriptive fields with a wrong type or value are ignored — left out
8218
+ * of the returned object and reported by {@link validate} — as is a `cover` that names no file.
8219
+ * Unknown fields are returned unchanged. For duplicate members the last one wins.
7807
8220
  */
7808
8221
  async readManifest() {
7809
8222
  if (_MdzArchiveCore.manifestCache.has(this.zip)) {
7810
8223
  return _MdzArchiveCore.manifestCache.get(this.zip) ?? null;
7811
8224
  }
7812
- const entry = this.zip.files["manifest.json"];
7813
- if (!entry) {
8225
+ const entry = this.findEntry("manifest.json");
8226
+ if (!entry || entry.dir) {
7814
8227
  _MdzArchiveCore.manifestCache.set(this.zip, null);
7815
8228
  return null;
7816
8229
  }
7817
- const raw = await entry.async("text");
7818
- let manifest;
7819
- try {
7820
- manifest = JSON.parse(String(raw));
7821
- } catch {
7822
- throw new Error("ERR_MANIFEST_INVALID: manifest.json is not valid JSON");
7823
- }
7824
- _MdzArchiveCore.validateManifest(manifest);
8230
+ const { manifest, duplicateMembers } = parseManifestText(String(await entry.async("text")));
8231
+ const ignoredFields = checkManifest(manifest);
8232
+ _MdzArchiveCore.checkLegacyMdzField(manifest);
7825
8233
  const normalized = { ...manifest };
7826
- if (normalized.cover) {
7827
- const coverPath = _MdzArchiveCore.normalizePath(normalized.cover);
7828
- const coverPathError = _MdzArchiveCore.validateArchivePath(coverPath);
7829
- const coverEntry = Object.entries(this.zip.files).find(([p]) => _MdzArchiveCore.normalizePath(p).toLowerCase() === coverPath.toLowerCase())?.[1];
7830
- const coverExistsAsFile = !!coverEntry && !coverEntry.dir;
7831
- if (coverPathError || !coverExistsAsFile) {
8234
+ for (const field of ignoredFields)
8235
+ delete normalized[field];
8236
+ let missingCover = null;
8237
+ if (typeof normalized.cover === "string") {
8238
+ const cover = this.findEntry(normalized.cover.replace(/^\.\//, ""));
8239
+ if (!cover || cover.dir) {
8240
+ missingCover = normalized.cover;
7832
8241
  delete normalized.cover;
7833
8242
  }
7834
8243
  }
8244
+ _MdzArchiveCore.manifestIssuesCache.set(this.zip, { ignoredFields, duplicateMembers, missingCover });
7835
8245
  _MdzArchiveCore.manifestCache.set(this.zip, normalized);
7836
8246
  return normalized;
7837
8247
  }
@@ -7844,29 +8254,26 @@ var MdzArchiveCore = class _MdzArchiveCore {
7844
8254
  }
7845
8255
  /**
7846
8256
  * Validates archive conformance and returns errors/warnings.
8257
+ *
8258
+ * Errors: invalid entry names (§5.4), an unreadable manifest or invalid critical field (§6.4),
8259
+ * and an entry point that can't be resolved (§5.5). Warnings: symbolic link entries, which are
8260
+ * skipped (§4, §13.1); ignored descriptive manifest fields and duplicate members (§6.4); a
8261
+ * missing `cover`; and missing version metadata.
7847
8262
  */
7848
8263
  async validate() {
7849
8264
  const errors = [];
7850
8265
  const warnings = [];
7851
- const archivePaths = _MdzArchiveCore.getArchivePaths(this.zip);
7852
- for (const path of archivePaths) {
7853
- const pathError = _MdzArchiveCore.validateArchivePath(path);
7854
- if (pathError)
7855
- errors.push(`ERR_PATH_INVALID: ${pathError}`);
8266
+ for (const path of _MdzArchiveCore.getArchivePaths(this.zip)) {
8267
+ const problem = pathProblem(path);
8268
+ if (problem)
8269
+ errors.push(`ERR_PATH_INVALID: ${problem.message}`);
8270
+ }
8271
+ for (const path of this.listSpecialFiles()) {
8272
+ warnings.push(`ERR_PATH_INVALID: symbolic link '${path}' is ignored and never followed (\xA74, \xA713.1).`);
7856
8273
  }
7857
8274
  let manifest = null;
7858
8275
  let manifestReadFailed = false;
7859
- const manifestEntry = this.findEntry("manifest.json");
7860
- if (manifestEntry) {
7861
- let rawManifest = null;
7862
- try {
7863
- const rawText = await manifestEntry.async("text");
7864
- const parsed = JSON.parse(String(rawText));
7865
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
7866
- rawManifest = parsed;
7867
- }
7868
- } catch {
7869
- }
8276
+ if (this.findEntry("manifest.json")) {
7870
8277
  try {
7871
8278
  manifest = await this.readManifest();
7872
8279
  } catch (error2) {
@@ -7874,25 +8281,21 @@ var MdzArchiveCore = class _MdzArchiveCore {
7874
8281
  errors.push(error2 instanceof Error ? error2.message : String(error2));
7875
8282
  }
7876
8283
  if (manifest) {
8284
+ const issues = _MdzArchiveCore.manifestIssuesCache.get(this.zip);
8285
+ for (const field of issues?.ignoredFields ?? []) {
8286
+ warnings.push(`manifest '${field}' has an invalid type or value and is ignored (\xA76.4).`);
8287
+ }
8288
+ if (issues?.duplicateMembers.length) {
8289
+ warnings.push(`manifest.json repeats ${issues.duplicateMembers.map((m) => `'${m}'`).join(", ")}; the last of each is used (\xA76.4).`);
8290
+ }
8291
+ if (issues?.missingCover) {
8292
+ warnings.push(`manifest 'cover' references '${issues.missingCover}' which does not exist in the archive.`);
8293
+ }
7877
8294
  const specVersion = manifest.spec?.version;
7878
8295
  if (!specVersion || specVersion.trim() === "") {
7879
8296
  warnings.push("manifest 'spec.version' is missing; version metadata is unavailable.");
7880
- } else if (_MdzArchiveCore.SEMVER_RE.test(specVersion)) {
7881
- const major = Number.parseInt(specVersion.split(".")[0] ?? "0", 10);
7882
- if (major < _MdzArchiveCore.SUPPORTED_MDZ_MAJOR) {
7883
- warnings.push(`manifest 'spec.version' major version ${major} is older than supported major ${_MdzArchiveCore.SUPPORTED_MDZ_MAJOR}.`);
7884
- }
7885
- }
7886
- if (manifest.entryPoint && !archivePaths.some((p) => p.toLowerCase() === manifest.entryPoint.toLowerCase())) {
7887
- errors.push(`ERR_ENTRYPOINT_MISSING: manifest 'entryPoint' references '${manifest.entryPoint}' which does not exist in the archive.`);
7888
- }
7889
- const coverCandidate = typeof rawManifest?.cover === "string" ? rawManifest.cover : manifest.cover;
7890
- if (coverCandidate) {
7891
- const cover = _MdzArchiveCore.normalizePath(coverCandidate);
7892
- const coverEntry = this.findEntry(cover);
7893
- if (!coverEntry || coverEntry.dir) {
7894
- warnings.push(`manifest 'cover' references '${coverCandidate}' which does not exist in the archive.`);
7895
- }
8297
+ } else if (Number.parseInt(specVersion, 10) < _MdzArchiveCore.SUPPORTED_MDZ_MAJOR) {
8298
+ warnings.push(`manifest 'spec.version' major version ${Number.parseInt(specVersion, 10)} is older than supported major ${_MdzArchiveCore.SUPPORTED_MDZ_MAJOR}.`);
7896
8299
  }
7897
8300
  }
7898
8301
  } else {
@@ -7902,7 +8305,12 @@ var MdzArchiveCore = class _MdzArchiveCore {
7902
8305
  try {
7903
8306
  await this.resolveEntryPoint();
7904
8307
  } catch (error2) {
7905
- errors.push(error2 instanceof Error ? error2.message : String(error2));
8308
+ let message = error2 instanceof Error ? error2.message : String(error2);
8309
+ const wanted = typeof manifest?.entryPoint === "string" ? manifest.entryPoint.replace(/^\.\//, "") : null;
8310
+ const nearMatch = wanted && message.startsWith("ERR_ENTRYPOINT_MISSING") ? this.findCaseOnlyMatch(wanted) : null;
8311
+ if (nearMatch)
8312
+ message += ` (did you mean "${nearMatch}"? Paths are case-sensitive.)`;
8313
+ errors.push(message);
7906
8314
  }
7907
8315
  }
7908
8316
  return {
@@ -7919,17 +8327,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7919
8327
  async resolveEntryPoint() {
7920
8328
  const manifest = await this.readManifest();
7921
8329
  const archivePaths = _MdzArchiveCore.getArchivePaths(this.zip);
7922
- if (manifest?.entryPoint && !archivePaths.some((p) => p.toLowerCase() === manifest.entryPoint.toLowerCase())) {
7923
- throw new Error(`ERR_ENTRYPOINT_MISSING: manifest.json references "${manifest.entryPoint}" which is not in the archive`);
7924
- }
7925
- const resolved = MdzPackagerCore.resolveEntryPoint(archivePaths, manifest);
7926
- if (resolved)
7927
- return resolved;
7928
- const rootMd = archivePaths.filter((p) => !p.includes("/") && _MdzArchiveCore.isMarkdownFile(p));
7929
- if (rootMd.length > 1) {
7930
- throw new Error("ERR_ENTRYPOINT_UNRESOLVED: Multiple Markdown files at the archive root and no manifest.json entryPoint. Add an index.md or manifest.json entryPoint.");
7931
- }
7932
- throw new Error("ERR_ENTRYPOINT_UNRESOLVED: No Markdown file found at the archive root.");
8330
+ return _MdzArchiveCore.resolveEntryPointStrict(archivePaths, manifest);
7933
8331
  }
7934
8332
  static getArchivePaths(zip, options) {
7935
8333
  return _MdzArchiveCore.getArchiveEntries(zip, options).map((entry) => entry.path);
@@ -7938,7 +8336,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7938
8336
  const includeDirectories = options?.includeDirectories === true;
7939
8337
  const normalize = options?.normalize !== false;
7940
8338
  const sort = options?.sort !== false;
7941
- const entries = Object.entries(zip.files).filter(([, entry]) => includeDirectories || !entry?.dir).map(([path, entry]) => ({
8339
+ const entries = Object.entries(zip.files).filter(([, entry]) => (includeDirectories || !entry?.dir) && !isSymlinkEntry(entry)).map(([path, entry]) => ({
7942
8340
  path: normalize ? _MdzArchiveCore.normalizePath(path) : path,
7943
8341
  isDirectory: !!entry?.dir
7944
8342
  }));
@@ -7979,7 +8377,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
7979
8377
  return _MdzArchiveCore.getPathExtension(path) in MDZ_IMAGE_MIME_TYPES;
7980
8378
  }
7981
8379
  static getDocumentTitle(path, manifest) {
7982
- const mapped = manifest?.files?.find((file2) => file2.path.toLowerCase() === path.toLowerCase());
8380
+ const mapped = manifest?.files?.find((file2) => typeof file2?.path === "string" && samePath(file2.path, path));
7983
8381
  if (mapped?.title?.trim())
7984
8382
  return mapped.title.trim();
7985
8383
  const fileName = _MdzArchiveCore.basename(path).replace(/\.[^/.]+$/, "");
@@ -8040,26 +8438,24 @@ var MdzArchiveCore = class _MdzArchiveCore {
8040
8438
  return /^[A-Za-z][A-Za-z\d+\-.]*:/.test(value);
8041
8439
  }
8042
8440
  static resolveAssetPathCase(assetPaths, resolvedPath) {
8043
- const lower = resolvedPath.toLowerCase();
8044
- const exact = assetPaths.find((p) => p.toLowerCase() === lower);
8045
- return exact ?? resolvedPath;
8441
+ return assetPaths.find((p) => samePath(p, resolvedPath)) ?? resolvedPath;
8046
8442
  }
8047
- static removeEntryIgnoreCase(zip, targetPath) {
8048
- const targetLower = targetPath.toLowerCase();
8443
+ static removeEntrySamePath(zip, targetPath) {
8049
8444
  for (const path of Object.keys(zip.files)) {
8050
- if (_MdzArchiveCore.normalizePath(path).toLowerCase() === targetLower) {
8445
+ if (samePath(_MdzArchiveCore.normalizePath(path), targetPath)) {
8051
8446
  zip.remove(path);
8052
8447
  }
8053
8448
  }
8054
8449
  }
8450
+ /** Files whose line endings a producer normalizes to LF when it writes them (§12.2). */
8055
8451
  static isTextFile(path) {
8056
- return /\.(md|markdown|json|txt|css|html|htm|xml|svg|yaml|yml|toml)$/i.test(path);
8452
+ return isMarkdownPath(path) || path === "manifest.json";
8057
8453
  }
8058
8454
  static normaliseLf(content) {
8059
8455
  return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8060
8456
  }
8061
8457
  static async readExistingManifestObject(zip, requireValid) {
8062
- const manifestEntry = Object.entries(zip.files).find(([p, entry]) => _MdzArchiveCore.normalizePath(p).toLowerCase() === "manifest.json" && !entry.dir)?.[1];
8458
+ const manifestEntry = Object.entries(zip.files).find(([p, entry]) => _MdzArchiveCore.normalizePath(p) === "manifest.json" && !entry.dir)?.[1];
8063
8459
  if (!manifestEntry)
8064
8460
  return null;
8065
8461
  const raw = await manifestEntry.async("text");
@@ -8128,15 +8524,6 @@ var MdzArchiveCore = class _MdzArchiveCore {
8128
8524
  }
8129
8525
  return next;
8130
8526
  }
8131
- static ensureCreatableEntryPoint(archivePaths, manifest) {
8132
- if (manifest?.entryPoint && !archivePaths.some((p) => p.toLowerCase() === manifest.entryPoint.toLowerCase())) {
8133
- throw new Error(`ERR_ENTRYPOINT_MISSING: manifest 'entryPoint' references '${manifest.entryPoint}' which does not exist in the archive.`);
8134
- }
8135
- const resolved = MdzPackagerCore.resolveEntryPoint(archivePaths, manifest);
8136
- if (!resolved) {
8137
- throw new Error("ERR_ENTRYPOINT_UNRESOLVED: No unambiguous entry point could be determined. Add index.md at the archive root, keep exactly one root Markdown file, or set manifest.entryPoint.");
8138
- }
8139
- }
8140
8527
  static async readMutationInputAsText(content) {
8141
8528
  if (typeof content === "string")
8142
8529
  return content;
@@ -8248,8 +8635,8 @@ var MdzPackagerCore = class _MdzPackagerCore {
8248
8635
  * @param usedPaths - Case-insensitive set of already-used paths.
8249
8636
  */
8250
8637
  static makeUniqueArchivePath(candidate, usedPaths) {
8251
- if (!usedPaths.has(candidate.toLowerCase())) {
8252
- usedPaths.add(candidate.toLowerCase());
8638
+ if (!usedPaths.has(nfc(candidate).toLowerCase())) {
8639
+ usedPaths.add(nfc(candidate).toLowerCase());
8253
8640
  return candidate;
8254
8641
  }
8255
8642
  const slash = candidate.lastIndexOf("/");
@@ -8261,8 +8648,8 @@ var MdzPackagerCore = class _MdzPackagerCore {
8261
8648
  let n = 2;
8262
8649
  while (true) {
8263
8650
  const next = `${dir2}${base}-${n}${ext}`;
8264
- if (!usedPaths.has(next.toLowerCase())) {
8265
- usedPaths.add(next.toLowerCase());
8651
+ if (!usedPaths.has(nfc(next).toLowerCase())) {
8652
+ usedPaths.add(nfc(next).toLowerCase());
8266
8653
  return next;
8267
8654
  }
8268
8655
  n += 1;
@@ -8348,7 +8735,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
8348
8735
  if (!hasManifestOption)
8349
8736
  return null;
8350
8737
  const now = (/* @__PURE__ */ new Date()).toISOString();
8351
- return {
8738
+ return _MdzPackagerCore.pruneManifest({
8352
8739
  spec: {
8353
8740
  name: "mdzip-spec",
8354
8741
  version: PRODUCER_SPEC_VERSION
@@ -8362,30 +8749,56 @@ var MdzPackagerCore = class _MdzPackagerCore {
8362
8749
  },
8363
8750
  title: options.title || rootName,
8364
8751
  mode: options.mode || void 0,
8365
- entryPoint: options.entryPoint || null,
8366
- language: options.language || "en",
8752
+ entryPoint: options.entryPoint || void 0,
8753
+ language: options.language || void 0,
8367
8754
  author: options.author ? { name: options.author } : void 0,
8368
- authors: options.author ? [{ name: options.author }] : null,
8369
- description: options.description || null,
8370
- version: options.docVersion || null,
8755
+ authors: options.author ? [{ name: options.author }] : void 0,
8756
+ description: options.description || void 0,
8757
+ version: options.docVersion || void 0,
8371
8758
  created: now,
8372
8759
  modified: now
8373
- };
8760
+ });
8761
+ }
8762
+ /** Library-managed manifest fields that are left out rather than written as `null` (§6.4). */
8763
+ static OPTIONAL_MANIFEST_FIELDS = [
8764
+ "title",
8765
+ "mode",
8766
+ "entryPoint",
8767
+ "language",
8768
+ "author",
8769
+ "authors",
8770
+ "description",
8771
+ "version",
8772
+ "license",
8773
+ "keywords",
8774
+ "cover"
8775
+ ];
8776
+ /**
8777
+ * Removes library-managed fields that are `null` or `undefined`: §6.4 treats `null` as a wrong
8778
+ * type, so a missing value is written as an absent member. Unknown fields are left alone.
8779
+ */
8780
+ static pruneManifest(manifest) {
8781
+ const record = manifest;
8782
+ for (const field of _MdzPackagerCore.OPTIONAL_MANIFEST_FIELDS) {
8783
+ if (record[field] == null)
8784
+ delete record[field];
8785
+ }
8786
+ return manifest;
8374
8787
  }
8375
8788
  /**
8376
- * Resolves entry point using manifest override, `index.md`, or single-root-markdown fallback.
8789
+ * Resolves the entry point per spec 1.2.0 §5.5 (see `MdzArchiveCore.resolveEntryPointStrict`),
8790
+ * returning null instead of throwing when there is none — including when the manifest's
8791
+ * `entryPoint` is invalid or missing, which never falls back to another file.
8377
8792
  *
8378
8793
  * @param archivePaths - Archive file paths.
8379
8794
  * @param manifest - Optional manifest with `entryPoint`.
8380
8795
  */
8381
8796
  static resolveEntryPoint(archivePaths, manifest) {
8382
- if (manifest?.entryPoint && archivePaths.some((p) => p.toLowerCase() === manifest.entryPoint.toLowerCase())) {
8383
- return manifest.entryPoint;
8797
+ try {
8798
+ return MdzArchiveCore.resolveEntryPointStrict(archivePaths, manifest);
8799
+ } catch {
8800
+ return null;
8384
8801
  }
8385
- if (archivePaths.some((p) => p.toLowerCase() === "index.md"))
8386
- return "index.md";
8387
- const rootMarkdown = archivePaths.filter((p) => !p.includes("/") && MdzArchiveCore.isMarkdownFile(p));
8388
- return rootMarkdown.length === 1 ? rootMarkdown[0] ?? null : null;
8389
8802
  }
8390
8803
  /**
8391
8804
  * Generates a simple index markdown page for archives without a clear entry point.
@@ -8417,7 +8830,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
8417
8830
  static createManifest(metadata) {
8418
8831
  const now = (/* @__PURE__ */ new Date()).toISOString();
8419
8832
  const author = typeof metadata?.author === "string" ? { name: metadata.author } : metadata?.author ?? void 0;
8420
- return {
8833
+ return _MdzPackagerCore.pruneManifest({
8421
8834
  spec: {
8422
8835
  name: "mdzip-spec",
8423
8836
  version: PRODUCER_SPEC_VERSION
@@ -8431,18 +8844,18 @@ var MdzPackagerCore = class _MdzPackagerCore {
8431
8844
  },
8432
8845
  title: metadata?.title ?? void 0,
8433
8846
  mode: metadata?.mode ?? void 0,
8434
- entryPoint: metadata?.entryPoint ?? null,
8435
- language: metadata?.language ?? "en",
8847
+ entryPoint: metadata?.entryPoint ?? void 0,
8848
+ language: metadata?.language ?? void 0,
8436
8849
  author,
8437
- authors: author ? [author] : null,
8438
- description: metadata?.description ?? null,
8439
- version: metadata?.version ?? null,
8850
+ authors: author ? [author] : void 0,
8851
+ description: metadata?.description ?? void 0,
8852
+ version: metadata?.version ?? void 0,
8440
8853
  license: metadata?.license ?? void 0,
8441
8854
  keywords: metadata?.keywords ?? void 0,
8442
8855
  cover: metadata?.cover ?? void 0,
8443
8856
  created: now,
8444
8857
  modified: now
8445
- };
8858
+ });
8446
8859
  }
8447
8860
  /**
8448
8861
  * Updates a manifest while preserving spec-managed fields unless explicitly changed elsewhere.
@@ -8460,27 +8873,27 @@ var MdzPackagerCore = class _MdzPackagerCore {
8460
8873
  if ("author" in updates) {
8461
8874
  const author = typeof updates.author === "string" ? { name: updates.author } : updates.author ?? void 0;
8462
8875
  next.author = author;
8463
- next.authors = author ? [author] : null;
8876
+ next.authors = author ? [author] : void 0;
8464
8877
  }
8465
8878
  if ("description" in updates)
8466
- next.description = updates.description ?? null;
8879
+ next.description = updates.description ?? void 0;
8467
8880
  if ("keywords" in updates)
8468
8881
  next.keywords = updates.keywords ?? void 0;
8469
8882
  if ("language" in updates)
8470
- next.language = updates.language ?? null;
8883
+ next.language = updates.language ?? void 0;
8471
8884
  if ("license" in updates)
8472
8885
  next.license = updates.license ?? void 0;
8473
8886
  if ("version" in updates)
8474
- next.version = updates.version ?? null;
8887
+ next.version = updates.version ?? void 0;
8475
8888
  if ("cover" in updates)
8476
8889
  next.cover = updates.cover ?? void 0;
8477
8890
  if ("mode" in updates)
8478
8891
  next.mode = updates.mode ?? void 0;
8479
8892
  if ("entryPoint" in updates)
8480
- next.entryPoint = updates.entryPoint ?? null;
8893
+ next.entryPoint = updates.entryPoint ?? void 0;
8481
8894
  }
8482
8895
  _MdzPackagerCore.ensureCanonicalManifest(next, options);
8483
- return next;
8896
+ return _MdzPackagerCore.pruneManifest(next);
8484
8897
  }
8485
8898
  /**
8486
8899
  * Splits a manifest into spec-managed fields and ordinary editable metadata.
@@ -8596,14 +9009,15 @@ var MdzPackagerCore = class _MdzPackagerCore {
8596
9009
  static normalizeLf(content) {
8597
9010
  return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8598
9011
  }
9012
+ /** Files whose line endings a producer normalizes to LF when it writes them (§12.2). */
8599
9013
  static isTextFile(path) {
8600
- return /\.(md|markdown|json|txt|css|html|htm|xml|svg|yaml|yml|toml)$/i.test(path);
9014
+ return isMarkdownPath(path) || path === "manifest.json";
8601
9015
  }
8602
9016
  static isImagePath(path) {
8603
9017
  return /\.(png|jpg|jpeg|gif|webp|bmp|ico|webm|mp4|mp3|wav|ogg)$/i.test(path);
8604
9018
  }
8605
9019
  static async readProvidedManifest(selected) {
8606
- const manifestItem = selected.find((item) => item.archivePath.toLowerCase() === "manifest.json");
9020
+ const manifestItem = selected.find((item) => item.archivePath === "manifest.json");
8607
9021
  if (!manifestItem)
8608
9022
  return null;
8609
9023
  const raw = manifestItem.file ? await manifestItem.file.text() : manifestItem.text ?? "";
@@ -8685,12 +9099,12 @@ var MdzPackagerCore = class _MdzPackagerCore {
8685
9099
  };
8686
9100
  for (const item of cleanInput) {
8687
9101
  const originalPath = item.path;
8688
- let archivePath = originalPath;
9102
+ let archivePath = nfc(originalPath);
8689
9103
  if (!_MdzPackagerCore.matchesAnyFilter(originalPath, options.filters)) {
8690
9104
  addSkip("excluded by filter");
8691
9105
  continue;
8692
9106
  }
8693
- if (manifest && archivePath.toLowerCase() === "manifest.json") {
9107
+ if (manifest && archivePath === "manifest.json") {
8694
9108
  addSkip("manifest.json replaced by generated manifest");
8695
9109
  continue;
8696
9110
  }
@@ -8719,14 +9133,24 @@ var MdzPackagerCore = class _MdzPackagerCore {
8719
9133
  manifestFiles.push({ path: archivePath, originalPath, title: base.replace(/[_-]+/g, " ").trim() || originalPath });
8720
9134
  }
8721
9135
  }
9136
+ let writeManifest = !!manifest;
8722
9137
  if (!manifest) {
8723
9138
  manifest = await _MdzPackagerCore.readProvidedManifest(selected);
9139
+ if (manifest && !manifest.spec?.version) {
9140
+ manifest.spec = { ...manifest.spec ?? {}, version: PRODUCER_SPEC_VERSION };
9141
+ writeManifest = true;
9142
+ }
9143
+ }
9144
+ const modeWasSet = !!manifest?.mode;
9145
+ if (!manifest) {
9146
+ manifest = _MdzPackagerCore.createManifest({ title: options.title || rootName });
9147
+ writeManifest = true;
8724
9148
  }
8725
9149
  let archivePaths = selected.map((f) => f.archivePath);
8726
9150
  let resolvedEntryPoint = _MdzPackagerCore.resolveEntryPoint(archivePaths, manifest);
8727
9151
  const warningMessages = [];
8728
9152
  const markdownPaths = archivePaths.filter(MdzArchiveCore.isMarkdownFile);
8729
- if (!manifest?.mode && markdownPaths.length > 1) {
9153
+ if (!modeWasSet && markdownPaths.length > 1) {
8730
9154
  warningMessages.push('Archive contains multiple Markdown files and no explicit manifest.mode; consumers will default to document mode. If these files are intended as separate documents, set mode: "project".');
8731
9155
  }
8732
9156
  if (options.createIndex && !resolvedEntryPoint) {
@@ -8741,10 +9165,19 @@ var MdzPackagerCore = class _MdzPackagerCore {
8741
9165
  if (manifest)
8742
9166
  manifest.entryPoint = "index.md";
8743
9167
  }
8744
- if (manifest?.entryPoint && !archivePaths.some((p) => p.toLowerCase() === manifest.entryPoint.toLowerCase())) {
8745
- throw new Error(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${manifest.entryPoint}" does not exist in archive.`);
9168
+ const declaredEntryPoint = manifest.entryPoint;
9169
+ if (declaredEntryPoint && !archivePaths.some((p) => samePath(p, declaredEntryPoint.replace(/^\.\//, "")))) {
9170
+ throw new Error(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${declaredEntryPoint}" does not exist in archive.`);
9171
+ }
9172
+ if (!declaredEntryPoint && resolvedEntryPoint) {
9173
+ manifest.entryPoint = resolvedEntryPoint;
9174
+ writeManifest = true;
9175
+ }
9176
+ if (!manifest.mode) {
9177
+ manifest.mode = "document";
9178
+ writeManifest = true;
8746
9179
  }
8747
- if (options.mapFiles && manifest) {
9180
+ if (options.mapFiles) {
8748
9181
  manifest.files = manifestFiles;
8749
9182
  }
8750
9183
  const factory = zipWriterFactory ?? _MdzPackagerCore.getDefaultZipWriterFactory();
@@ -8777,7 +9210,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
8777
9210
  }
8778
9211
  }
8779
9212
  }
8780
- if (manifest) {
9213
+ if (writeManifest) {
8781
9214
  zip.file("manifest.json", JSON.stringify(manifest, null, 2));
8782
9215
  }
8783
9216
  const blob2 = await zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 1 } }, options.onProgress ? (metadata) => {