@ngroznykh/papirus 0.3.6 → 0.3.8

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)
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 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}"` : "";
8681
8685
  let shape;
8682
8686
  switch (node.typeName) {
8683
8687
  case "rectangle": {
8684
- const radius = (style.cornerRadius ?? 0).toString();
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.getCenter(), node.label.style) : "";
8714
+ const label = this.renderNodeLabel(node, bounds);
8715
+ const icon = this.renderNodeIcon(node, bounds);
8711
8716
  return [
8712
- `${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,
8713
8719
  label
8714
8720
  ].join("");
8715
8721
  }
@@ -8721,14 +8727,72 @@ class SvgExporter {
8721
8727
  const style = edge.style;
8722
8728
  const stroke = style.strokeColor ?? "#666666";
8723
8729
  const strokeWidth = style.strokeWidth ?? 2;
8724
- const opacity = style.opacity ?? 1;
8730
+ const strokeOpacity = (style.strokeOpacity ?? 1) * (style.opacity ?? 1);
8731
+ const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
8732
+ const lineJoin = style.lineJoin ? ` stroke-linejoin="${style.lineJoin}"` : "";
8725
8733
  const dashValues = style.flowDash ?? style.lineDash;
8726
8734
  const dash = dashValues ? ` stroke-dasharray="${dashValues.join(" ")}"` : "";
8727
8735
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
8728
8736
  const d = this.buildPath(edge);
8729
8737
  const markerShapes = this.renderEdgeMarkers(edge, stroke);
8730
- const label = edge.label ? this.renderTextLabel(edge.label.text, this.getEdgeLabelPoint(edge, edgeLabelOffset), edge.label.style) : "";
8731
- return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" opacity="${opacity}" color="${stroke}"${dash}${dashOffset}/>${markerShapes}${label}`;
8738
+ const label = this.renderEdgeLabel(edge, edgeLabelOffset);
8739
+ return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}" color="${stroke}"${lineCap}${lineJoin}${dash}${dashOffset}/>${markerShapes}${label}`;
8740
+ }
8741
+ renderEdgeLabel(edge, edgeLabelOffset) {
8742
+ if (!edge.label) {
8743
+ return "";
8744
+ }
8745
+ const labelPoint = this.getEdgeLabelPoint(edge, edgeLabelOffset);
8746
+ const text = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
8747
+ const bg = this.renderEdgeLabelBackground(edge, labelPoint);
8748
+ return `${bg}${text}`;
8749
+ }
8750
+ renderEdgeLabelBackground(edge, point) {
8751
+ if (!edge.label) {
8752
+ return "";
8753
+ }
8754
+ const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.padding, edge.label.margin);
8755
+ const bgPadding = edge.labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
8756
+ const bgColor = edge.labelBackground?.color ?? "#ffffff";
8757
+ const bgOpacity = edge.labelBackground?.opacity ?? 1;
8758
+ const bgRadius = edge.labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
8759
+ const x = point.x - metrics.width / 2 - bgPadding;
8760
+ const y = point.y - metrics.height / 2 - bgPadding;
8761
+ const width = metrics.width + bgPadding * 2;
8762
+ const height = metrics.height + bgPadding * 2;
8763
+ const radius = Math.max(0, Math.min(bgRadius, width / 2, height / 2));
8764
+ if (radius <= 0) {
8765
+ return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
8766
+ }
8767
+ return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
8768
+ }
8769
+ measureTextLabel(text, style = {}, padding = 8, margin = 0) {
8770
+ const fontSize = style.fontSize ?? 14;
8771
+ const fontFamily = style.fontFamily ?? "sans-serif";
8772
+ const fontWeight = style.fontWeight ?? "normal";
8773
+ const lineHeight = fontSize * 1.2;
8774
+ const lines = text.split("\n");
8775
+ let maxWidth = 0;
8776
+ if (typeof document !== "undefined") {
8777
+ const canvas = document.createElement("canvas");
8778
+ const ctx = canvas.getContext("2d");
8779
+ if (ctx) {
8780
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
8781
+ for (const line of lines) {
8782
+ maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
8783
+ }
8784
+ }
8785
+ }
8786
+ if (maxWidth === 0) {
8787
+ const longestLine = lines.reduce((max, line) => line.length > max.length ? line : max, "");
8788
+ maxWidth = longestLine.length * fontSize * 0.6;
8789
+ }
8790
+ const resolvedPadding = Math.max(0, padding);
8791
+ const resolvedMargin = Math.max(0, margin);
8792
+ return {
8793
+ width: maxWidth + resolvedPadding * 2 + resolvedMargin * 2,
8794
+ height: lines.length * lineHeight + resolvedPadding * 2 + resolvedMargin * 2
8795
+ };
8732
8796
  }
8733
8797
  resolveMarkerConfig(edge, side) {
8734
8798
  const marker = side === "start" ? edge.startMarker : edge.endMarker;
@@ -8896,11 +8960,281 @@ class SvgExporter {
8896
8960
  const fontSize = style.fontSize ?? 14;
8897
8961
  const fontFamily = style.fontFamily ?? "sans-serif";
8898
8962
  const fontWeight = style.fontWeight ?? "normal";
8963
+ const opacity = style.opacity ?? 1;
8899
8964
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
8900
8965
  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(
8902
- text
8903
- )}</text>`;
8966
+ const lines = text.split("\n");
8967
+ if (lines.length <= 1) {
8968
+ 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(
8969
+ text
8970
+ )}</text>`;
8971
+ }
8972
+ const lineHeight = fontSize * 1.2;
8973
+ const startY = point.y - (lines.length - 1) * lineHeight / 2;
8974
+ const tspans = lines.map((line, index) => `<tspan x="${point.x}" y="${startY + index * lineHeight}">${this.escapeText(line)}</tspan>`).join("");
8975
+ 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}">${tspans}</text>`;
8976
+ }
8977
+ renderNodeLabel(node, bounds) {
8978
+ const label = node.label;
8979
+ if (!label) {
8980
+ return "";
8981
+ }
8982
+ const style = label.style;
8983
+ const fontSize = style.fontSize ?? 14;
8984
+ const lineHeight = fontSize * 1.2;
8985
+ const lines = label.text.split("\n");
8986
+ const textHeight = Math.max(lineHeight, lines.length * lineHeight);
8987
+ const padding = Math.max(0, label.padding);
8988
+ const margin = Math.max(0, label.margin);
8989
+ const inset = padding + margin;
8990
+ const align = style.align ?? "center";
8991
+ const placement = node.labelPlacement === "auto" ? "center" : node.labelPlacement;
8992
+ const inner = {
8993
+ x: bounds.x + margin,
8994
+ y: bounds.y + margin,
8995
+ width: Math.max(0, bounds.width - margin * 2),
8996
+ height: Math.max(0, bounds.height - margin * 2)
8997
+ };
8998
+ let x = inner.x + inner.width / 2;
8999
+ if (align === "left") {
9000
+ x = inner.x + inset;
9001
+ } else if (align === "right") {
9002
+ x = inner.x + inner.width - inset;
9003
+ }
9004
+ let y = inner.y + inner.height / 2;
9005
+ if (placement === "top") {
9006
+ y = inner.y + inset + textHeight / 2;
9007
+ } else if (placement === "bottom") {
9008
+ y = inner.y + inner.height - inset - textHeight / 2;
9009
+ } else if (placement === "left") {
9010
+ x = inner.x + inset;
9011
+ } else if (placement === "right") {
9012
+ x = inner.x + inner.width - inset;
9013
+ }
9014
+ return this.renderTextLabel(label.text, { x, y }, style);
9015
+ }
9016
+ getNodeCornerRadius(node, bounds) {
9017
+ const rectangleRadius = "cornerRadius" in node && typeof node.cornerRadius === "number" ? node.cornerRadius ?? 0 : node.style.cornerRadius ?? 0;
9018
+ return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
9019
+ }
9020
+ renderNodeIcon(node, nodeBounds) {
9021
+ const icon = node.icon;
9022
+ if (!icon) {
9023
+ return "";
9024
+ }
9025
+ const opts = icon.options;
9026
+ const iconSize = icon.getSize();
9027
+ if (iconSize.width <= 0 || iconSize.height <= 0) {
9028
+ return "";
9029
+ }
9030
+ const iconBoxSize = this.getIconBoxSize(opts, iconSize);
9031
+ const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center");
9032
+ const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize);
9033
+ if (drawRect.width <= 0 || drawRect.height <= 0) {
9034
+ return "";
9035
+ }
9036
+ const href = this.resolveIconHref(opts);
9037
+ if (!href) {
9038
+ return "";
9039
+ }
9040
+ const opacity = opts.opacity ?? 1;
9041
+ return `<image href="${this.escapeAttribute(href)}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
9042
+ }
9043
+ getIconBoxSize(opts, imageSize) {
9044
+ const padding = opts.padding ?? 8;
9045
+ const margin = Math.max(0, opts.margin ?? 0);
9046
+ return {
9047
+ width: imageSize.width + padding * 2 + margin * 2,
9048
+ height: imageSize.height + padding * 2 + margin * 2
9049
+ };
9050
+ }
9051
+ getIconBounds(bounds, iconBoxSize, placement) {
9052
+ switch (placement) {
9053
+ case "top":
9054
+ return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
9055
+ case "bottom":
9056
+ return {
9057
+ x: bounds.x,
9058
+ y: bounds.y + bounds.height - iconBoxSize.height,
9059
+ width: bounds.width,
9060
+ height: iconBoxSize.height
9061
+ };
9062
+ case "left":
9063
+ return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: bounds.height };
9064
+ case "right":
9065
+ return {
9066
+ x: bounds.x + bounds.width - iconBoxSize.width,
9067
+ y: bounds.y,
9068
+ width: iconBoxSize.width,
9069
+ height: bounds.height
9070
+ };
9071
+ case "top-left":
9072
+ return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: iconBoxSize.height };
9073
+ case "top-right":
9074
+ return {
9075
+ x: bounds.x + bounds.width - iconBoxSize.width,
9076
+ y: bounds.y,
9077
+ width: iconBoxSize.width,
9078
+ height: iconBoxSize.height
9079
+ };
9080
+ case "bottom-left":
9081
+ return {
9082
+ x: bounds.x,
9083
+ y: bounds.y + bounds.height - iconBoxSize.height,
9084
+ width: iconBoxSize.width,
9085
+ height: iconBoxSize.height
9086
+ };
9087
+ case "bottom-right":
9088
+ return {
9089
+ x: bounds.x + bounds.width - iconBoxSize.width,
9090
+ y: bounds.y + bounds.height - iconBoxSize.height,
9091
+ width: iconBoxSize.width,
9092
+ height: iconBoxSize.height
9093
+ };
9094
+ case "center":
9095
+ default:
9096
+ return bounds;
9097
+ }
9098
+ }
9099
+ getIconDrawRect(bounds, opts, imageSize) {
9100
+ const padding = opts.padding ?? 8;
9101
+ const margin = Math.max(0, opts.margin ?? 0);
9102
+ const fit = opts.fit ?? "none";
9103
+ const scaleWithBounds = opts.scaleWithBounds ?? false;
9104
+ const align = opts.align ?? "center";
9105
+ const verticalAlign = opts.verticalAlign ?? "center";
9106
+ const offsetX = opts.offsetX ?? 0;
9107
+ const offsetY = opts.offsetY ?? 0;
9108
+ const innerBounds = {
9109
+ x: bounds.x + margin,
9110
+ y: bounds.y + margin,
9111
+ width: Math.max(0, bounds.width - margin * 2),
9112
+ height: Math.max(0, bounds.height - margin * 2)
9113
+ };
9114
+ const availableWidth = Math.max(0, innerBounds.width - padding * 2);
9115
+ const availableHeight = Math.max(0, innerBounds.height - padding * 2);
9116
+ let drawWidth = opts.width ?? imageSize.width;
9117
+ let drawHeight = opts.height ?? imageSize.height;
9118
+ if (scaleWithBounds) {
9119
+ if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
9120
+ const scaleX = availableWidth / imageSize.width;
9121
+ const scaleY = availableHeight / imageSize.height;
9122
+ const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
9123
+ drawWidth = imageSize.width * scale;
9124
+ drawHeight = imageSize.height * scale;
9125
+ } else if (fit === "stretch") {
9126
+ drawWidth = availableWidth;
9127
+ drawHeight = availableHeight;
9128
+ }
9129
+ }
9130
+ drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
9131
+ drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
9132
+ let x = innerBounds.x + padding;
9133
+ let y = innerBounds.y + padding;
9134
+ if (align === "center") {
9135
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
9136
+ } else if (align === "right") {
9137
+ x = innerBounds.x + innerBounds.width - drawWidth - padding;
9138
+ }
9139
+ if (verticalAlign === "center") {
9140
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
9141
+ } else if (verticalAlign === "bottom") {
9142
+ y = innerBounds.y + innerBounds.height - drawHeight - padding;
9143
+ }
9144
+ return {
9145
+ x: x + offsetX,
9146
+ y: y + offsetY,
9147
+ width: drawWidth,
9148
+ height: drawHeight
9149
+ };
9150
+ }
9151
+ resolveIconHref(opts) {
9152
+ const source = opts.source;
9153
+ if (source instanceof HTMLImageElement) {
9154
+ return source.src;
9155
+ }
9156
+ if (!source) {
9157
+ return "";
9158
+ }
9159
+ if (this.isSvgMarkup(source)) {
9160
+ return this.svgToDataUrl(this.tintSvg(source, opts.strokeColor, opts.fillColor));
9161
+ }
9162
+ const shouldInlineSvg = source.toLowerCase().endsWith(".svg");
9163
+ if (shouldInlineSvg) {
9164
+ const svgText = this.readSvgFromUrlSync(source);
9165
+ if (svgText) {
9166
+ return this.svgToDataUrl(this.tintSvg(svgText, opts.strokeColor, opts.fillColor));
9167
+ }
9168
+ }
9169
+ return source;
9170
+ }
9171
+ readSvgFromUrlSync(url) {
9172
+ if (typeof XMLHttpRequest === "undefined") {
9173
+ return null;
9174
+ }
9175
+ try {
9176
+ const xhr = new XMLHttpRequest();
9177
+ xhr.open("GET", url, false);
9178
+ xhr.send();
9179
+ if (xhr.status >= 200 && xhr.status < 300) {
9180
+ return xhr.responseText || null;
9181
+ }
9182
+ } catch {
9183
+ }
9184
+ return null;
9185
+ }
9186
+ isSvgMarkup(value) {
9187
+ const trimmed = value.trim().toLowerCase();
9188
+ return trimmed.startsWith("<svg") || trimmed.includes("<svg");
9189
+ }
9190
+ styleSetColor(style, key, color) {
9191
+ const hasKey = new RegExp(`${key}\\s*:`).test(style);
9192
+ if (hasKey) {
9193
+ return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
9194
+ }
9195
+ const suffix = style.trim().endsWith(";") || style.trim() === "" ? "" : ";";
9196
+ return `${style}${suffix}${key}:${color};`;
9197
+ }
9198
+ tintSvg(svgText, strokeColor, fillColor) {
9199
+ if (!strokeColor && !fillColor) return svgText;
9200
+ const parser = new DOMParser();
9201
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
9202
+ const root = doc.documentElement;
9203
+ if (!root || root.nodeName.toLowerCase() === "parsererror") {
9204
+ return svgText;
9205
+ }
9206
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
9207
+ for (const el of all) {
9208
+ const stroke = el.getAttribute("stroke");
9209
+ if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
9210
+ el.setAttribute("stroke", strokeColor);
9211
+ }
9212
+ const fill = el.getAttribute("fill");
9213
+ if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
9214
+ el.setAttribute("fill", fillColor);
9215
+ }
9216
+ const style = el.getAttribute("style");
9217
+ if (style) {
9218
+ let next = style;
9219
+ if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) {
9220
+ next = this.styleSetColor(next, "stroke", strokeColor);
9221
+ }
9222
+ if (fillColor && /fill\s*:\s*(?!none)/.test(style)) {
9223
+ next = this.styleSetColor(next, "fill", fillColor);
9224
+ }
9225
+ if (next !== style) {
9226
+ el.setAttribute("style", next);
9227
+ }
9228
+ }
9229
+ }
9230
+ return new XMLSerializer().serializeToString(root);
9231
+ }
9232
+ svgToDataUrl(svg) {
9233
+ const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
9234
+ return `data:image/svg+xml;utf8,${encoded}`;
9235
+ }
9236
+ escapeAttribute(value) {
9237
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8904
9238
  }
8905
9239
  createEmptySvg(width, height, backgroundColor, includeBackground) {
8906
9240
  return [