@vectojs/core 0.2.2 → 0.2.4

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.
@@ -185,9 +185,100 @@ var CanvasRenderer = class _CanvasRenderer {
185
185
  }
186
186
  return grad;
187
187
  }
188
+ /**
189
+ * Canvas2D drawing contexts are automatically released when their
190
+ * `<canvas>` element is GC'd, so there's no explicit GPU handle to free.
191
+ * This method clears our internal batch state and is idempotent.
192
+ */
193
+ dispose() {
194
+ this.batchCount = 0;
195
+ this.batchColor = "";
196
+ this.batchAlpha = 1;
197
+ this.batchActive = false;
198
+ }
188
199
  };
189
200
 
201
+ // src/renderer/url.ts
202
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:"]);
203
+ var SCHEME_PATTERN = /^[a-z][a-z\d+.-]*$/i;
204
+ function normalizeSchemeCandidate(value) {
205
+ let result = "";
206
+ for (const character of value) {
207
+ const code = character.charCodeAt(0);
208
+ if (code > 32 && (code < 127 || code > 159)) result += character;
209
+ }
210
+ return result;
211
+ }
212
+ function hasSafeSchemeOrIsRelative(url) {
213
+ const colonIndex = url.indexOf(":");
214
+ if (colonIndex < 0) return true;
215
+ const candidate = normalizeSchemeCandidate(url.slice(0, colonIndex));
216
+ if (!SCHEME_PATTERN.test(candidate)) return true;
217
+ return SAFE_SCHEMES.has(`${candidate.toLowerCase()}:`);
218
+ }
219
+ function sanitizeUrl(href) {
220
+ if (typeof href !== "string") return "";
221
+ const trimmed = href.trim();
222
+ if (trimmed === "") return "";
223
+ return hasSafeSchemeOrIsRelative(trimmed) ? trimmed : "#";
224
+ }
225
+ function isSafeUrl(urlStr) {
226
+ if (typeof urlStr !== "string") return false;
227
+ const trimmed = urlStr.trim();
228
+ if (trimmed === "") return false;
229
+ return hasSafeSchemeOrIsRelative(trimmed);
230
+ }
231
+
190
232
  // src/renderer/SVGRenderer.ts
233
+ function parseFontSizeToken(font) {
234
+ for (let i = 0; i < font.length; i++) {
235
+ const ch = font[i];
236
+ if (!(ch >= "0" && ch <= "9" || ch === ".")) continue;
237
+ let j = i + 1;
238
+ while (j < font.length) {
239
+ const next = font[j];
240
+ if (next >= "0" && next <= "9" || next === ".") {
241
+ j++;
242
+ } else {
243
+ break;
244
+ }
245
+ }
246
+ const unit = font.startsWith("rem", j) ? "rem" : font.startsWith("em", j) ? "em" : font.startsWith("px", j) ? "px" : null;
247
+ if (!unit) {
248
+ i = j;
249
+ continue;
250
+ }
251
+ let end = j + unit.length;
252
+ if (font[end] === "/") {
253
+ end++;
254
+ while (end < font.length && font[end] !== " " && font[end] !== " ") end++;
255
+ }
256
+ const value = Number.parseFloat(font.slice(i, j));
257
+ return Number.isFinite(value) ? { value, unit, start: i, end } : null;
258
+ }
259
+ return null;
260
+ }
261
+ function fontFamilyFromShorthand(font, sizeToken) {
262
+ const withoutSize = sizeToken ? `${font.slice(0, sizeToken.start)} ${font.slice(sizeToken.end)}`.trim() : font.trim();
263
+ const family = withoutSize.split(" ").map((part) => part.trim()).filter(
264
+ (part) => part && ![
265
+ "bold",
266
+ "italic",
267
+ "oblique",
268
+ "normal",
269
+ "900",
270
+ "800",
271
+ "700",
272
+ "600",
273
+ "500",
274
+ "400",
275
+ "300",
276
+ "200",
277
+ "100"
278
+ ].includes(part.toLowerCase())
279
+ ).join(" ");
280
+ return family || "sans-serif";
281
+ }
191
282
  var SVGRenderer = class {
192
283
  width;
193
284
  height;
@@ -395,7 +486,7 @@ var SVGRenderer = class {
395
486
  }
396
487
  fill(colorOrGradient) {
397
488
  this.flush();
398
- const fillVal = this.resolveGradient(colorOrGradient);
489
+ const fillVal = this.escapeXML(this.resolveGradient(colorOrGradient));
399
490
  const dStr = this.currentPath.join(" ");
400
491
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
401
492
  this.buffer.push(
@@ -404,7 +495,7 @@ var SVGRenderer = class {
404
495
  }
405
496
  stroke(colorOrGradient, lineWidth = 1) {
406
497
  this.flush();
407
- const strokeVal = this.resolveGradient(colorOrGradient);
498
+ const strokeVal = this.escapeXML(this.resolveGradient(colorOrGradient));
408
499
  const dStr = this.currentPath.join(" ");
409
500
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
410
501
  this.buffer.push(
@@ -413,18 +504,18 @@ var SVGRenderer = class {
413
504
  }
414
505
  fillText(text, x, y, font, color) {
415
506
  this.flush();
416
- const sizeMatch = font.match(/(\d+(?:\.\d+)?)(px|em|rem)/);
417
- let fontSize = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
418
- if (sizeMatch && sizeMatch[2] !== "px") {
507
+ const sizeToken = parseFontSizeToken(font);
508
+ let fontSize = sizeToken ? sizeToken.value : 16;
509
+ if (sizeToken && sizeToken.unit !== "px") {
419
510
  fontSize = fontSize * 16;
420
511
  }
421
- const styleMatch = font.match(/(italic|oblique)/i);
422
- const fontWeightMatch = font.match(/(bold|[1-9]00)/i);
423
- const fontStyle = styleMatch ? styleMatch[1].toLowerCase() : "normal";
424
- const fontWeight = fontWeightMatch ? fontWeightMatch[1].toLowerCase() : "normal";
425
- const cleanFont = font.replace(/\d+(?:\.\d+)?(px|em|rem)(?:\/\d+(?:\.\d+)?(?:px|em|rem|%)?)?/, "").trim();
426
- const fontFamily = cleanFont.replace(/(bold|italic|normal|600|500|400|300|100)\s+/gi, "").trim() || "sans-serif";
427
- const fillVal = this.resolveGradient(color);
512
+ const lowerFont = font.toLowerCase();
513
+ const fontStyle = lowerFont.includes("italic") ? "italic" : lowerFont.includes("oblique") ? "oblique" : "normal";
514
+ const fontWeight = lowerFont.includes("bold") ? "bold" : ["900", "800", "700", "600", "500", "400", "300", "200", "100"].find(
515
+ (weight) => lowerFont.includes(weight)
516
+ ) ?? "normal";
517
+ const fontFamily = fontFamilyFromShorthand(font, sizeToken);
518
+ const fillVal = this.escapeXML(this.resolveGradient(color));
428
519
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
429
520
  this.buffer.push(
430
521
  `<g transform="${transformStr}"><text x="${x}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" font-family="${this.escapeXML(fontFamily)}" fill="${fillVal}" opacity="${this.globalAlpha}">${this.escapeXML(text)}</text></g>`
@@ -446,7 +537,9 @@ var SVGRenderer = class {
446
537
  }
447
538
  drawImage(source, dx, dy, dw, dh) {
448
539
  this.flush();
449
- const href = typeof source?.toDataURL === "function" ? source.toDataURL() : source?.src || "";
540
+ const fromCanvas2 = typeof source?.toDataURL === "function";
541
+ const rawHref = fromCanvas2 ? source.toDataURL() : source?.src || "";
542
+ const href = fromCanvas2 && this.isSafeRasterDataUrl(rawHref) ? rawHref : sanitizeUrl(String(rawHref));
450
543
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
451
544
  if (href) {
452
545
  this.buffer.push(
@@ -459,6 +552,21 @@ var SVGRenderer = class {
459
552
  console.warn("drawImage source fallback triggered");
460
553
  }
461
554
  }
555
+ /**
556
+ * Embed SVG markup as an isolated nested image.
557
+ *
558
+ * This explicit path is used by `SVGEntity` during vector export. General
559
+ * `drawImage()` input remains subject to the URL policy and therefore still
560
+ * rejects caller-provided SVG data URLs.
561
+ */
562
+ drawSVG(source, dx, dy, dw, dh) {
563
+ this.flush();
564
+ const href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
565
+ const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
566
+ this.buffer.push(
567
+ `<image href="${this.escapeXML(href)}" x="${dx}" y="${dy}" width="${dw}" height="${dh}" transform="${transformStr}" />`
568
+ );
569
+ }
462
570
  flush() {
463
571
  if (!this.batchActive || this.batchCircles.length === 0) return;
464
572
  let d = "";
@@ -566,6 +674,17 @@ var SVGRenderer = class {
566
674
  }
567
675
  });
568
676
  }
677
+ isSafeRasterDataUrl(value) {
678
+ return typeof value === "string" && /^data:image\/(?:png|jpeg|gif|webp);base64,[a-z\d+/=\s]*$/i.test(value);
679
+ }
680
+ /**
681
+ * SVGRenderer accumulates strings in memory; nothing external is allocated.
682
+ * Drop the buffers for GC and become idempotent.
683
+ */
684
+ dispose() {
685
+ this.clear();
686
+ this.gradientCache.clear();
687
+ }
569
688
  };
570
689
 
571
690
  // src/renderer/colorParse.ts
@@ -831,6 +950,7 @@ function createWebGLPointRenderer(canvas) {
831
950
  let logicalW = 0;
832
951
  let logicalH = 0;
833
952
  let dpr = 1;
953
+ let destroyed = false;
834
954
  return {
835
955
  resize(width, height) {
836
956
  logicalW = width;
@@ -1037,6 +1157,8 @@ function createWebGLPointRenderer(canvas) {
1037
1157
  gl.bindVertexArray(null);
1038
1158
  },
1039
1159
  destroy() {
1160
+ if (destroyed) return;
1161
+ destroyed = true;
1040
1162
  gl.deleteBuffer(pointBuffer);
1041
1163
  gl.deleteBuffer(rectBuffer);
1042
1164
  gl.deleteBuffer(spriteBuffer);
@@ -1326,10 +1448,14 @@ var WebGPUParticleSystemManager = class {
1326
1448
  if (!this.computePipeline || !entity.computeBindGroup) return;
1327
1449
  const uniformArray = new Float32Array(20);
1328
1450
  const color = parseColorToRGBA(entity.baseColor);
1451
+ let opacity = 1;
1452
+ for (let current = entity; current; current = current.parent) {
1453
+ opacity *= current.opacity;
1454
+ }
1329
1455
  uniformArray[0] = color[0];
1330
1456
  uniformArray[1] = color[1];
1331
1457
  uniformArray[2] = color[2];
1332
- uniformArray[3] = color[3];
1458
+ uniformArray[3] = color[3] * opacity;
1333
1459
  const mActive = !isNaN(mouseX) && !isNaN(mouseY) && mouseX > -9e3 && mouseY > -9e3;
1334
1460
  uniformArray[4] = mActive ? mouseX : -9999;
1335
1461
  uniformArray[5] = mActive ? mouseY : -9999;
@@ -1373,6 +1499,8 @@ var WebGPUParticleSystemManager = class {
1373
1499
 
1374
1500
  export {
1375
1501
  CanvasRenderer,
1502
+ sanitizeUrl,
1503
+ isSafeUrl,
1376
1504
  SVGRenderer,
1377
1505
  parseColorToRGBA,
1378
1506
  createWebGLPointRenderer,
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './renderer/SVGRenderer';
4
4
  export * from './renderer/WebGLPointRenderer';
5
5
  export * from './renderer/WebGPUParticleSystemManager';
6
6
  export * from './renderer/colorParse';
7
+ export * from './renderer/url';
7
8
  export * from './tree/Entity';
8
9
  export * from './tree/Scene';
9
10
  export * from './components/TextEntity';