@uniweb/semantic-parser 1.10.0 → 1.11.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sanitize.js +40 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
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
@@ -347,9 +347,16 @@ function rebuild(input, policy) {
347
347
 
348
348
  /**
349
349
  * What a rebuild would leave out of `input`, item by item — the same walk as
350
- * `rebuild`, reporting instead of writing. Empty when the markup is within the
351
- * policy, which is when a rebuild changes nothing but spelling (quotes,
352
- * escapes, a style's spacing).
350
+ * `rebuild`, reporting instead of writing — and, when it leaves nothing out but
351
+ * writes the markup differently, one `spelling` item at the first difference.
352
+ * Empty when a rebuild writes `input` back exactly as given.
353
+ *
354
+ * ⭐ Exactly, not "up to spelling" (since 2026-10-18): every text run and
355
+ * attribute value a rebuild writes is escaped, so markup in that form reads the
356
+ * same to any reader. Markup spelled otherwise — a `<` left in text, a CDATA
357
+ * section, a reference the rebuild would write as its character — can read one
358
+ * way here and another way to a reader that parses it as written, which a store
359
+ * holding it for every reader cannot allow.
353
360
  */
354
361
  function inspect(input, policy) {
355
362
  const outside = [];
@@ -394,6 +401,14 @@ function inspect(input, policy) {
394
401
  }
395
402
  }
396
403
  }
404
+ if (!outside.length) {
405
+ const rebuilt = rebuild(input, policy);
406
+ if (rebuilt !== input) {
407
+ let at = 0;
408
+ while (input[at] === rebuilt[at]) at++;
409
+ outside.push({ reason: "spelling", value: input.slice(at, at + 40) });
410
+ }
411
+ }
397
412
  return outside;
398
413
  }
399
414
 
@@ -412,14 +427,16 @@ export function sanitizeMathML(mathml) {
412
427
  /**
413
428
  * What in a MathML string falls outside the allowlist `sanitizeMathML` rebuilds
414
429
  * from: each element and attribute a rebuild drops, each style declaration it
415
- * drops, and any comment, doctype or processing instruction. Empty when the
416
- * markup is within it.
430
+ * drops, and any comment, doctype or processing instruction — or, when it drops
431
+ * nothing, a `spelling` item if the rebuild writes the markup differently. Empty
432
+ * when `sanitizeMathML` writes the markup back exactly as given, so a writer
433
+ * that stores what `sanitizeMathML` returns stores markup within it.
417
434
  *
418
435
  * This is how content is checked rather than cleaned — a writer refused with
419
436
  * what it sent can fix it, where a cleaned value changes without a word.
420
437
  *
421
438
  * @param {string} mathml
422
- * @returns {Array<{reason: 'element'|'attribute'|'style'|'markup', element?: string, attribute?: string, value?: string}>}
439
+ * @returns {Array<{reason: 'element'|'attribute'|'style'|'markup'|'spelling', element?: string, attribute?: string, value?: string}>}
423
440
  */
424
441
  export function inspectMathML(mathml) {
425
442
  if (typeof mathml !== "string" || !mathml) return [];
@@ -443,11 +460,11 @@ export function sanitizeSVG(svg) {
443
460
 
444
461
  /**
445
462
  * 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.
463
+ * from, reported as `inspectMathML` reports MathML. Empty when `sanitizeSVG`
464
+ * writes the markup back exactly as given.
448
465
  *
449
466
  * @param {string} svg
450
- * @returns {Array<{reason: 'element'|'attribute'|'style'|'markup', element?: string, attribute?: string, value?: string}>}
467
+ * @returns {Array<{reason: 'element'|'attribute'|'style'|'markup'|'spelling', element?: string, attribute?: string, value?: string}>}
451
468
  */
452
469
  export function inspectSVG(svg) {
453
470
  if (typeof svg !== "string" || !svg) return [];
@@ -480,8 +497,8 @@ export function isImageSource(url) {
480
497
 
481
498
  /**
482
499
  * 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
500
+ * or AVIF in base64, or SVG — percent-encoded or base64 — whose markup is
501
+ * exactly what `sanitizeSVG` writes of it (`inspectSVG`). ⭐ Admitted [Diego, 2026-10-16]: a
485
502
  * picture an `<img>` loads runs no script, and a built-in starter image is one.
486
503
  * The URL is read as a browser reads it: tab and newline removed, leading and
487
504
  * trailing spaces and controls trimmed.
@@ -598,6 +615,14 @@ export function isCssColor(value) {
598
615
  return typeof value === "string" && CSS_COLOR.test(value) && !NOT_A_COLOR.test(value);
599
616
  }
600
617
 
618
+ /** How a rebuild writes markup — what `inspect` compares the input with. */
619
+ const WRITTEN =
620
+ "exactly as a rebuild writes it: a text run with `&`, `<` and `>` written `&amp;`, `&lt;` and `&gt;`, and every other character as itself; " +
621
+ "a start tag `<name>` or `<name/>` and an end tag `</name>`, the name as `elements` spells it, with no space before `>` or `/>`; " +
622
+ "each attribute once, as one space then `name=\"value\"` — the name as `attributes` spells it, or a common one in lower case — " +
623
+ "with `&`, `<`, `>` and `\"` in the value written `&amp;`, `&lt;`, `&gt;` and `&quot;`; " +
624
+ "a `style` value as its declarations, each `property:value;` with the property in lower case and the value trimmed";
625
+
601
626
  /**
602
627
  * The same rules, as data — for a reader that implements them in another
603
628
  * language, such as a backend checking content when it is written. Every list
@@ -632,7 +657,8 @@ export function allowlists() {
632
657
  unsafeValue: UNSAFE_VALUE.source,
633
658
  urlInAValue: "only url(#id), a reference within the document",
634
659
  style: "declaration by declaration: a property of letters and -, a value with none of \\ < > { } /* and no unsafe value",
635
- markup: "no comment, doctype or processing instruction; CDATA is text",
660
+ markup: "no comment, doctype, processing instruction or CDATA section",
661
+ written: WRITTEN,
636
662
  },
637
663
  imageData: {
638
664
  raster: RASTER_DATA.source,
@@ -649,7 +675,8 @@ export function allowlists() {
649
675
  unsafeValue: UNSAFE_VALUE.source,
650
676
  urlInAValue: "only url(#id), a reference within the document",
651
677
  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",
678
+ markup: "no comment, doctype, processing instruction or CDATA section",
679
+ written: `${WRITTEN}; an \`href\` or \`xlink:href\` value trimmed`,
653
680
  },
654
681
  },
655
682
  };