@mdzip/editor 2.2.0 → 2.3.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 +35 -3
- package/dist/asset-cache.d.ts +15 -0
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +57 -0
- package/dist/asset-cache.js.map +1 -1
- package/dist/diff-view-css.d.ts.map +1 -1
- package/dist/diff-view-css.js +43 -39
- package/dist/diff-view-css.js.map +1 -1
- package/dist/library-info.d.ts +2 -2
- package/dist/library-info.js +2 -2
- package/dist/mdz-archive.worker.js +111 -44
- package/dist/svg-inline.d.ts +11 -0
- package/dist/svg-inline.d.ts.map +1 -0
- package/dist/svg-inline.js +105 -0
- package/dist/svg-inline.js.map +1 -0
- package/dist/view.d.ts +72 -3
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +200 -12
- package/dist/view.js.map +1 -1
- package/package.json +2 -2
|
@@ -6728,8 +6728,21 @@ var MDZ_SPEC_VERSION = "1.2.0";
|
|
|
6728
6728
|
var MDZ_SUPPORTED_MAJOR = 1;
|
|
6729
6729
|
var MDZ_MODES = ["document", "project"];
|
|
6730
6730
|
var MDZ_SUPPORT_FILE_NAMES = ["readme", "agents", "license", "changelog", "contributing"];
|
|
6731
|
-
|
|
6732
|
-
|
|
6731
|
+
var MdzError = class _MdzError extends Error {
|
|
6732
|
+
code;
|
|
6733
|
+
constructor(code, message, options) {
|
|
6734
|
+
super(`${code}: ${message}`, options);
|
|
6735
|
+
this.name = "MdzError";
|
|
6736
|
+
this.code = code;
|
|
6737
|
+
}
|
|
6738
|
+
/** An `MdzError` from a `"CODE: message"` string. */
|
|
6739
|
+
static fromMessage(text, options) {
|
|
6740
|
+
const match = /^(ERR_[A-Z0-9_]+):\s*([\s\S]*)$/.exec(text);
|
|
6741
|
+
return match ? new _MdzError(match[1], match[2], options) : new _MdzError("ERR_UNKNOWN", text, options);
|
|
6742
|
+
}
|
|
6743
|
+
};
|
|
6744
|
+
function mdzError(code, message, options) {
|
|
6745
|
+
return new MdzError(code, message, options);
|
|
6733
6746
|
}
|
|
6734
6747
|
var CONTROL_RE = /[\u0000-\u001f\u007f]/;
|
|
6735
6748
|
var RESERVED_RE = /[\\:*?"<>|]/;
|
|
@@ -7071,7 +7084,7 @@ function insideArchive(archive, segments, host, withFragment, directory) {
|
|
|
7071
7084
|
|
|
7072
7085
|
// ../../node_modules/@mdzip/core-js/dist/mdz-core.js
|
|
7073
7086
|
var PRODUCER_SPEC_VERSION = MDZ_SPEC_VERSION;
|
|
7074
|
-
var CORE_LIBRARY_VERSION = "2.
|
|
7087
|
+
var CORE_LIBRARY_VERSION = "2.1.1";
|
|
7075
7088
|
var CORE_LIBRARY_URL = "https://github.com/mdzip-project/mdzip-core-js";
|
|
7076
7089
|
function isSymlinkEntry(entry) {
|
|
7077
7090
|
const mode = entry?.unixPermissions;
|
|
@@ -7150,10 +7163,9 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7150
7163
|
try {
|
|
7151
7164
|
return await load4();
|
|
7152
7165
|
} catch (error2) {
|
|
7153
|
-
if (/^ERR_[A-Z_]+:/.test(error2 instanceof Error ? error2.message : ""))
|
|
7166
|
+
if (error2 instanceof MdzError || /^ERR_[A-Z_]+:/.test(error2 instanceof Error ? error2.message : ""))
|
|
7154
7167
|
throw error2;
|
|
7155
|
-
|
|
7156
|
-
throw mdzError("ERR_ZIP_INVALID", `not a readable ZIP archive (${message})`);
|
|
7168
|
+
throw mdzError("ERR_ZIP_INVALID", "The file is not a valid ZIP archive.", { cause: error2 });
|
|
7157
7169
|
}
|
|
7158
7170
|
}
|
|
7159
7171
|
/** Rejects NFC-duplicate entries and files that are also directories (§5.4, §13.1): never picks one. */
|
|
@@ -7190,10 +7202,11 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7190
7202
|
*
|
|
7191
7203
|
* @param input - Raw archive bytes.
|
|
7192
7204
|
* @param zipFactory - Optional custom zip loader.
|
|
7205
|
+
* @param options - Validation controls (see {@link MdzValidateOptions}).
|
|
7193
7206
|
*/
|
|
7194
|
-
static async validate(input, zipFactory) {
|
|
7207
|
+
static async validate(input, zipFactory, options) {
|
|
7195
7208
|
const archive = await _MdzArchiveCore.open(input, zipFactory);
|
|
7196
|
-
return archive.validate();
|
|
7209
|
+
return archive.validate(options);
|
|
7197
7210
|
}
|
|
7198
7211
|
/**
|
|
7199
7212
|
* Adds or replaces an archive entry and returns a newly generated archive blob.
|
|
@@ -7255,24 +7268,24 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7255
7268
|
const path = nfc(MdzPackagerCore.normalizePath(write.path));
|
|
7256
7269
|
const pathError = _MdzArchiveCore.validateArchivePath(path);
|
|
7257
7270
|
if (pathError) {
|
|
7258
|
-
throw
|
|
7271
|
+
throw MdzError.fromMessage(`ERR_PATH_INVALID: ${pathError}`);
|
|
7259
7272
|
}
|
|
7260
7273
|
normalizedWrites.set(path, { path, content: write.content });
|
|
7261
7274
|
}
|
|
7262
7275
|
const sourceBytes = await _MdzArchiveCore.readArchiveInputBytes(input);
|
|
7263
7276
|
const archive = await _MdzArchiveCore.open(sourceBytes);
|
|
7264
7277
|
const existingPaths = archive.listPaths();
|
|
7265
|
-
const removable = [...
|
|
7278
|
+
const removable = [...archive.listPaths({ includeDirectories: true }), ...archive.listSpecialFiles()];
|
|
7266
7279
|
const normalizedRemovals = /* @__PURE__ */ new Map();
|
|
7267
7280
|
for (const removal of removals) {
|
|
7268
7281
|
const path = MdzPackagerCore.normalizePath(removal);
|
|
7269
7282
|
const pathError = _MdzArchiveCore.validateArchivePath(path);
|
|
7270
7283
|
if (pathError) {
|
|
7271
|
-
throw
|
|
7284
|
+
throw MdzError.fromMessage(`ERR_PATH_INVALID: ${pathError}`);
|
|
7272
7285
|
}
|
|
7273
7286
|
const stored = removable.find((p) => samePath(p, path));
|
|
7274
7287
|
if (stored === void 0) {
|
|
7275
|
-
throw
|
|
7288
|
+
throw MdzError.fromMessage(`ERR_NOT_FOUND: Entry "${path}" was not found in archive.`);
|
|
7276
7289
|
}
|
|
7277
7290
|
normalizedRemovals.set(nfc(stored), stored);
|
|
7278
7291
|
}
|
|
@@ -7325,7 +7338,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7325
7338
|
const nextManifest = manifestObject ?? (normalizedRemovals.has("manifest.json") ? null : existingManifest);
|
|
7326
7339
|
const entryPointAfter = _MdzArchiveCore.resolveEntryPointStrict(nextPaths, nextManifest);
|
|
7327
7340
|
if (entryPointBefore && !explicitManifest && !samePath(entryPointBefore, entryPointAfter)) {
|
|
7328
|
-
throw
|
|
7341
|
+
throw MdzError.fromMessage(`ERR_ENTRYPOINT_CHANGED: this change would move the entry point from "${entryPointBefore}" to "${entryPointAfter}"; set manifest entryPoint to change it.`);
|
|
7329
7342
|
}
|
|
7330
7343
|
let patchedBytes;
|
|
7331
7344
|
try {
|
|
@@ -7380,13 +7393,13 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7380
7393
|
continue;
|
|
7381
7394
|
}
|
|
7382
7395
|
if (entry.localOffset + 30 > source.length || view.getUint32(entry.localOffset, true) !== 67324752) {
|
|
7383
|
-
throw
|
|
7396
|
+
throw MdzError.fromMessage(`ERR_ZIP_UNSUPPORTED: malformed local header for "${entry.name}"`);
|
|
7384
7397
|
}
|
|
7385
7398
|
const localNameLen = view.getUint16(entry.localOffset + 26, true);
|
|
7386
7399
|
const localExtraLen = view.getUint16(entry.localOffset + 28, true);
|
|
7387
7400
|
const dataStart = entry.localOffset + 30 + localNameLen + localExtraLen;
|
|
7388
7401
|
if (dataStart + entry.compressedSize > source.length) {
|
|
7389
|
-
throw
|
|
7402
|
+
throw MdzError.fromMessage(`ERR_ZIP_UNSUPPORTED: entry data out of bounds for "${entry.name}"`);
|
|
7390
7403
|
}
|
|
7391
7404
|
output.push({
|
|
7392
7405
|
nameBytes: entry.nameBytes,
|
|
@@ -7512,27 +7525,27 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7512
7525
|
}
|
|
7513
7526
|
}
|
|
7514
7527
|
if (eocd < 0) {
|
|
7515
|
-
throw
|
|
7528
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: end of central directory not found");
|
|
7516
7529
|
}
|
|
7517
7530
|
if (view.getUint16(eocd + 4, true) !== 0 || view.getUint16(eocd + 6, true) !== 0) {
|
|
7518
|
-
throw
|
|
7531
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: multi-disk archives are not supported");
|
|
7519
7532
|
}
|
|
7520
7533
|
const totalEntries = view.getUint16(eocd + 10, true);
|
|
7521
7534
|
const centralSize = view.getUint32(eocd + 12, true);
|
|
7522
7535
|
const centralOffset = view.getUint32(eocd + 16, true);
|
|
7523
7536
|
if (totalEntries === 65535 || centralSize === 4294967295 || centralOffset === 4294967295) {
|
|
7524
|
-
throw
|
|
7537
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: ZIP64 archives are not supported");
|
|
7525
7538
|
}
|
|
7526
7539
|
const entries = [];
|
|
7527
7540
|
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
7528
7541
|
let pos = centralOffset;
|
|
7529
7542
|
for (let i = 0; i < totalEntries; i++) {
|
|
7530
7543
|
if (pos + 46 > source.length || view.getUint32(pos, true) !== 33639248) {
|
|
7531
|
-
throw
|
|
7544
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: malformed central directory");
|
|
7532
7545
|
}
|
|
7533
7546
|
const flags = view.getUint16(pos + 8, true);
|
|
7534
7547
|
if ((flags & 1) !== 0) {
|
|
7535
|
-
throw
|
|
7548
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: encrypted entries are not supported");
|
|
7536
7549
|
}
|
|
7537
7550
|
const compressedSize = view.getUint32(pos + 20, true);
|
|
7538
7551
|
const uncompressedSize = view.getUint32(pos + 24, true);
|
|
@@ -7541,11 +7554,11 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7541
7554
|
const commentLen = view.getUint16(pos + 32, true);
|
|
7542
7555
|
const localOffset = view.getUint32(pos + 42, true);
|
|
7543
7556
|
if (compressedSize === 4294967295 || uncompressedSize === 4294967295 || localOffset === 4294967295) {
|
|
7544
|
-
throw
|
|
7557
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: ZIP64 archives are not supported");
|
|
7545
7558
|
}
|
|
7546
7559
|
const nameBytes = source.slice(pos + 46, pos + 46 + nameLen);
|
|
7547
7560
|
if ((flags & 2048) === 0 && nameBytes.some((byte) => byte > 127)) {
|
|
7548
|
-
throw
|
|
7561
|
+
throw MdzError.fromMessage("ERR_ZIP_UNSUPPORTED: non-UTF-8 entry names are not supported");
|
|
7549
7562
|
}
|
|
7550
7563
|
entries.push({
|
|
7551
7564
|
name: _MdzArchiveCore.normalizePath(decoder.decode(nameBytes)),
|
|
@@ -7712,11 +7725,11 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7712
7725
|
if (manifest && "entryPoint" in manifest && manifest.entryPoint !== void 0) {
|
|
7713
7726
|
const problem = _MdzArchiveCore.entryPointError(manifest.entryPoint);
|
|
7714
7727
|
if (problem)
|
|
7715
|
-
throw
|
|
7728
|
+
throw MdzError.fromMessage(`ERR_ENTRYPOINT_INVALID: ${problem}`);
|
|
7716
7729
|
const value = manifest.entryPoint;
|
|
7717
7730
|
const found = find(value.startsWith("./") ? value.slice(2) : value);
|
|
7718
7731
|
if (!found)
|
|
7719
|
-
throw
|
|
7732
|
+
throw MdzError.fromMessage(`ERR_ENTRYPOINT_MISSING: manifest.json references "${value}" which is not in the archive`);
|
|
7720
7733
|
return found;
|
|
7721
7734
|
}
|
|
7722
7735
|
const index = find("index.md");
|
|
@@ -7729,8 +7742,8 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
7729
7742
|
if (content.length === 0 && rootMarkdown.length === 1)
|
|
7730
7743
|
return rootMarkdown[0];
|
|
7731
7744
|
if (rootMarkdown.length === 0)
|
|
7732
|
-
throw
|
|
7733
|
-
throw
|
|
7745
|
+
throw MdzError.fromMessage("ERR_ENTRYPOINT_UNRESOLVED: No Markdown file found at the archive root.");
|
|
7746
|
+
throw MdzError.fromMessage("ERR_ENTRYPOINT_UNRESOLVED: Multiple Markdown files at the archive root and no manifest.json entryPoint. Add an index.md or manifest.json entryPoint.");
|
|
7734
7747
|
}
|
|
7735
7748
|
/**
|
|
7736
7749
|
* Infers a MIME type from an archive path.
|
|
@@ -8134,7 +8147,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8134
8147
|
* @param options - Workspace open controls.
|
|
8135
8148
|
*/
|
|
8136
8149
|
async openWorkspace(options) {
|
|
8137
|
-
const validation = await this.validate();
|
|
8150
|
+
const validation = await this.validate({ checkImages: false });
|
|
8138
8151
|
const manifest = await this.readManifest().catch(() => null);
|
|
8139
8152
|
const mode = await this.resolveMode().catch(() => "document");
|
|
8140
8153
|
const entryPoint = await this.resolveEntryPoint().catch(() => null);
|
|
@@ -8195,7 +8208,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8195
8208
|
*/
|
|
8196
8209
|
static validateManifest(manifest) {
|
|
8197
8210
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
8198
|
-
throw
|
|
8211
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: manifest.json must be a JSON object.");
|
|
8199
8212
|
}
|
|
8200
8213
|
const ignored = checkManifest(manifest);
|
|
8201
8214
|
_MdzArchiveCore.checkLegacyMdzField(manifest);
|
|
@@ -8260,7 +8273,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8260
8273
|
* skipped (§4, §13.1); ignored descriptive manifest fields and duplicate members (§6.4); a
|
|
8261
8274
|
* missing `cover`; and missing version metadata.
|
|
8262
8275
|
*/
|
|
8263
|
-
async validate() {
|
|
8276
|
+
async validate(options) {
|
|
8264
8277
|
const errors = [];
|
|
8265
8278
|
const warnings = [];
|
|
8266
8279
|
for (const path of _MdzArchiveCore.getArchivePaths(this.zip)) {
|
|
@@ -8313,12 +8326,49 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8313
8326
|
errors.push(message);
|
|
8314
8327
|
}
|
|
8315
8328
|
}
|
|
8329
|
+
if (options?.checkImages !== false)
|
|
8330
|
+
warnings.push(...await this.missingImageWarnings());
|
|
8316
8331
|
return {
|
|
8317
8332
|
isValid: errors.length === 0,
|
|
8318
8333
|
errors,
|
|
8319
8334
|
warnings
|
|
8320
8335
|
};
|
|
8321
8336
|
}
|
|
8337
|
+
/**
|
|
8338
|
+
* Images a Markdown document uses (`` and `<img src>`) that aren't in the archive. A
|
|
8339
|
+
* warning, not an error: the spec doesn't make a broken image non-conforming, but it's the
|
|
8340
|
+
* most common producer mistake. Web and `data:` images, and paths that leave the archive, are
|
|
8341
|
+
* not checked here.
|
|
8342
|
+
*/
|
|
8343
|
+
async missingImageWarnings() {
|
|
8344
|
+
const warnings = [];
|
|
8345
|
+
for (const document of this.listPaths().filter((p) => _MdzArchiveCore.isMarkdownFile(p))) {
|
|
8346
|
+
let markdown;
|
|
8347
|
+
try {
|
|
8348
|
+
markdown = await this.readText(document);
|
|
8349
|
+
} catch {
|
|
8350
|
+
continue;
|
|
8351
|
+
}
|
|
8352
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8353
|
+
for (const ref of _MdzArchiveCore.extractImageReferences(markdown)) {
|
|
8354
|
+
if (seen.has(ref))
|
|
8355
|
+
continue;
|
|
8356
|
+
seen.add(ref);
|
|
8357
|
+
const target = this.resolveLink(document, ref, void 0, { use: "embed" });
|
|
8358
|
+
if (target.kind !== "unresolved" || target.reason !== "not-found")
|
|
8359
|
+
continue;
|
|
8360
|
+
let message = `${document} uses image "${ref}", which is not in the archive.`;
|
|
8361
|
+
try {
|
|
8362
|
+
const nearMatch = this.findCaseOnlyMatch(_MdzArchiveCore.resolvePath(document, ref));
|
|
8363
|
+
if (nearMatch)
|
|
8364
|
+
message += ` (Did you mean "${nearMatch}"? Paths are case-sensitive.)`;
|
|
8365
|
+
} catch {
|
|
8366
|
+
}
|
|
8367
|
+
warnings.push(message);
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
return warnings;
|
|
8371
|
+
}
|
|
8322
8372
|
/**
|
|
8323
8373
|
* Resolves the primary markdown entry point for rendering.
|
|
8324
8374
|
*
|
|
@@ -8359,10 +8409,10 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8359
8409
|
const normalized = _MdzArchiveCore.normalizePath(path);
|
|
8360
8410
|
const entry = this.findEntryWithDirectoryFallback(normalized);
|
|
8361
8411
|
if (!entry) {
|
|
8362
|
-
throw
|
|
8412
|
+
throw MdzError.fromMessage(`ERR_NOT_FOUND: Entry "${normalized}" was not found in archive.`);
|
|
8363
8413
|
}
|
|
8364
8414
|
if (entry.dir) {
|
|
8365
|
-
throw
|
|
8415
|
+
throw MdzError.fromMessage(`ERR_IS_DIRECTORY: Entry "${normalized}" is a directory.`);
|
|
8366
8416
|
}
|
|
8367
8417
|
return entry;
|
|
8368
8418
|
}
|
|
@@ -8463,7 +8513,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8463
8513
|
const parsed = JSON.parse(String(raw));
|
|
8464
8514
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8465
8515
|
if (requireValid)
|
|
8466
|
-
throw
|
|
8516
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: Existing manifest.json is invalid: expected a JSON object.");
|
|
8467
8517
|
return null;
|
|
8468
8518
|
}
|
|
8469
8519
|
return parsed;
|
|
@@ -8471,7 +8521,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8471
8521
|
if (requireValid) {
|
|
8472
8522
|
if (error2 instanceof Error && /expected a JSON object/.test(error2.message))
|
|
8473
8523
|
throw error2;
|
|
8474
|
-
throw
|
|
8524
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: Existing manifest.json is invalid JSON.");
|
|
8475
8525
|
}
|
|
8476
8526
|
return null;
|
|
8477
8527
|
}
|
|
@@ -8482,7 +8532,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8482
8532
|
const parsed = JSON.parse(raw);
|
|
8483
8533
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8484
8534
|
if (requireValid)
|
|
8485
|
-
throw
|
|
8535
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: Replacement manifest.json is invalid: expected a JSON object.");
|
|
8486
8536
|
return {};
|
|
8487
8537
|
}
|
|
8488
8538
|
return parsed;
|
|
@@ -8490,7 +8540,7 @@ var MdzArchiveCore = class _MdzArchiveCore {
|
|
|
8490
8540
|
if (requireValid) {
|
|
8491
8541
|
if (error2 instanceof Error && /expected a JSON object/.test(error2.message))
|
|
8492
8542
|
throw error2;
|
|
8493
|
-
throw
|
|
8543
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: Replacement manifest.json is invalid JSON.");
|
|
8494
8544
|
}
|
|
8495
8545
|
return {};
|
|
8496
8546
|
}
|
|
@@ -8730,17 +8780,31 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
8730
8780
|
* @param rootName - Root/source label for fallback title.
|
|
8731
8781
|
* @param options - Packaging options.
|
|
8732
8782
|
*/
|
|
8783
|
+
/** The `producer.application` to write: the given fields that are non-empty strings, or null if none are. */
|
|
8784
|
+
static producerApplication(application) {
|
|
8785
|
+
if (!application)
|
|
8786
|
+
return null;
|
|
8787
|
+
const node = {};
|
|
8788
|
+
for (const key of ["name", "version", "url"]) {
|
|
8789
|
+
const value = application[key];
|
|
8790
|
+
if (typeof value === "string" && value.trim())
|
|
8791
|
+
node[key] = value.trim();
|
|
8792
|
+
}
|
|
8793
|
+
return Object.keys(node).length ? node : null;
|
|
8794
|
+
}
|
|
8733
8795
|
static buildManifestFromOptions(rootName, options) {
|
|
8734
|
-
const hasManifestOption = options.mapFiles || !!options.title || !!options.mode || !!options.entryPoint || !!options.language || !!options.author || !!options.description || !!options.docVersion;
|
|
8796
|
+
const hasManifestOption = options.mapFiles || !!options.title || !!options.mode || !!options.entryPoint || !!options.language || !!options.author || !!options.description || !!options.docVersion || !!_MdzPackagerCore.producerApplication(options.application);
|
|
8735
8797
|
if (!hasManifestOption)
|
|
8736
8798
|
return null;
|
|
8737
8799
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8800
|
+
const application = _MdzPackagerCore.producerApplication(options.application);
|
|
8738
8801
|
return _MdzPackagerCore.pruneManifest({
|
|
8739
8802
|
spec: {
|
|
8740
8803
|
name: "mdzip-spec",
|
|
8741
8804
|
version: PRODUCER_SPEC_VERSION
|
|
8742
8805
|
},
|
|
8743
8806
|
producer: {
|
|
8807
|
+
...application ? { application } : {},
|
|
8744
8808
|
core: {
|
|
8745
8809
|
name: "mdzip-core-js",
|
|
8746
8810
|
version: CORE_LIBRARY_VERSION,
|
|
@@ -8935,7 +8999,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
8935
8999
|
const path = _MdzPackagerCore.normalizePath(targetPath || (isFile ? source.name : "asset.bin"));
|
|
8936
9000
|
const pathError = _MdzPackagerCore.validateArchivePath(path);
|
|
8937
9001
|
if (pathError)
|
|
8938
|
-
throw
|
|
9002
|
+
throw MdzError.fromMessage(`ERR_PATH_INVALID: ${pathError}`);
|
|
8939
9003
|
const bytes = await _MdzPackagerCore.readBinarySource(source);
|
|
8940
9004
|
const mimeType = isBlob && source.type ? source.type : MdzArchiveCore.inferMimeType(path);
|
|
8941
9005
|
return {
|
|
@@ -8981,13 +9045,16 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
8981
9045
|
mode,
|
|
8982
9046
|
entryPoint
|
|
8983
9047
|
});
|
|
9048
|
+
const application = _MdzPackagerCore.producerApplication(options?.application);
|
|
9049
|
+
if (application)
|
|
9050
|
+
manifest.producer = { ...manifest.producer ?? {}, application };
|
|
8984
9051
|
const files = [
|
|
8985
9052
|
...await Promise.all(workspace.documents.map(async (document) => {
|
|
8986
9053
|
let text = document.text;
|
|
8987
9054
|
if (!text && document.readText) {
|
|
8988
9055
|
text = await document.readText();
|
|
8989
9056
|
} else if (!text && document.isLazy) {
|
|
8990
|
-
throw
|
|
9057
|
+
throw MdzError.fromMessage(`ERR_LAZY_TEXT_UNAVAILABLE: document "${document.path}" was opened lazily but has no readText() reader (likely dropped by serialization); building would write an empty file`);
|
|
8991
9058
|
}
|
|
8992
9059
|
return { path: document.path, text };
|
|
8993
9060
|
})),
|
|
@@ -9025,7 +9092,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
9025
9092
|
try {
|
|
9026
9093
|
manifest = JSON.parse(raw);
|
|
9027
9094
|
} catch {
|
|
9028
|
-
throw
|
|
9095
|
+
throw MdzError.fromMessage("ERR_MANIFEST_INVALID: manifest.json is not valid JSON");
|
|
9029
9096
|
}
|
|
9030
9097
|
MdzArchiveCore.validateManifest(manifest);
|
|
9031
9098
|
return manifest;
|
|
@@ -9065,14 +9132,14 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
9065
9132
|
return new Uint8Array(source);
|
|
9066
9133
|
if (typeof source.arrayBuffer === "function")
|
|
9067
9134
|
return new Uint8Array(await source.arrayBuffer());
|
|
9068
|
-
throw
|
|
9135
|
+
throw MdzError.fromMessage("ERR_ASSET_BYTES_INVALID: Asset source is not readable as bytes.");
|
|
9069
9136
|
}
|
|
9070
9137
|
static async readWorkspaceAssetBytes(asset) {
|
|
9071
9138
|
if (asset.bytes)
|
|
9072
9139
|
return _MdzPackagerCore.readBinarySource(asset.bytes);
|
|
9073
9140
|
if (asset.readBytes)
|
|
9074
9141
|
return asset.readBytes();
|
|
9075
|
-
throw
|
|
9142
|
+
throw MdzError.fromMessage(`ERR_ASSET_BYTES_MISSING: Asset "${asset.path}" has no bytes or readBytes() source.`);
|
|
9076
9143
|
}
|
|
9077
9144
|
/**
|
|
9078
9145
|
* Builds an `.mdz` archive from input files and packaging options.
|
|
@@ -9085,7 +9152,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
9085
9152
|
static async buildArchive(files, rootName, options, zipWriterFactory) {
|
|
9086
9153
|
const cleanInput = files.map((f) => ({ path: _MdzPackagerCore.normalizePath(f.path), file: f.file, data: f.data, text: f.text })).filter((f) => f.path && !f.path.endsWith("/"));
|
|
9087
9154
|
if (!cleanInput.length) {
|
|
9088
|
-
throw
|
|
9155
|
+
throw MdzError.fromMessage("ERR_PACK_NO_INPUT: No files found to package.");
|
|
9089
9156
|
}
|
|
9090
9157
|
let manifest = _MdzPackagerCore.buildManifestFromOptions(rootName, options);
|
|
9091
9158
|
const skipMap = /* @__PURE__ */ new Map();
|
|
@@ -9155,7 +9222,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
9155
9222
|
}
|
|
9156
9223
|
if (options.createIndex && !resolvedEntryPoint) {
|
|
9157
9224
|
if (manifest?.entryPoint) {
|
|
9158
|
-
throw
|
|
9225
|
+
throw MdzError.fromMessage(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${manifest.entryPoint}" does not exist.`);
|
|
9159
9226
|
}
|
|
9160
9227
|
const sortedMarkdownPaths = markdownPaths.slice().sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
9161
9228
|
const generated = _MdzPackagerCore.buildGeneratedIndex(sortedMarkdownPaths, options.title || rootName);
|
|
@@ -9167,7 +9234,7 @@ var MdzPackagerCore = class _MdzPackagerCore {
|
|
|
9167
9234
|
}
|
|
9168
9235
|
const declaredEntryPoint = manifest.entryPoint;
|
|
9169
9236
|
if (declaredEntryPoint && !archivePaths.some((p) => samePath(p, declaredEntryPoint.replace(/^\.\//, "")))) {
|
|
9170
|
-
throw
|
|
9237
|
+
throw MdzError.fromMessage(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${declaredEntryPoint}" does not exist in archive.`);
|
|
9171
9238
|
}
|
|
9172
9239
|
if (!declaredEntryPoint && resolvedEntryPoint) {
|
|
9173
9240
|
manifest.entryPoint = resolvedEntryPoint;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** True when SVG text contains a hyperlink, the only case the preview inlines. */
|
|
2
|
+
export declare function svgHasLinks(svgText: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The cleaned `<svg>` element, owned by the page's document and ready to
|
|
5
|
+
* append to a shadow root (clone it for each use), or null when the text
|
|
6
|
+
* isn't a usable SVG. Never serialize it and parse it again (see above).
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeInlineSvg(svgText: string): SVGSVGElement | null;
|
|
9
|
+
/** Styles inside each inline SVG's shadow root. */
|
|
10
|
+
export declare const INLINE_SVG_SHADOW_CSS: string;
|
|
11
|
+
//# sourceMappingURL=svg-inline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-inline.d.ts","sourceRoot":"","sources":["../src/svg-inline.ts"],"names":[],"mappings":"AA+EA,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAavE;AAED,mDAAmD;AACnD,eAAO,MAAM,qBAAqB,QACkC,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
// Inline SVG for the preview: an archive SVG that contains links is shown as
|
|
3
|
+
// its own markup (in a shadow root) instead of an <img>, because an SVG loaded
|
|
4
|
+
// as an image is a flat picture whose links can't be clicked. Everything here
|
|
5
|
+
// keeps that markup as safe as the <img> it replaces:
|
|
6
|
+
//
|
|
7
|
+
// - no scripts, event handlers or javascript: links (DOMPurify);
|
|
8
|
+
// - nothing loaded from outside the archive: on every element but <a>, an
|
|
9
|
+
// href may only point inside the SVG (#id) or be a data: image, and CSS
|
|
10
|
+
// url() and @import are removed — an <img> SVG can't fetch anything either,
|
|
11
|
+
// and spec §9.6 keeps embeds inside the archive by default;
|
|
12
|
+
// - links are plain `href` (xlink:href is rewritten), so the preview's link
|
|
13
|
+
// handling and external-link policy see them like any other link.
|
|
14
|
+
//
|
|
15
|
+
// Styles and ids stay inside the shadow root, so they can't restyle the page
|
|
16
|
+
// or collide with another SVG.
|
|
17
|
+
//
|
|
18
|
+
// draw.io draws its labels as HTML inside <foreignObject>, which DOMPurify 3.4
|
|
19
|
+
// removes by default to prevent mutation XSS (markup that changes meaning when
|
|
20
|
+
// serialized and parsed again). It's allowed here because the result is never
|
|
21
|
+
// re-parsed: the sanitizer returns DOM nodes, which go straight into the
|
|
22
|
+
// shadow root.
|
|
23
|
+
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
24
|
+
const SAFE_EMBED_HREF = /^(?:#|data:image\/)/i;
|
|
25
|
+
const CSS_URL = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
|
26
|
+
const CSS_IMPORT = /@import\b[^;]*;?/gi;
|
|
27
|
+
let purifier = null;
|
|
28
|
+
function svgPurifier() {
|
|
29
|
+
if (purifier)
|
|
30
|
+
return purifier;
|
|
31
|
+
const globalWindow = globalThis.window;
|
|
32
|
+
if (!globalWindow) {
|
|
33
|
+
throw new Error('Inline SVG needs a DOM window.');
|
|
34
|
+
}
|
|
35
|
+
// A separate instance: hooks registered here never touch the preview's main sanitizer.
|
|
36
|
+
purifier = DOMPurify(globalWindow);
|
|
37
|
+
purifier.addHook('afterSanitizeAttributes', (node) => {
|
|
38
|
+
const element = node;
|
|
39
|
+
if (typeof element.getAttribute !== 'function')
|
|
40
|
+
return;
|
|
41
|
+
const isLink = element.localName === 'a';
|
|
42
|
+
const xlinkHref = element.getAttributeNS(XLINK_NS, 'href') ?? element.getAttribute('xlink:href');
|
|
43
|
+
if (xlinkHref !== null) {
|
|
44
|
+
element.removeAttributeNS(XLINK_NS, 'href');
|
|
45
|
+
element.removeAttribute('xlink:href');
|
|
46
|
+
if (!element.hasAttribute('href'))
|
|
47
|
+
element.setAttribute('href', xlinkHref);
|
|
48
|
+
}
|
|
49
|
+
const href = element.getAttribute('href');
|
|
50
|
+
if (href !== null && !isLink && !SAFE_EMBED_HREF.test(href.trim())) {
|
|
51
|
+
element.removeAttribute('href');
|
|
52
|
+
}
|
|
53
|
+
// HTML inside <foreignObject> can load things too (<img src>, srcset, …).
|
|
54
|
+
for (const name of ['src', 'srcset', 'poster', 'background', 'data']) {
|
|
55
|
+
const value = element.getAttribute(name);
|
|
56
|
+
if (value !== null && !(name === 'src' && /^data:image\//i.test(value.trim()))) {
|
|
57
|
+
element.removeAttribute(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const style = element.getAttribute('style');
|
|
61
|
+
if (style !== null)
|
|
62
|
+
element.setAttribute('style', cleanCss(style));
|
|
63
|
+
// draw.io keeps the whole editable diagram in the root's `content`: not needed in the page.
|
|
64
|
+
if (element.localName === 'svg')
|
|
65
|
+
element.removeAttribute('content');
|
|
66
|
+
});
|
|
67
|
+
purifier.addHook('uponSanitizeElement', (node, data) => {
|
|
68
|
+
if (data.tagName === 'style' && node.textContent) {
|
|
69
|
+
node.textContent = cleanCss(node.textContent);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return purifier;
|
|
73
|
+
}
|
|
74
|
+
function cleanCss(css) {
|
|
75
|
+
return css
|
|
76
|
+
.replace(CSS_IMPORT, '')
|
|
77
|
+
.replace(CSS_URL, (match, _quote, url) => (SAFE_EMBED_HREF.test(url.trim()) ? match : 'none'));
|
|
78
|
+
}
|
|
79
|
+
/** True when SVG text contains a hyperlink, the only case the preview inlines. */
|
|
80
|
+
export function svgHasLinks(svgText) {
|
|
81
|
+
return /<a[\s>]/i.test(svgText);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The cleaned `<svg>` element, owned by the page's document and ready to
|
|
85
|
+
* append to a shadow root (clone it for each use), or null when the text
|
|
86
|
+
* isn't a usable SVG. Never serialize it and parse it again (see above).
|
|
87
|
+
*/
|
|
88
|
+
export function sanitizeInlineSvg(svgText) {
|
|
89
|
+
const fragment = svgPurifier().sanitize(svgText, {
|
|
90
|
+
USE_PROFILES: { svg: true, svgFilters: true, html: true },
|
|
91
|
+
ADD_TAGS: ['foreignObject', 'switch'],
|
|
92
|
+
ADD_ATTR: ['target', 'requiredFeatures', 'pointer-events'],
|
|
93
|
+
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'base', 'link', 'meta', 'audio', 'video', 'source', 'track'],
|
|
94
|
+
HTML_INTEGRATION_POINTS: { foreignobject: true },
|
|
95
|
+
RETURN_DOM_FRAGMENT: true
|
|
96
|
+
});
|
|
97
|
+
const root = Array.from(fragment.childNodes).find((node) => node.nodeType === 1);
|
|
98
|
+
return root && root.localName === 'svg' && root.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
99
|
+
? root
|
|
100
|
+
: null;
|
|
101
|
+
}
|
|
102
|
+
/** Styles inside each inline SVG's shadow root. */
|
|
103
|
+
export const INLINE_SVG_SHADOW_CSS = ':host{display:inline-block;max-width:100%;line-height:0;vertical-align:middle}'
|
|
104
|
+
+ 'svg{display:block;max-width:100%;height:auto}a{cursor:pointer}';
|
|
105
|
+
//# sourceMappingURL=svg-inline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-inline.js","sourceRoot":"","sources":["../src/svg-inline.ts"],"names":[],"mappings":"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"}
|