@uniweb/semantic-parser 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sanitize.js +100 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/sanitize.js CHANGED
@@ -441,6 +441,19 @@ export function sanitizeSVG(svg) {
441
441
  return rebuild(svg, SVG);
442
442
  }
443
443
 
444
+ /**
445
+ * What in an SVG string falls outside the allowlist `sanitizeSVG` rebuilds
446
+ * from, reported as `inspectMathML` reports MathML. Empty when the markup is
447
+ * within it.
448
+ *
449
+ * @param {string} svg
450
+ * @returns {Array<{reason: 'element'|'attribute'|'style'|'markup', element?: string, attribute?: string, value?: string}>}
451
+ */
452
+ export function inspectSVG(svg) {
453
+ if (typeof svg !== "string" || !svg) return [];
454
+ return inspect(svg, SVG);
455
+ }
456
+
444
457
  /**
445
458
  * Whether a link may be followed: a relative URL, or `http:`, `https:`,
446
459
  * `mailto:`, `tel:`, or the framework's `page:` / `topic:` references. Control
@@ -454,6 +467,75 @@ export function isSafeLink(url) {
454
467
  return typeof url === "string" && isSafeUrl(url);
455
468
  }
456
469
 
470
+ /**
471
+ * Whether an address may be loaded as an image: a link a page may follow
472
+ * (`isSafeLink`), or the image itself, as data (`isImageData`).
473
+ *
474
+ * @param {string} url
475
+ * @returns {boolean}
476
+ */
477
+ export function isImageSource(url) {
478
+ return isSafeLink(url) || isImageData(url);
479
+ }
480
+
481
+ /**
482
+ * Whether a `data:` URL holds an image a page may show: a PNG, JPEG, GIF, WebP
483
+ * or AVIF in base64, or SVG — percent-encoded or base64 — whose markup is within
484
+ * the allowlist `sanitizeSVG` rebuilds from. ⭐ Admitted [Diego, 2026-10-16]: a
485
+ * picture an `<img>` loads runs no script, and a built-in starter image is one.
486
+ * The URL is read as a browser reads it: tab and newline removed, leading and
487
+ * trailing spaces and controls trimmed.
488
+ *
489
+ * @param {string} url
490
+ * @returns {boolean}
491
+ */
492
+ export function isImageData(url) {
493
+ if (typeof url !== "string") return false;
494
+ const address = url.replace(/[\t\n\r]/g, "").replace(/^[\u0000- ]+|[\u0000- ]+$/g, "");
495
+ if (RASTER_DATA.test(address)) return true;
496
+ const svg = svgOfData(address);
497
+ return svg !== null && inspect(svg, SVG).length === 0;
498
+ }
499
+
500
+ const SVG_DATA = /^data:image\/svg\+xml((?:;[^,;]*)*?)(;base64)?,([\s\S]*)$/i;
501
+
502
+ /** The markup an SVG `data:` URL holds, decoded as a browser decodes it; null for any other URL. */
503
+ function svgOfData(address) {
504
+ const data = SVG_DATA.exec(address);
505
+ if (!data) return null;
506
+ const body = percentDecode(data[3]);
507
+ if (!data[2]) return body;
508
+ try {
509
+ const binary = atob(body.replace(/[\t\n\f\r ]+/g, ""));
510
+ return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+
516
+ /** Percent-decode as a URL is: `%` and two hex digits is a byte, anything else stays, and the bytes are read as UTF-8. */
517
+ function percentDecode(text) {
518
+ if (!text.includes("%")) return text;
519
+ const bytes = [];
520
+ const utf8 = new TextEncoder();
521
+ let run = "";
522
+ const flush = () => {
523
+ for (const byte of utf8.encode(run)) bytes.push(byte);
524
+ run = "";
525
+ };
526
+ for (let i = 0; i < text.length; i++) {
527
+ if (text[i] === "%" && /^[0-9a-f]{2}$/i.test(text.slice(i + 1, i + 3))) {
528
+ flush();
529
+ bytes.push(parseInt(text.slice(i + 1, i + 3), 16));
530
+ i += 2;
531
+ } else {
532
+ run += text[i];
533
+ }
534
+ }
535
+ flush();
536
+ return new TextDecoder().decode(new Uint8Array(bytes));
537
+ }
538
+
457
539
  // ─── Values content may hold ─────────────────────────────────────────────────
458
540
  //
459
541
  // The rules below decide what a text's marks may put into the markup the parser
@@ -552,5 +634,23 @@ export function allowlists() {
552
634
  style: "declaration by declaration: a property of letters and -, a value with none of \\ < > { } /* and no unsafe value",
553
635
  markup: "no comment, doctype or processing instruction; CDATA is text",
554
636
  },
637
+ imageData: {
638
+ raster: RASTER_DATA.source,
639
+ rasterFlags: "i",
640
+ svg: {
641
+ mediaType: "image/svg+xml, with any parameters, and `;base64` when the body is base64",
642
+ decoding: "the body percent-decoded as a URL is (`%` and two hex digits is a byte, anything else stays), then base64-decoded when marked, read as UTF-8",
643
+ elements: [...SVG_ELEMENTS.values()],
644
+ attributes: [...SVG_ATTRIBUTES.values()],
645
+ commonAttributes: "class, id, role, dir, aria-<letters>, data-<letters, digits and ->",
646
+ droppedWithContent: [...DROP_WITH_CONTENT],
647
+ neverAttributes: "on…, in any case; src",
648
+ references: "href and xlink:href only: a fragment, `#` then a letter then letters, digits, _ . : or -; on `image`, also a raster image matching `raster`, or a URL with no scheme or one of the link schemes",
649
+ unsafeValue: UNSAFE_VALUE.source,
650
+ urlInAValue: "only url(#id), a reference within the document",
651
+ style: "declaration by declaration: a property of letters and -, a value with none of \\ < > { } /* and no unsafe value",
652
+ markup: "no comment, doctype or processing instruction; CDATA is text",
653
+ },
654
+ },
555
655
  };
556
656
  }