@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.
- package/README.md +256 -250
- package/dist/README.md +256 -250
- package/dist/fesm2022/mdzip-editor-ng.mjs +46 -6
- package/dist/fesm2022/mdzip-editor-ng.mjs.map +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/mdz-archive.worker.js +738 -305
- package/package.json +54 -54
|
@@ -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 =
|
|
6728
|
-
var CORE_LIBRARY_VERSION = "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6887
|
-
|
|
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
|
-
|
|
6890
|
-
|
|
7279
|
+
if (options?.dropSpecialFiles) {
|
|
7280
|
+
for (const stored of archive.listSpecialFiles())
|
|
7281
|
+
normalizedRemovals.set(nfc(stored), stored);
|
|
6891
7282
|
}
|
|
6892
|
-
const
|
|
6893
|
-
|
|
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
|
-
|
|
6908
|
-
|
|
6909
|
-
if (
|
|
6910
|
-
manifestObject = _MdzArchiveCore.stampManifestObject(
|
|
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
|
|
7322
|
+
...existingPaths.filter((p) => !normalizedRemovals.has(nfc(p)) && !normalizedWrites.has(nfc(p))),
|
|
6922
7323
|
...[...normalizedWrites.values()].map((w) => w.path)
|
|
6923
7324
|
];
|
|
6924
|
-
|
|
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.
|
|
7339
|
+
_MdzArchiveCore.removeEntrySamePath(zip, path);
|
|
6935
7340
|
}
|
|
6936
7341
|
for (const write of normalizedWrites.values()) {
|
|
6937
|
-
_MdzArchiveCore.
|
|
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
|
|
6974
|
-
if (removals.has(
|
|
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
|
|
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
|
|
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
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
return
|
|
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
|
|
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)
|
|
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)?.[
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
7609
|
-
const orphanedAssetPaths = assetPaths.filter((p) => !referencedSet.has(p
|
|
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
|
|
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
|
|
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
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
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
|
-
|
|
7798
|
-
|
|
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
|
|
8214
|
+
* Parses and checks `manifest.json` if present (§6.4).
|
|
7805
8215
|
*
|
|
7806
|
-
*
|
|
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.
|
|
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
|
|
7818
|
-
|
|
7819
|
-
|
|
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
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
const
|
|
7831
|
-
if (
|
|
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
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
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
|
-
|
|
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 (
|
|
7881
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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 ||
|
|
8366
|
-
language: options.language ||
|
|
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 }] :
|
|
8369
|
-
description: options.description ||
|
|
8370
|
-
version: options.docVersion ||
|
|
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
|
|
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
|
-
|
|
8383
|
-
return manifest
|
|
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 ??
|
|
8435
|
-
language: metadata?.language ??
|
|
8847
|
+
entryPoint: metadata?.entryPoint ?? void 0,
|
|
8848
|
+
language: metadata?.language ?? void 0,
|
|
8436
8849
|
author,
|
|
8437
|
-
authors: author ? [author] :
|
|
8438
|
-
description: metadata?.description ??
|
|
8439
|
-
version: metadata?.version ??
|
|
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] :
|
|
8876
|
+
next.authors = author ? [author] : void 0;
|
|
8464
8877
|
}
|
|
8465
8878
|
if ("description" in updates)
|
|
8466
|
-
next.description = updates.description ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
8745
|
-
|
|
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
|
|
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 (
|
|
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) => {
|