@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 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 = 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="${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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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}">`,