@ngroznykh/papirus 0.3.6 → 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 +14 -16
- package/LICENSE.ru.md +21 -0
- package/README.md +17 -2
- package/README.ru.md +17 -2
- package/dist/papirus.js +235 -5
- package/dist/papirus.js.map +1 -1
- package/dist/utils/SvgExporter.d.ts +12 -0
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/package.json +6 -6
package/LICENSE
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
Papirus Dual License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Papirus Contributors
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
[](https://www.npmjs.com/package/@ngroznykh/papirus)
|
|
4
|
-
[](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
|
|
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
|
[](https://www.npmjs.com/package/@ngroznykh/papirus)
|
|
4
|
-
[](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
|
-
|
|
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)
|
package/dist/papirus.js
CHANGED
|
@@ -8677,11 +8677,15 @@ class SvgExporter {
|
|
|
8677
8677
|
const fill = style.fillColor ?? "#ffffff";
|
|
8678
8678
|
const stroke = style.strokeColor ?? "#333333";
|
|
8679
8679
|
const strokeWidth = style.strokeWidth ?? 2;
|
|
8680
|
-
const
|
|
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}"` : "";
|
|
8681
8685
|
let shape;
|
|
8682
8686
|
switch (node.typeName) {
|
|
8683
8687
|
case "rectangle": {
|
|
8684
|
-
const radius = (
|
|
8688
|
+
const radius = this.getNodeCornerRadius(node, bounds);
|
|
8685
8689
|
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
|
|
8686
8690
|
break;
|
|
8687
8691
|
}
|
|
@@ -8707,9 +8711,11 @@ class SvgExporter {
|
|
|
8707
8711
|
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
|
|
8708
8712
|
}
|
|
8709
8713
|
}
|
|
8710
|
-
const label = node.label ? this.renderTextLabel(node.label.text, node.
|
|
8714
|
+
const label = node.label ? this.renderTextLabel(node.label.text, node.getLabelPosition(), node.label.style) : "";
|
|
8715
|
+
const icon = this.renderNodeIcon(node, bounds);
|
|
8711
8716
|
return [
|
|
8712
|
-
`${shape} fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" opacity="${
|
|
8717
|
+
`${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
|
|
8718
|
+
icon,
|
|
8713
8719
|
label
|
|
8714
8720
|
].join("");
|
|
8715
8721
|
}
|
|
@@ -8896,12 +8902,236 @@ class SvgExporter {
|
|
|
8896
8902
|
const fontSize = style.fontSize ?? 14;
|
|
8897
8903
|
const fontFamily = style.fontFamily ?? "sans-serif";
|
|
8898
8904
|
const fontWeight = style.fontWeight ?? "normal";
|
|
8905
|
+
const opacity = style.opacity ?? 1;
|
|
8899
8906
|
const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
|
|
8900
8907
|
const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
|
|
8901
|
-
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(
|
|
8902
8909
|
text
|
|
8903
8910
|
)}</text>`;
|
|
8904
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9134
|
+
}
|
|
8905
9135
|
createEmptySvg(width, height, backgroundColor, includeBackground) {
|
|
8906
9136
|
return [
|
|
8907
9137
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
|