@ai-gui/core 0.19.0 → 0.20.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/dist/index.cjs CHANGED
@@ -1738,6 +1738,44 @@ function measure(svg, options) {
1738
1738
  };
1739
1739
  }
1740
1740
  /**
1741
+ * A copy of the drawing that a canvas will accept.
1742
+ *
1743
+ * The problem is `<foreignObject>`. Mermaid lays every node label out as HTML inside one, and a browser
1744
+ * taints the canvas the moment an image containing one is drawn — after which `toDataURL` throws
1745
+ * `SecurityError` and the export is lost. Nothing about the diagram is unsafe; the rule is categorical.
1746
+ *
1747
+ * So each `<foreignObject>` becomes plain SVG `<text>` at the same position, carrying the same words.
1748
+ * The result is a slightly plainer label — no wrapping, no HTML styling — and a diagram that exports at
1749
+ * all, which is the trade the alternative does not offer.
1750
+ */
1751
+ function rasterisable(svg) {
1752
+ const copy = svg.cloneNode(true);
1753
+ const foreign = Array.from(copy.querySelectorAll("foreignObject"));
1754
+ for (const object of foreign) {
1755
+ const words = (object.textContent ?? "").trim();
1756
+ const parent = object.parentNode;
1757
+ if (!parent) continue;
1758
+ if (!words) {
1759
+ parent.removeChild(object);
1760
+ continue;
1761
+ }
1762
+ const x = Number.parseFloat(object.getAttribute("x") ?? "0");
1763
+ const y = Number.parseFloat(object.getAttribute("y") ?? "0");
1764
+ const width = Number.parseFloat(object.getAttribute("width") ?? "0");
1765
+ const height = Number.parseFloat(object.getAttribute("height") ?? "0");
1766
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
1767
+ text.setAttribute("x", String(x + width / 2));
1768
+ text.setAttribute("y", String(y + height / 2));
1769
+ text.setAttribute("text-anchor", "middle");
1770
+ text.setAttribute("dominant-baseline", "middle");
1771
+ text.setAttribute("font-size", "14");
1772
+ text.setAttribute("fill", "currentColor");
1773
+ text.textContent = words;
1774
+ parent.replaceChild(text, object);
1775
+ }
1776
+ return copy;
1777
+ }
1778
+ /**
1741
1779
  * Render an SVG element to a raster data URL.
1742
1780
  *
1743
1781
  * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
@@ -1746,7 +1784,7 @@ function measure(svg, options) {
1746
1784
  async function exportSVGToImage(svg, options = {}) {
1747
1785
  const { width, height } = measure(svg, options);
1748
1786
  const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1749
- const source = new XMLSerializer().serializeToString(svg);
1787
+ const source = new XMLSerializer().serializeToString(rasterisable(svg));
1750
1788
  const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1751
1789
  const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1752
1790
  try {
@@ -1779,9 +1817,15 @@ async function exportSVGToImage(svg, options = {}) {
1779
1817
  * decide what lands in it.
1780
1818
  */
1781
1819
  async function exportRenderedImages(root, options = {}) {
1782
- const drawings = Array.from(root.querySelectorAll("svg"));
1783
1820
  const exported = [];
1784
- for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1821
+ for (const drawing of Array.from(root.querySelectorAll("svg"))) {
1822
+ if (drawing.closest(".katex")) continue;
1823
+ try {
1824
+ exported.push(await exportSVGToImage(drawing, options));
1825
+ } catch (reason) {
1826
+ options.onSkip?.(drawing, reason);
1827
+ }
1828
+ }
1785
1829
  return exported;
1786
1830
  }
1787
1831
  /** Save an exported image under the given file name. */
package/dist/index.d.cts CHANGED
@@ -655,6 +655,14 @@ interface ExportImageOptions {
655
655
  height?: number;
656
656
  type?: "image/png" | "image/jpeg" | "image/webp";
657
657
  quality?: number;
658
+ /**
659
+ * Told about each drawing that could not be exported, instead of the whole export failing.
660
+ *
661
+ * Some drawings cannot be rasterised at all: a Mermaid diagram lays its labels out in a
662
+ * `<foreignObject>`, and a browser taints the canvas the moment one is drawn onto it, so `toDataURL`
663
+ * throws `SecurityError`. One of those used to take every other drawing on the page down with it.
664
+ */
665
+ onSkip?: (drawing: SVGElement, reason: unknown) => void;
658
666
  }
659
667
  interface ExportedImage {
660
668
  dataUrl: string;
package/dist/index.d.ts CHANGED
@@ -655,6 +655,14 @@ interface ExportImageOptions {
655
655
  height?: number;
656
656
  type?: "image/png" | "image/jpeg" | "image/webp";
657
657
  quality?: number;
658
+ /**
659
+ * Told about each drawing that could not be exported, instead of the whole export failing.
660
+ *
661
+ * Some drawings cannot be rasterised at all: a Mermaid diagram lays its labels out in a
662
+ * `<foreignObject>`, and a browser taints the canvas the moment one is drawn onto it, so `toDataURL`
663
+ * throws `SecurityError`. One of those used to take every other drawing on the page down with it.
664
+ */
665
+ onSkip?: (drawing: SVGElement, reason: unknown) => void;
658
666
  }
659
667
  interface ExportedImage {
660
668
  dataUrl: string;
package/dist/index.js CHANGED
@@ -1714,6 +1714,44 @@ function measure(svg, options) {
1714
1714
  };
1715
1715
  }
1716
1716
  /**
1717
+ * A copy of the drawing that a canvas will accept.
1718
+ *
1719
+ * The problem is `<foreignObject>`. Mermaid lays every node label out as HTML inside one, and a browser
1720
+ * taints the canvas the moment an image containing one is drawn — after which `toDataURL` throws
1721
+ * `SecurityError` and the export is lost. Nothing about the diagram is unsafe; the rule is categorical.
1722
+ *
1723
+ * So each `<foreignObject>` becomes plain SVG `<text>` at the same position, carrying the same words.
1724
+ * The result is a slightly plainer label — no wrapping, no HTML styling — and a diagram that exports at
1725
+ * all, which is the trade the alternative does not offer.
1726
+ */
1727
+ function rasterisable(svg) {
1728
+ const copy = svg.cloneNode(true);
1729
+ const foreign = Array.from(copy.querySelectorAll("foreignObject"));
1730
+ for (const object of foreign) {
1731
+ const words = (object.textContent ?? "").trim();
1732
+ const parent = object.parentNode;
1733
+ if (!parent) continue;
1734
+ if (!words) {
1735
+ parent.removeChild(object);
1736
+ continue;
1737
+ }
1738
+ const x = Number.parseFloat(object.getAttribute("x") ?? "0");
1739
+ const y = Number.parseFloat(object.getAttribute("y") ?? "0");
1740
+ const width = Number.parseFloat(object.getAttribute("width") ?? "0");
1741
+ const height = Number.parseFloat(object.getAttribute("height") ?? "0");
1742
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
1743
+ text.setAttribute("x", String(x + width / 2));
1744
+ text.setAttribute("y", String(y + height / 2));
1745
+ text.setAttribute("text-anchor", "middle");
1746
+ text.setAttribute("dominant-baseline", "middle");
1747
+ text.setAttribute("font-size", "14");
1748
+ text.setAttribute("fill", "currentColor");
1749
+ text.textContent = words;
1750
+ parent.replaceChild(text, object);
1751
+ }
1752
+ return copy;
1753
+ }
1754
+ /**
1717
1755
  * Render an SVG element to a raster data URL.
1718
1756
  *
1719
1757
  * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
@@ -1722,7 +1760,7 @@ function measure(svg, options) {
1722
1760
  async function exportSVGToImage(svg, options = {}) {
1723
1761
  const { width, height } = measure(svg, options);
1724
1762
  const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1725
- const source = new XMLSerializer().serializeToString(svg);
1763
+ const source = new XMLSerializer().serializeToString(rasterisable(svg));
1726
1764
  const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1727
1765
  const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1728
1766
  try {
@@ -1755,9 +1793,15 @@ async function exportSVGToImage(svg, options = {}) {
1755
1793
  * decide what lands in it.
1756
1794
  */
1757
1795
  async function exportRenderedImages(root, options = {}) {
1758
- const drawings = Array.from(root.querySelectorAll("svg"));
1759
1796
  const exported = [];
1760
- for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1797
+ for (const drawing of Array.from(root.querySelectorAll("svg"))) {
1798
+ if (drawing.closest(".katex")) continue;
1799
+ try {
1800
+ exported.push(await exportSVGToImage(drawing, options));
1801
+ } catch (reason) {
1802
+ options.onSkip?.(drawing, reason);
1803
+ }
1804
+ }
1761
1805
  return exported;
1762
1806
  }
1763
1807
  /** Save an exported image under the given file name. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/core",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Headless streaming renderer core for LLM-generated UI — markdown/JSON repair, card registry, AST diff, plugin engine, sanitizer.",
5
5
  "keywords": [
6
6
  "llm",