@ngroznykh/papirus 0.3.5 → 0.3.7

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/LICENSE CHANGED
@@ -1,21 +1,19 @@
1
- MIT License
1
+ Papirus Dual License
2
2
 
3
3
  Copyright (c) 2026 Papirus Contributors
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ This project is dual-licensed:
11
6
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
7
+ 1) GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
8
+ You may use, modify, and distribute this software under AGPL terms.
9
+ If you modify and run this software (including over a network), you must
10
+ provide complete corresponding source code under AGPL.
14
11
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
12
+ 2) Commercial License
13
+ For proprietary, closed-source, or commercial use cases that are not
14
+ compatible with AGPL obligations, you must obtain a commercial license.
15
+ See LICENSE_COMMERCIAL.md for terms and contact details.
16
+
17
+ SPDX-License-Identifier: AGPL-3.0-or-later
18
+
19
+ Full AGPL text: https://www.gnu.org/licenses/agpl-3.0.txt
package/LICENSE.ru.md ADDED
@@ -0,0 +1,21 @@
1
+ # Лицензирование Papirus (перевод)
2
+
3
+ Этот документ является русскоязычным переводом и пояснением файла `LICENSE`.
4
+
5
+ В случае расхождений приоритет имеет оригинальный текст `LICENSE` (английская версия).
6
+
7
+ ## Dual License
8
+
9
+ Проект распространяется по двойной лицензии:
10
+
11
+ 1. **GNU Affero General Public License v3.0 или новее (AGPL-3.0-or-later)**
12
+ - Вы можете использовать, изменять и распространять ПО по условиям AGPL.
13
+ - Если вы модифицируете ПО и запускаете его (включая сетевой доступ), вы обязаны предоставить полный исходный код соответствующей версии по AGPL.
14
+
15
+ 2. **Коммерческая лицензия**
16
+ - Для проприетарного/закрытого или иного коммерческого использования, несовместимого с обязанностями AGPL, требуется коммерческая лицензия.
17
+ - См. `LICENSE_COMMERCIAL.md`.
18
+
19
+ SPDX: `AGPL-3.0-or-later`
20
+
21
+ Полный текст AGPL: https://www.gnu.org/licenses/agpl-3.0.txt
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Papirus
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40ngroznykh%2Fpapirus.svg)](https://www.npmjs.com/package/@ngroznykh/papirus)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![License: AGPL%20v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
5
5
 
6
6
  TypeScript library for building interactive 2D diagrams and flowcharts on HTML Canvas. Supports nodes, edges, groups, styling, serialization, export, and interactivity.
7
7
 
@@ -65,6 +65,7 @@ renderer.enableInteractions();
65
65
  - [Utils](./docs/utils.md)
66
66
  - [Changelog](./CHANGELOG.md)
67
67
  - [Security Policy](./SECURITY.md)
68
+ - [Open Source Preparation](./docs/OPEN_SOURCE_PREPARATION.md)
68
69
 
69
70
  ## Features
70
71
 
@@ -193,6 +194,20 @@ npm run test:coverage # Tests with coverage
193
194
 
194
195
  Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
195
196
 
197
+ Key governance and contribution files:
198
+
199
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) / [CONTRIBUTING.ru.md](./CONTRIBUTING.ru.md)
200
+ - [SECURITY.md](./SECURITY.md) / [SECURITY.ru.md](./SECURITY.ru.md)
201
+ - [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) / [CODE_OF_CONDUCT.ru.md](./CODE_OF_CONDUCT.ru.md)
202
+
196
203
  ## License
197
204
 
198
- This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
205
+ This project uses dual licensing:
206
+
207
+ - `AGPL-3.0-or-later` for open-source usage
208
+ - Commercial license for proprietary/closed-source commercial usage
209
+
210
+ See:
211
+
212
+ - [LICENSE](./LICENSE)
213
+ - [LICENSE_COMMERCIAL.md](./LICENSE_COMMERCIAL.md)
package/README.ru.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Papirus
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40ngroznykh%2Fpapirus.svg)](https://www.npmjs.com/package/@ngroznykh/papirus)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![License: AGPL%20v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
5
5
 
6
6
  Papirus — библиотека на TypeScript для построения интерактивных 2D‑схем на HTML Canvas. Поддерживает узлы, связи, группы, стили, сериализацию, экспорт и интерактивность.
7
7
 
@@ -65,6 +65,7 @@ renderer.enableInteractions();
65
65
  - [Утилиты](./docs/utils.md)
66
66
  - [Changelog](./CHANGELOG.md)
67
67
  - [Политика безопасности](./SECURITY.md)
68
+ - [Чеклист подготовки к Open Source](./docs/OPEN_SOURCE_PREPARATION.ru.md)
68
69
 
69
70
  ## Возможности
70
71
 
@@ -194,6 +195,20 @@ npm run test:coverage # Тесты с покрытием
194
195
 
195
196
  См. [CONTRIBUTING.md](./CONTRIBUTING.md).
196
197
 
198
+ Ключевые файлы для open-source процесса:
199
+
200
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) / [CONTRIBUTING.ru.md](./CONTRIBUTING.ru.md)
201
+ - [SECURITY.md](./SECURITY.md) / [SECURITY.ru.md](./SECURITY.ru.md)
202
+ - [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) / [CODE_OF_CONDUCT.ru.md](./CODE_OF_CONDUCT.ru.md)
203
+
197
204
  ## Лицензия
198
205
 
199
- MIT см. [LICENSE](./LICENSE).
206
+ Проект использует dual licensing:
207
+
208
+ - `AGPL-3.0-or-later` для open-source использования
209
+ - Коммерческая лицензия для проприетарного/закрытого коммерческого использования
210
+
211
+ См.:
212
+
213
+ - [LICENSE](./LICENSE) / [LICENSE.ru.md](./LICENSE.ru.md)
214
+ - [LICENSE_COMMERCIAL.md](./LICENSE_COMMERCIAL.md) / [LICENSE_COMMERCIAL.ru.md](./LICENSE_COMMERCIAL.ru.md)
@@ -89,7 +89,7 @@ export declare class TextLabel {
89
89
  /**
90
90
  * Render the label within bounds
91
91
  */
92
- render(ctx: CanvasRenderingContext2D, bounds: Bounds): void;
92
+ render(ctx: CanvasRenderingContext2D, bounds: Bounds, alignOverride?: 'left' | 'center' | 'right'): void;
93
93
  /**
94
94
  * Render at a specific point (for edge labels)
95
95
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TextLabel.d.ts","sourceRoot":"","sources":["../../src/elements/TextLabel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TextLabel.d.ts","sourceRoot":"","sources":["../../src/elements/TextLabel.ts"],"names":[],"mappings":"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"}
package/dist/papirus.js CHANGED
@@ -2081,11 +2081,15 @@ class TextLabel {
2081
2081
  /**
2082
2082
  * Render the label within bounds
2083
2083
  */
2084
- render(ctx, bounds) {
2084
+ render(ctx, bounds, alignOverride) {
2085
2085
  if (this._lines.length === 0) {
2086
2086
  this.measure(ctx);
2087
2087
  }
2088
2088
  this.applyStyle(ctx);
2089
+ const align = alignOverride ?? this._style.align ?? "center";
2090
+ if (alignOverride) {
2091
+ ctx.textAlign = align;
2092
+ }
2089
2093
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2090
2094
  const totalHeight = this._lines.length * lineHeight;
2091
2095
  const margin = this._margin;
@@ -2096,7 +2100,7 @@ class TextLabel {
2096
2100
  height: Math.max(0, bounds.height - margin * 2)
2097
2101
  };
2098
2102
  let x;
2099
- switch (this._style.align) {
2103
+ switch (align) {
2100
2104
  case "left":
2101
2105
  x = innerBounds.x + this._padding;
2102
2106
  break;
@@ -8673,11 +8677,15 @@ class SvgExporter {
8673
8677
  const fill = style.fillColor ?? "#ffffff";
8674
8678
  const stroke = style.strokeColor ?? "#333333";
8675
8679
  const strokeWidth = style.strokeWidth ?? 2;
8676
- const opacity = style.opacity ?? 1;
8680
+ const baseOpacity = style.opacity ?? 1;
8681
+ const fillOpacity = (style.fillOpacity ?? 1) * baseOpacity;
8682
+ const strokeOpacity = (style.strokeOpacity ?? 1) * baseOpacity;
8683
+ const dash = style.lineDash?.length ? ` stroke-dasharray="${style.lineDash.join(" ")}"` : "";
8684
+ const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
8677
8685
  let shape;
8678
8686
  switch (node.typeName) {
8679
8687
  case "rectangle": {
8680
- const radius = (style.cornerRadius ?? 0).toString();
8688
+ const radius = this.getNodeCornerRadius(node, bounds);
8681
8689
  shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
8682
8690
  break;
8683
8691
  }
@@ -8703,9 +8711,11 @@ class SvgExporter {
8703
8711
  shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
8704
8712
  }
8705
8713
  }
8706
- const label = node.label ? this.renderTextLabel(node.label.text, node.getCenter(), node.label.style) : "";
8714
+ const label = node.label ? this.renderTextLabel(node.label.text, node.getLabelPosition(), node.label.style) : "";
8715
+ const icon = this.renderNodeIcon(node, bounds);
8707
8716
  return [
8708
- `${shape} fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" opacity="${opacity}"/>`,
8717
+ `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
8718
+ icon,
8709
8719
  label
8710
8720
  ].join("");
8711
8721
  }
@@ -8892,12 +8902,236 @@ class SvgExporter {
8892
8902
  const fontSize = style.fontSize ?? 14;
8893
8903
  const fontFamily = style.fontFamily ?? "sans-serif";
8894
8904
  const fontWeight = style.fontWeight ?? "normal";
8905
+ const opacity = style.opacity ?? 1;
8895
8906
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
8896
8907
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
8897
- return `<text x="${point.x}" y="${point.y}" fill="${fill}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}">${this.escapeText(
8908
+ return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}">${this.escapeText(
8898
8909
  text
8899
8910
  )}</text>`;
8900
8911
  }
8912
+ getNodeCornerRadius(node, bounds) {
8913
+ const rectangleRadius = "cornerRadius" in node && typeof node.cornerRadius === "number" ? node.cornerRadius ?? 0 : node.style.cornerRadius ?? 0;
8914
+ return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
8915
+ }
8916
+ renderNodeIcon(node, nodeBounds) {
8917
+ const icon = node.icon;
8918
+ if (!icon) {
8919
+ return "";
8920
+ }
8921
+ const opts = icon.options;
8922
+ const iconSize = icon.getSize();
8923
+ if (iconSize.width <= 0 || iconSize.height <= 0) {
8924
+ return "";
8925
+ }
8926
+ const iconBoxSize = this.getIconBoxSize(opts, iconSize);
8927
+ const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center");
8928
+ const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize);
8929
+ if (drawRect.width <= 0 || drawRect.height <= 0) {
8930
+ return "";
8931
+ }
8932
+ const href = this.resolveIconHref(opts);
8933
+ if (!href) {
8934
+ return "";
8935
+ }
8936
+ const opacity = opts.opacity ?? 1;
8937
+ return `<image href="${this.escapeAttribute(href)}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
8938
+ }
8939
+ getIconBoxSize(opts, imageSize) {
8940
+ const padding = opts.padding ?? 8;
8941
+ const margin = Math.max(0, opts.margin ?? 0);
8942
+ return {
8943
+ width: imageSize.width + padding * 2 + margin * 2,
8944
+ height: imageSize.height + padding * 2 + margin * 2
8945
+ };
8946
+ }
8947
+ getIconBounds(bounds, iconBoxSize, placement) {
8948
+ switch (placement) {
8949
+ case "top":
8950
+ return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
8951
+ case "bottom":
8952
+ return {
8953
+ x: bounds.x,
8954
+ y: bounds.y + bounds.height - iconBoxSize.height,
8955
+ width: bounds.width,
8956
+ height: iconBoxSize.height
8957
+ };
8958
+ case "left":
8959
+ return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: bounds.height };
8960
+ case "right":
8961
+ return {
8962
+ x: bounds.x + bounds.width - iconBoxSize.width,
8963
+ y: bounds.y,
8964
+ width: iconBoxSize.width,
8965
+ height: bounds.height
8966
+ };
8967
+ case "top-left":
8968
+ return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: iconBoxSize.height };
8969
+ case "top-right":
8970
+ return {
8971
+ x: bounds.x + bounds.width - iconBoxSize.width,
8972
+ y: bounds.y,
8973
+ width: iconBoxSize.width,
8974
+ height: iconBoxSize.height
8975
+ };
8976
+ case "bottom-left":
8977
+ return {
8978
+ x: bounds.x,
8979
+ y: bounds.y + bounds.height - iconBoxSize.height,
8980
+ width: iconBoxSize.width,
8981
+ height: iconBoxSize.height
8982
+ };
8983
+ case "bottom-right":
8984
+ return {
8985
+ x: bounds.x + bounds.width - iconBoxSize.width,
8986
+ y: bounds.y + bounds.height - iconBoxSize.height,
8987
+ width: iconBoxSize.width,
8988
+ height: iconBoxSize.height
8989
+ };
8990
+ case "center":
8991
+ default:
8992
+ return bounds;
8993
+ }
8994
+ }
8995
+ getIconDrawRect(bounds, opts, imageSize) {
8996
+ const padding = opts.padding ?? 8;
8997
+ const margin = Math.max(0, opts.margin ?? 0);
8998
+ const fit = opts.fit ?? "none";
8999
+ const scaleWithBounds = opts.scaleWithBounds ?? false;
9000
+ const align = opts.align ?? "center";
9001
+ const verticalAlign = opts.verticalAlign ?? "center";
9002
+ const offsetX = opts.offsetX ?? 0;
9003
+ const offsetY = opts.offsetY ?? 0;
9004
+ const innerBounds = {
9005
+ x: bounds.x + margin,
9006
+ y: bounds.y + margin,
9007
+ width: Math.max(0, bounds.width - margin * 2),
9008
+ height: Math.max(0, bounds.height - margin * 2)
9009
+ };
9010
+ const availableWidth = Math.max(0, innerBounds.width - padding * 2);
9011
+ const availableHeight = Math.max(0, innerBounds.height - padding * 2);
9012
+ let drawWidth = opts.width ?? imageSize.width;
9013
+ let drawHeight = opts.height ?? imageSize.height;
9014
+ if (scaleWithBounds) {
9015
+ if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
9016
+ const scaleX = availableWidth / imageSize.width;
9017
+ const scaleY = availableHeight / imageSize.height;
9018
+ const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
9019
+ drawWidth = imageSize.width * scale;
9020
+ drawHeight = imageSize.height * scale;
9021
+ } else if (fit === "stretch") {
9022
+ drawWidth = availableWidth;
9023
+ drawHeight = availableHeight;
9024
+ }
9025
+ }
9026
+ drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
9027
+ drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
9028
+ let x = innerBounds.x + padding;
9029
+ let y = innerBounds.y + padding;
9030
+ if (align === "center") {
9031
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
9032
+ } else if (align === "right") {
9033
+ x = innerBounds.x + innerBounds.width - drawWidth - padding;
9034
+ }
9035
+ if (verticalAlign === "center") {
9036
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
9037
+ } else if (verticalAlign === "bottom") {
9038
+ y = innerBounds.y + innerBounds.height - drawHeight - padding;
9039
+ }
9040
+ return {
9041
+ x: x + offsetX,
9042
+ y: y + offsetY,
9043
+ width: drawWidth,
9044
+ height: drawHeight
9045
+ };
9046
+ }
9047
+ resolveIconHref(opts) {
9048
+ const source = opts.source;
9049
+ if (source instanceof HTMLImageElement) {
9050
+ return source.src;
9051
+ }
9052
+ if (!source) {
9053
+ return "";
9054
+ }
9055
+ if (this.isSvgMarkup(source)) {
9056
+ return this.svgToDataUrl(this.tintSvg(source, opts.strokeColor, opts.fillColor));
9057
+ }
9058
+ const shouldInlineSvg = source.toLowerCase().endsWith(".svg");
9059
+ if (shouldInlineSvg) {
9060
+ const svgText = this.readSvgFromUrlSync(source);
9061
+ if (svgText) {
9062
+ return this.svgToDataUrl(this.tintSvg(svgText, opts.strokeColor, opts.fillColor));
9063
+ }
9064
+ }
9065
+ return source;
9066
+ }
9067
+ readSvgFromUrlSync(url) {
9068
+ if (typeof XMLHttpRequest === "undefined") {
9069
+ return null;
9070
+ }
9071
+ try {
9072
+ const xhr = new XMLHttpRequest();
9073
+ xhr.open("GET", url, false);
9074
+ xhr.send();
9075
+ if (xhr.status >= 200 && xhr.status < 300) {
9076
+ return xhr.responseText || null;
9077
+ }
9078
+ } catch {
9079
+ }
9080
+ return null;
9081
+ }
9082
+ isSvgMarkup(value) {
9083
+ const trimmed = value.trim().toLowerCase();
9084
+ return trimmed.startsWith("<svg") || trimmed.includes("<svg");
9085
+ }
9086
+ styleSetColor(style, key, color) {
9087
+ const hasKey = new RegExp(`${key}\\s*:`).test(style);
9088
+ if (hasKey) {
9089
+ return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
9090
+ }
9091
+ const suffix = style.trim().endsWith(";") || style.trim() === "" ? "" : ";";
9092
+ return `${style}${suffix}${key}:${color};`;
9093
+ }
9094
+ tintSvg(svgText, strokeColor, fillColor) {
9095
+ if (!strokeColor && !fillColor) return svgText;
9096
+ const parser = new DOMParser();
9097
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
9098
+ const root = doc.documentElement;
9099
+ if (!root || root.nodeName.toLowerCase() === "parsererror") {
9100
+ return svgText;
9101
+ }
9102
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
9103
+ for (const el of all) {
9104
+ const stroke = el.getAttribute("stroke");
9105
+ if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
9106
+ el.setAttribute("stroke", strokeColor);
9107
+ }
9108
+ const fill = el.getAttribute("fill");
9109
+ if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
9110
+ el.setAttribute("fill", fillColor);
9111
+ }
9112
+ const style = el.getAttribute("style");
9113
+ if (style) {
9114
+ let next = style;
9115
+ if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) {
9116
+ next = this.styleSetColor(next, "stroke", strokeColor);
9117
+ }
9118
+ if (fillColor && /fill\s*:\s*(?!none)/.test(style)) {
9119
+ next = this.styleSetColor(next, "fill", fillColor);
9120
+ }
9121
+ if (next !== style) {
9122
+ el.setAttribute("style", next);
9123
+ }
9124
+ }
9125
+ }
9126
+ return new XMLSerializer().serializeToString(root);
9127
+ }
9128
+ svgToDataUrl(svg) {
9129
+ const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
9130
+ return `data:image/svg+xml;utf8,${encoded}`;
9131
+ }
9132
+ escapeAttribute(value) {
9133
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9134
+ }
8901
9135
  createEmptySvg(width, height, backgroundColor, includeBackground) {
8902
9136
  return [
8903
9137
  `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,