@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.
@@ -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
- function mdzError(code, message) {
6732
- return new Error(`${code}: ${message}`);
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.0.1";
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
- const message = error2 instanceof Error ? error2.message : String(error2);
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 new Error(`ERR_PATH_INVALID: ${pathError}`);
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 = [...existingPaths, ...archive.listSpecialFiles()];
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 new Error(`ERR_PATH_INVALID: ${pathError}`);
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 new Error(`ERR_NOT_FOUND: Entry "${path}" was not found in archive.`);
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 new Error(`ERR_ENTRYPOINT_CHANGED: this change would move the entry point from "${entryPointBefore}" to "${entryPointAfter}"; set manifest entryPoint to change it.`);
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 new Error(`ERR_ZIP_UNSUPPORTED: malformed local header for "${entry.name}"`);
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 new Error(`ERR_ZIP_UNSUPPORTED: entry data out of bounds for "${entry.name}"`);
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 new Error("ERR_ZIP_UNSUPPORTED: end of central directory not found");
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 new Error("ERR_ZIP_UNSUPPORTED: multi-disk archives are not supported");
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 new Error("ERR_ZIP_UNSUPPORTED: ZIP64 archives are not supported");
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 new Error("ERR_ZIP_UNSUPPORTED: malformed central directory");
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 new Error("ERR_ZIP_UNSUPPORTED: encrypted entries are not supported");
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 new Error("ERR_ZIP_UNSUPPORTED: ZIP64 archives are not supported");
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 new Error("ERR_ZIP_UNSUPPORTED: non-UTF-8 entry names are not supported");
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 new Error(`ERR_ENTRYPOINT_INVALID: ${problem}`);
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 new Error(`ERR_ENTRYPOINT_MISSING: manifest.json references "${value}" which is not in the archive`);
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 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.");
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 new Error("ERR_MANIFEST_INVALID: manifest.json must be a JSON object.");
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 new Error(`ERR_NOT_FOUND: Entry "${normalized}" was not found in archive.`);
8412
+ throw MdzError.fromMessage(`ERR_NOT_FOUND: Entry "${normalized}" was not found in archive.`);
8363
8413
  }
8364
8414
  if (entry.dir) {
8365
- throw new Error(`ERR_IS_DIRECTORY: Entry "${normalized}" is a directory.`);
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 new Error("ERR_MANIFEST_INVALID: Existing manifest.json is invalid: expected a JSON object.");
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 new Error("ERR_MANIFEST_INVALID: Existing manifest.json is invalid JSON.");
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 new Error("ERR_MANIFEST_INVALID: Replacement manifest.json is invalid: expected a JSON object.");
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 new Error("ERR_MANIFEST_INVALID: Replacement manifest.json is invalid JSON.");
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 new Error(`ERR_PATH_INVALID: ${pathError}`);
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 new Error(`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`);
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 new Error("ERR_MANIFEST_INVALID: manifest.json is not valid JSON");
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 new Error("ERR_ASSET_BYTES_INVALID: Asset source is not readable as bytes.");
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 new Error(`ERR_ASSET_BYTES_MISSING: Asset "${asset.path}" has no bytes or readBytes() source.`);
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 new Error("ERR_PACK_NO_INPUT: No files found to package.");
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 new Error(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${manifest.entryPoint}" does not exist.`);
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 new Error(`ERR_PACK_ENTRYPOINT_MISSING: Manifest entry-point "${declaredEntryPoint}" does not exist in archive.`);
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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"}