@weasel-js/svg 1.7.0 → 1.7.1

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, SCRIPT_METRICS, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, resolveMarkerSize, getPaintKind } from '@weasel-js/core';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, PathBuilder, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, resolveRunFace, parseColor, rgbaToHex, resolveMarkerSize, getPaintKind } from '@weasel-js/core';
2
2
  import { identity, multiply, invert } from '@weasel-js/geom';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
@@ -145,16 +145,16 @@ function rectToPolygon(r) {
145
145
  b.close();
146
146
  return b.build();
147
147
  }
148
- function rectElementToPath(x, y, width, height, rx, ry, m = IDENTITY_MATRIX) {
148
+ function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
149
149
  if (rx <= 0 && ry <= 0) {
150
- return transformPath({ kind: "rect", x, y, width, height }, m);
150
+ return transformPath({ kind: "rect", x, y, width: width2, height: height2 }, m);
151
151
  }
152
- const r = Math.min(Math.max(rx, ry), Math.min(width, height) / 2);
153
- const r2 = Math.min(rx > 0 ? rx : r, ry > 0 ? ry : r, Math.min(width, height) / 2);
152
+ const r = Math.min(Math.max(rx, ry), Math.min(width2, height2) / 2);
153
+ const r2 = Math.min(rx > 0 ? rx : r, ry > 0 ? ry : r, Math.min(width2, height2) / 2);
154
154
  const rad = r2;
155
155
  const k = KAPPA * rad;
156
- const x2 = x + width;
157
- const y2 = y + height;
156
+ const x2 = x + width2;
157
+ const y2 = y + height2;
158
158
  const b = new PathBuilder();
159
159
  b.moveTo(x + rad, y);
160
160
  b.lineTo(x2 - rad, y);
@@ -217,65 +217,12 @@ function polygonToPath(points, m = IDENTITY_MATRIX) {
217
217
  b.close();
218
218
  return transformPath(b.build(), m);
219
219
  }
220
- function anchorOffset(style, width) {
220
+ function anchorOffset(style, width2) {
221
221
  const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
222
- return edge === "center" ? width / 2 : edge === "right" ? width : 0;
223
- }
224
- function parsePaintAttr(raw) {
225
- if (raw == null) return null;
226
- const s = raw.trim();
227
- if (s === "") return null;
228
- if (s === "none") return { kind: "none" };
229
- if (s === "currentColor" || s === "currentcolor") {
230
- return { kind: "solid", color: "#000000", alpha: 1 };
231
- }
232
- const urlMatch = /^url\(\s*#([^)\s]+)\s*\)(.*)$/.exec(s);
233
- if (urlMatch) {
234
- const out = { kind: "ref", id: urlMatch[1] };
235
- const rest = urlMatch[2].trim();
236
- if (rest !== "" && rest !== "none") {
237
- const fallback = parsePaintAttr(rest);
238
- if (fallback?.kind === "solid") {
239
- out.fallback = { color: fallback.color, alpha: fallback.alpha };
240
- }
241
- }
242
- return out;
243
- }
244
- try {
245
- const [r, g, b, a] = parseColor(s);
246
- return { kind: "solid", color: rgbaToHex([r, g, b]), alpha: a };
247
- } catch {
248
- return null;
249
- }
222
+ return edge === "center" ? width2 / 2 : edge === "right" ? width2 : 0;
250
223
  }
251
224
 
252
- // src/cascade.ts
253
- var EMPTY_STYLE = {};
254
- var INHERITABLE = [
255
- "fill",
256
- "fill-opacity",
257
- "fill-rule",
258
- "stroke",
259
- "stroke-width",
260
- "stroke-opacity",
261
- "stroke-linecap",
262
- "stroke-linejoin",
263
- "stroke-dasharray",
264
- "stroke-miterlimit",
265
- "marker-start",
266
- "marker-mid",
267
- "marker-end",
268
- "color",
269
- "font-size",
270
- "font-family",
271
- "font-weight",
272
- "font-style",
273
- "text-anchor",
274
- "letter-spacing",
275
- "text-decoration",
276
- "direction",
277
- "text-transform"
278
- ];
225
+ // src/cssScan.ts
279
226
  function skipString(s, i) {
280
227
  const q = s[i];
281
228
  for (let j = i + 1; j < s.length; j++) {
@@ -350,26 +297,21 @@ var IMPORTANT = /!\s*important\s*$/i;
350
297
  function parseDeclarations(block) {
351
298
  const out = [];
352
299
  for (const part of splitTopLevel(stripComments(block), ";", true)) {
353
- const colon = part.indexOf(":");
354
- if (colon < 0) continue;
355
- const prop = part.slice(0, colon).trim().toLowerCase();
356
- if (!PROP_NAME.test(prop)) continue;
357
- let value = part.slice(colon + 1).trim();
358
- const important = IMPORTANT.test(value);
359
- if (important) value = value.replace(IMPORTANT, "").trim();
360
- if (!value) continue;
361
- out.push({ prop, value, important });
300
+ const d = parseDeclaration(part);
301
+ if (d) out.push(d);
362
302
  }
363
303
  return out;
364
304
  }
365
- function identEnd(s, i) {
366
- while (i < s.length) {
367
- const ch = s[i];
368
- if (ch === "\\") i += 2;
369
- else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
370
- else break;
371
- }
372
- return i;
305
+ function parseDeclaration(text) {
306
+ const colon = text.indexOf(":");
307
+ if (colon < 0) return null;
308
+ const prop = text.slice(0, colon).trim().toLowerCase();
309
+ if (!PROP_NAME.test(prop)) return null;
310
+ let value = text.slice(colon + 1).trim();
311
+ const important = IMPORTANT.test(value);
312
+ if (important) value = value.replace(IMPORTANT, "").trim();
313
+ if (!value) return null;
314
+ return { prop, value, important };
373
315
  }
374
316
  function parenEnd(s, i) {
375
317
  let depth = 0;
@@ -385,6 +327,561 @@ function parenEnd(s, i) {
385
327
  }
386
328
  return s.length;
387
329
  }
330
+
331
+ // src/condition.ts
332
+ var ConditionSyntaxError = class extends Error {
333
+ };
334
+ var triNot = (a) => a == null ? null : !a;
335
+ var triAnd = (a, b) => a === false || b === false ? false : a == null || b == null ? null : true;
336
+ var triOr = (a, b) => a === true || b === true ? true : a == null || b == null ? null : false;
337
+ var WS = /\s/;
338
+ var IDENT = /^-?[a-zA-Z_][\w-]*/;
339
+ function skipWs(p) {
340
+ while (p.i < p.s.length && WS.test(p.s[p.i])) p.i++;
341
+ }
342
+ function keyword(p, word) {
343
+ const end = p.i + word.length;
344
+ if (p.s.slice(p.i, end).toLowerCase() !== word) return false;
345
+ if (end >= p.s.length || !WS.test(p.s[end])) return false;
346
+ p.i = end;
347
+ return true;
348
+ }
349
+ function evaluateCondition(s, leaves, allowOr = true) {
350
+ const p = { s, i: 0 };
351
+ const r = condition(p, leaves, allowOr);
352
+ skipWs(p);
353
+ if (p.i < s.length) throw new ConditionSyntaxError(`unexpected "${s.slice(p.i)}"`);
354
+ return r;
355
+ }
356
+ function condition(p, leaves, allowOr) {
357
+ skipWs(p);
358
+ if (keyword(p, "not")) return triNot(inParens(p, leaves));
359
+ let r = inParens(p, leaves);
360
+ let op = null;
361
+ for (; ; ) {
362
+ const save = p.i;
363
+ skipWs(p);
364
+ if (p.i >= p.s.length) {
365
+ p.i = save;
366
+ return r;
367
+ }
368
+ const next = keyword(p, "and") ? "and" : keyword(p, "or") ? "or" : null;
369
+ if (!next || op && next !== op || next === "or" && !allowOr) {
370
+ throw new ConditionSyntaxError(`unexpected "${p.s.slice(p.i)}"`);
371
+ }
372
+ op = next;
373
+ const rhs = inParens(p, leaves);
374
+ r = op === "and" ? triAnd(r, rhs) : triOr(r, rhs);
375
+ }
376
+ }
377
+ function looksLikeCondition(inner) {
378
+ const t = inner.trimStart();
379
+ return t.startsWith("(") || /^not\s/i.test(t) || /^-?[a-zA-Z_][\w-]*\(/.test(t);
380
+ }
381
+ function inParens(p, leaves) {
382
+ skipWs(p);
383
+ const start = p.i;
384
+ let name = null;
385
+ if (p.s[p.i] !== "(") {
386
+ const m = IDENT.exec(p.s.slice(p.i));
387
+ if (!m || p.s[p.i + m[0].length] !== "(") throw new ConditionSyntaxError(`expected "(" at "${p.s.slice(p.i)}"`);
388
+ name = m[0].toLowerCase();
389
+ p.i += m[0].length;
390
+ }
391
+ const end = parenEnd(p.s, p.i);
392
+ if (p.s[end - 1] !== ")") throw new ConditionSyntaxError(`unbalanced "${p.s.slice(start)}"`);
393
+ const inner = p.s.slice(p.i + 1, end - 1);
394
+ p.i = end;
395
+ if (name != null) return leaves.fn(name, inner);
396
+ if (looksLikeCondition(inner)) {
397
+ try {
398
+ return evaluateCondition(inner, leaves);
399
+ } catch (e) {
400
+ if (!(e instanceof ConditionSyntaxError)) throw e;
401
+ }
402
+ }
403
+ return leaves.paren(inner);
404
+ }
405
+
406
+ // src/media.ts
407
+ var DEFAULT_MEDIA_ENVIRONMENT = {
408
+ type: "screen",
409
+ width: 300,
410
+ height: 150,
411
+ resolution: 1,
412
+ prefersColorScheme: "light",
413
+ prefersReducedMotion: "no-preference",
414
+ hover: "none",
415
+ pointer: "none",
416
+ update: "none",
417
+ color: 8,
418
+ monochrome: 0
419
+ };
420
+ var ABSOLUTE_PX = {
421
+ px: 1,
422
+ in: 96,
423
+ cm: 96 / 2.54,
424
+ mm: 96 / 25.4,
425
+ q: 96 / 101.6,
426
+ pt: 96 / 72,
427
+ pc: 16
428
+ };
429
+ function absoluteLength(raw) {
430
+ const m = raw == null ? null : /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]*)\s*$/i.exec(raw);
431
+ if (!m) return void 0;
432
+ const scale = ABSOLUTE_PX[m[2].toLowerCase() || "px"];
433
+ const n = parseFloat(m[1]) * (scale ?? NaN);
434
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
435
+ }
436
+ function mediaEnvironmentFor(root, overrides = {}) {
437
+ let width2 = absoluteLength(root.getAttribute("width"));
438
+ let height2 = absoluteLength(root.getAttribute("height"));
439
+ const vb = (root.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/).map(Number);
440
+ if (vb.length === 4 && vb.every(Number.isFinite) && vb[2] > 0 && vb[3] > 0) {
441
+ const [, , w, h] = vb;
442
+ if (width2 == null && height2 == null) {
443
+ width2 = w;
444
+ height2 = h;
445
+ } else if (width2 == null) width2 = height2 * (w / h);
446
+ else if (height2 == null) height2 = width2 * (h / w);
447
+ }
448
+ return {
449
+ ...DEFAULT_MEDIA_ENVIRONMENT,
450
+ width: width2 ?? DEFAULT_MEDIA_ENVIRONMENT.width,
451
+ height: height2 ?? DEFAULT_MEDIA_ENVIRONMENT.height,
452
+ ...overrides
453
+ };
454
+ }
455
+ var TOKEN = /\s*(?:([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)|(-?[a-z_][\w-]*)|(<=|>=|<|>|=)|(:)|(\/))/iy;
456
+ function tokenize(s) {
457
+ const out = [];
458
+ TOKEN.lastIndex = 0;
459
+ while (TOKEN.lastIndex < s.length) {
460
+ if (/^\s*$/.test(s.slice(TOKEN.lastIndex))) break;
461
+ const m = TOKEN.exec(s);
462
+ if (!m) return null;
463
+ if (m[1] != null) out.push({ k: "num", v: parseFloat(m[1]), unit: m[2].toLowerCase() });
464
+ else if (m[3] != null) out.push({ k: "ident", v: m[3].toLowerCase() });
465
+ else if (m[4] != null) out.push({ k: "op", v: m[4] });
466
+ else if (m[5] != null) out.push({ k: ":" });
467
+ else out.push({ k: "/" });
468
+ }
469
+ return out;
470
+ }
471
+ var width = (e) => e.width;
472
+ var height = (e) => e.height;
473
+ var aspect = (e) => e.width / e.height;
474
+ var hover = { kind: "discrete", values: ["none", "hover"], off: "none", get: (e) => e.hover };
475
+ var pointer = { kind: "discrete", values: ["none", "coarse", "fine"], off: "none", get: (e) => e.pointer };
476
+ var FEATURES = {
477
+ "width": { kind: "range", type: "length", get: width },
478
+ "height": { kind: "range", type: "length", get: height },
479
+ "device-width": { kind: "range", type: "length", get: width },
480
+ "device-height": { kind: "range", type: "length", get: height },
481
+ "aspect-ratio": { kind: "range", type: "ratio", get: aspect },
482
+ "device-aspect-ratio": { kind: "range", type: "ratio", get: aspect },
483
+ "resolution": { kind: "range", type: "resolution", get: (e) => e.resolution },
484
+ "color": { kind: "range", type: "integer", get: (e) => e.color },
485
+ "monochrome": { kind: "range", type: "integer", get: (e) => e.monochrome },
486
+ "orientation": {
487
+ kind: "discrete",
488
+ values: ["portrait", "landscape"],
489
+ get: (e) => e.height >= e.width ? "portrait" : "landscape"
490
+ },
491
+ "prefers-color-scheme": { kind: "discrete", values: ["light", "dark"], get: (e) => e.prefersColorScheme },
492
+ "prefers-reduced-motion": {
493
+ kind: "discrete",
494
+ values: ["no-preference", "reduce"],
495
+ off: "no-preference",
496
+ get: (e) => e.prefersReducedMotion
497
+ },
498
+ "update": { kind: "discrete", values: ["none", "slow", "fast"], off: "none", get: (e) => e.update },
499
+ "hover": hover,
500
+ "any-hover": hover,
501
+ "pointer": pointer,
502
+ "any-pointer": pointer
503
+ };
504
+ var FONT_PX = 16;
505
+ function lengthPx(v, unit, env) {
506
+ if (unit === "") return v === 0 ? 0 : null;
507
+ const abs = ABSOLUTE_PX[unit];
508
+ if (abs != null) return v * abs;
509
+ switch (unit) {
510
+ case "em":
511
+ case "rem":
512
+ return v * FONT_PX;
513
+ case "ex":
514
+ case "ch":
515
+ return v * FONT_PX / 2;
516
+ case "vw":
517
+ return v * env.width / 100;
518
+ case "vh":
519
+ return v * env.height / 100;
520
+ case "vmin":
521
+ return v * Math.min(env.width, env.height) / 100;
522
+ case "vmax":
523
+ return v * Math.max(env.width, env.height) / 100;
524
+ default:
525
+ return null;
526
+ }
527
+ }
528
+ var DPPX = { dppx: 1, x: 1, dpi: 1 / 96, dpcm: 2.54 / 96 };
529
+ function rangeValue(toks, type, env) {
530
+ const [a, b, c] = toks;
531
+ if (type === "ratio") {
532
+ if (toks.length === 1 && a.k === "num" && a.unit === "" && a.v >= 0) return a.v;
533
+ if (toks.length === 3 && a.k === "num" && b.k === "/" && c.k === "num" && a.unit === "" && c.unit === "" && a.v >= 0 && c.v > 0) return a.v / c.v;
534
+ return null;
535
+ }
536
+ if (toks.length !== 1 || a.k !== "num") return null;
537
+ if (type === "length") return lengthPx(a.v, a.unit, env);
538
+ if (type === "resolution") return DPPX[a.unit] != null ? a.v * DPPX[a.unit] : null;
539
+ return a.unit === "" && Number.isInteger(a.v) ? a.v : null;
540
+ }
541
+ function close(a, b) {
542
+ return Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
543
+ }
544
+ function compare(a, op, b) {
545
+ switch (op) {
546
+ case "=":
547
+ return close(a, b);
548
+ case "<":
549
+ return a < b && !close(a, b);
550
+ case "<=":
551
+ return a < b || close(a, b);
552
+ case ">":
553
+ return a > b && !close(a, b);
554
+ default:
555
+ return a > b || close(a, b);
556
+ }
557
+ }
558
+ var FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=", "=": "=" };
559
+ function evaluateFeature(inner, env) {
560
+ const toks = tokenize(inner);
561
+ if (!toks || toks.length === 0) return null;
562
+ const first = toks[0];
563
+ if (toks.length === 1) {
564
+ if (first.k !== "ident") return null;
565
+ const f = FEATURES[first.v];
566
+ if (!f) return null;
567
+ return f.kind === "range" ? f.get(env) !== 0 : f.get(env) !== f.off;
568
+ }
569
+ if (first.k === "ident" && toks[1].k === ":") {
570
+ const prefix = /^(min|max)-/.exec(first.v)?.[1];
571
+ const f = FEATURES[prefix ? first.v.slice(4) : first.v];
572
+ if (!f) return null;
573
+ const rest = toks.slice(2);
574
+ if (f.kind === "discrete") {
575
+ if (prefix || rest.length !== 1 || rest[0].k !== "ident" || !f.values.includes(rest[0].v)) return null;
576
+ return f.get(env) === rest[0].v;
577
+ }
578
+ const v = rangeValue(rest, f.type, env);
579
+ if (v == null) return null;
580
+ return compare(f.get(env), prefix === "min" ? ">=" : prefix === "max" ? "<=" : "=", v);
581
+ }
582
+ const segments = [[]];
583
+ const ops = [];
584
+ for (const t of toks) {
585
+ if (t.k === "op") {
586
+ ops.push(t.v);
587
+ segments.push([]);
588
+ } else segments[segments.length - 1].push(t);
589
+ }
590
+ if (segments.some((s) => s.length === 0)) return null;
591
+ const featureAt = (s) => s.length === 1 && s[0].k === "ident" ? FEATURES[s[0].v] : void 0;
592
+ if (ops.length === 1) {
593
+ const [lhs, rhs] = segments;
594
+ const named = featureAt(lhs) ?? featureAt(rhs);
595
+ if (!named) return null;
596
+ if (named.kind !== "range") return null;
597
+ const nameFirst = featureAt(lhs) != null;
598
+ const v = rangeValue(nameFirst ? rhs : lhs, named.type, env);
599
+ if (v == null) return null;
600
+ return nameFirst ? compare(named.get(env), ops[0], v) : compare(named.get(env), FLIP[ops[0]], v);
601
+ }
602
+ if (ops.length === 2) {
603
+ const f = featureAt(segments[1]);
604
+ if (!f || f.kind !== "range") return null;
605
+ const lt = ops.every((o) => o === "<" || o === "<=");
606
+ const gt = ops.every((o) => o === ">" || o === ">=");
607
+ if (!lt && !gt) return null;
608
+ const lo = rangeValue(segments[0], f.type, env);
609
+ const hi = rangeValue(segments[2], f.type, env);
610
+ if (lo == null || hi == null) return null;
611
+ const x = f.get(env);
612
+ return compare(lo, ops[0], x) && compare(x, ops[1], hi);
613
+ }
614
+ return null;
615
+ }
616
+ var RESERVED_TYPES = /* @__PURE__ */ new Set(["not", "and", "or", "only", "layer"]);
617
+ var TYPED_QUERY = /^(?:(not|only)\s+)?(-?[a-z_][\w-]*)(?:\s+and\s+([\s\S]+))?$/;
618
+ function evaluateQuery(query, env) {
619
+ const leaves = {
620
+ paren: (inner) => evaluateFeature(inner, env),
621
+ fn: () => null
622
+ };
623
+ if (/^(?:\(|not\s+\(|-?[a-z_][\w-]*\()/.test(query)) return evaluateCondition(query, leaves);
624
+ const m = TYPED_QUERY.exec(query);
625
+ if (!m || RESERVED_TYPES.has(m[2])) throw new ConditionSyntaxError(`invalid media query "${query}"`);
626
+ const typed = triAnd(m[2] === "all" || m[2] === env.type, m[3] ? evaluateCondition(m[3], leaves, false) : true);
627
+ return m[1] === "not" ? triNot(typed) : typed;
628
+ }
629
+ function evaluateMediaQuery(list, env) {
630
+ const src = list.trim().toLowerCase();
631
+ if (src === "") return true;
632
+ return splitTopLevel(src, ",").some((raw) => {
633
+ const query = raw.trim();
634
+ if (query === "") return false;
635
+ try {
636
+ return evaluateQuery(query, env) === true;
637
+ } catch (e) {
638
+ if (e instanceof ConditionSyntaxError) return false;
639
+ throw e;
640
+ }
641
+ });
642
+ }
643
+ function parsePaintAttr(raw) {
644
+ if (raw == null) return null;
645
+ const s = raw.trim();
646
+ if (s === "") return null;
647
+ if (s === "none") return { kind: "none" };
648
+ if (s.toLowerCase() === "currentcolor") {
649
+ return { kind: "solid", color: "#000000", alpha: 1 };
650
+ }
651
+ const urlMatch = /^url\(\s*#([^)\s]+)\s*\)(.*)$/.exec(s);
652
+ if (urlMatch) {
653
+ const out = { kind: "ref", id: urlMatch[1] };
654
+ const rest = urlMatch[2].trim();
655
+ if (rest !== "" && rest !== "none") {
656
+ const fallback = parsePaintAttr(rest);
657
+ if (fallback?.kind === "solid") {
658
+ out.fallback = { color: fallback.color, alpha: fallback.alpha };
659
+ }
660
+ }
661
+ return out;
662
+ }
663
+ try {
664
+ const [r, g, b, a] = parseColor(s);
665
+ return { kind: "solid", color: rgbaToHex([r, g, b]), alpha: a };
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+
671
+ // src/properties.ts
672
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
673
+ var DIMENSION = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
674
+ function dimension(v) {
675
+ const m = DIMENSION.exec(v);
676
+ return m ? { n: Number(m[1]), unit: m[2].toLowerCase() } : void 0;
677
+ }
678
+ var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
679
+ var length = (native, coercedAs) => (v, warn, prop) => {
680
+ const d = dimension(v);
681
+ if (!d) return void 0;
682
+ if (d.unit && !native.includes(d.unit)) {
683
+ warn(`${prop} "${v}" uses unit "${d.unit}", which is not converted; treated as ${d.n} ${coercedAs}`);
684
+ }
685
+ return d.n;
686
+ };
687
+ var worldLength = length(["px"], "world units");
688
+ var keyword2 = (...words) => (v) => words.includes(v) ? v : void 0;
689
+ var isCurrentColor = (v) => v.toLowerCase() === "currentcolor";
690
+ function readPaint(v) {
691
+ if (v === "context-fill") return { kind: "context", of: "fill" };
692
+ if (v === "context-stroke") return { kind: "context", of: "stroke" };
693
+ return parsePaintAttr(v) ?? void 0;
694
+ }
695
+ function readRatio(v) {
696
+ const d = dimension(v);
697
+ if (!d || d.unit !== "" && d.unit !== "%") return void 0;
698
+ return clamp01(d.unit === "%" ? d.n / 100 : d.n);
699
+ }
700
+ function readSolidColor(v, warn, prop) {
701
+ const parsed = parsePaintAttr(v);
702
+ if (parsed?.kind !== "solid") return void 0;
703
+ if (isCurrentColor(v)) warn(`${prop}="${v}" is read as black; it is not resolved against color`);
704
+ return parsed;
705
+ }
706
+ function readMarkerRef(v) {
707
+ if (v === "none") return null;
708
+ return /^url\(\s*#([^)\s]+)\s*\)$/.exec(v)?.[1];
709
+ }
710
+ function readLinecap(v, warn) {
711
+ const cap = keyword2("butt", "round", "square")(v);
712
+ if (!cap) warn(`unsupported stroke-linecap: ${v}`);
713
+ return cap;
714
+ }
715
+ function readLinejoin(v, warn) {
716
+ const join = keyword2("miter", "round", "bevel")(v);
717
+ if (join) return join;
718
+ if (v === "arcs" || v === "miter-clip") {
719
+ warn(`stroke-linejoin "${v}" not supported; falling back to miter`);
720
+ return "miter";
721
+ }
722
+ warn(`unsupported stroke-linejoin: ${v}`);
723
+ return void 0;
724
+ }
725
+ function readDashArray(v, warn) {
726
+ if (v === "none" || v === "") return null;
727
+ const nums = [];
728
+ for (const t of v.split(/[\s,]+/).filter(Boolean)) {
729
+ const d = dimension(t);
730
+ if (!d || d.unit !== "" && d.unit !== "px" || d.n < 0) {
731
+ warn(`unrecognized stroke-dasharray: ${v}`);
732
+ return void 0;
733
+ }
734
+ nums.push(d.n);
735
+ }
736
+ return nums.length % 2 === 1 ? [...nums, ...nums] : nums;
737
+ }
738
+ function readMiterLimit(v, warn) {
739
+ const d = dimension(v);
740
+ if (d && d.unit === "" && d.n >= 1) return d.n;
741
+ warn(`unrecognized stroke-miterlimit: ${v}`);
742
+ return void 0;
743
+ }
744
+ function readColor(v) {
745
+ return isCurrentColor(v) || parsePaintAttr(v)?.kind === "solid" ? v : void 0;
746
+ }
747
+ function readFontSize(v, warn, prop) {
748
+ const d = dimension(v);
749
+ if (d?.unit === "%") return { fontScale: d.n / 100 };
750
+ const n = worldLength(v, warn, prop);
751
+ return n == null ? void 0 : { fontSize: n };
752
+ }
753
+ function readFontWeight(v) {
754
+ if (v === "normal" || v === "bold" || v === "bolder") return v;
755
+ const d = dimension(v);
756
+ return d && d.unit === "" && d.n >= 1 && d.n <= 1e3 ? d.n : void 0;
757
+ }
758
+ function readFontStyle(v, warn) {
759
+ if (v === "normal" || v === "italic") return v;
760
+ if (v === "oblique") {
761
+ warn('font-style "oblique" is read as italic');
762
+ return "italic";
763
+ }
764
+ return void 0;
765
+ }
766
+ function readLetterSpacing(v, warn, prop) {
767
+ return v === "normal" ? null : worldLength(v, warn, prop);
768
+ }
769
+ function readDecoration(v, warn) {
770
+ const out = {};
771
+ for (const t of v.toLowerCase().split(/\s+/)) {
772
+ if (t === "underline") out.underline = true;
773
+ else if (t === "line-through") out.strikethrough = true;
774
+ else if (t === "overline") out.overline = true;
775
+ else if (t !== "none" && t !== "blink") warn(`text-decoration "${v}": "${t}" is not modeled; dropped`);
776
+ }
777
+ return out;
778
+ }
779
+ function readFontVariant(v, warn) {
780
+ let out;
781
+ for (const t of v.toLowerCase().split(/\s+/)) {
782
+ if (t === "small-caps" || t === "normal") out = t;
783
+ else if (t) warn(`font-variant "${v}": "${t}" is not modeled; dropped`);
784
+ }
785
+ return out;
786
+ }
787
+ function readTextTransform(v) {
788
+ const t = v.toLowerCase();
789
+ return TEXT_TRANSFORMS.has(t) ? t : void 0;
790
+ }
791
+ var emLength = length(["em"], "em");
792
+ function readBaselineShift(v, warn, prop) {
793
+ const k = v.toLowerCase();
794
+ if (k === "super" || k === "sub") return { script: k };
795
+ if (k === "baseline") return {};
796
+ const d = dimension(v);
797
+ if (d?.unit === "%") return d.n === 0 ? {} : { baselineShift: d.n / 100 };
798
+ const n = emLength(v, warn, prop);
799
+ if (n == null) return void 0;
800
+ return n === 0 ? {} : { baselineShift: n };
801
+ }
802
+ var inherited = (read) => ({ inherits: true, read });
803
+ var own = (read) => ({ inherits: false, read });
804
+ var PROPERTIES = {
805
+ "fill": inherited(readPaint),
806
+ "fill-opacity": inherited(readRatio),
807
+ "fill-rule": inherited(keyword2("nonzero", "evenodd")),
808
+ "stroke": inherited(readPaint),
809
+ "stroke-width": inherited(worldLength),
810
+ "stroke-opacity": inherited(readRatio),
811
+ "stroke-linecap": inherited(readLinecap),
812
+ "stroke-linejoin": inherited(readLinejoin),
813
+ "stroke-dasharray": inherited(readDashArray),
814
+ "stroke-miterlimit": inherited(readMiterLimit),
815
+ "marker-start": inherited(readMarkerRef),
816
+ "marker-mid": inherited(readMarkerRef),
817
+ "marker-end": inherited(readMarkerRef),
818
+ "color": inherited(readColor),
819
+ "font-size": inherited(readFontSize),
820
+ "font-family": inherited((v) => v.length > 0 ? v : void 0),
821
+ "font-weight": inherited(readFontWeight),
822
+ "font-style": inherited(readFontStyle),
823
+ "text-anchor": inherited(keyword2("start", "middle", "end")),
824
+ "letter-spacing": inherited(readLetterSpacing),
825
+ "text-decoration": inherited(readDecoration),
826
+ "direction": inherited(keyword2("ltr", "rtl")),
827
+ "text-transform": inherited(readTextTransform),
828
+ "font-variant": inherited(readFontVariant),
829
+ "opacity": own(readRatio),
830
+ "stop-color": own(readSolidColor),
831
+ "stop-opacity": own(readRatio),
832
+ "baseline-shift": own(readBaselineShift),
833
+ "vector-effect": own(keyword2("none", "non-scaling-stroke"))
834
+ };
835
+ var INHERITED_PROPERTIES = Object.keys(PROPERTIES).filter((p) => PROPERTIES[p].inherits);
836
+ var silent = () => {
837
+ };
838
+ function readProperty(prop, raw, warn = silent) {
839
+ if (raw == null) return void 0;
840
+ const v = raw.trim();
841
+ if (v === "inherit") return void 0;
842
+ return PROPERTIES[prop].read(v, warn, prop);
843
+ }
844
+ function isPropertyName(prop) {
845
+ return Object.prototype.hasOwnProperty.call(PROPERTIES, prop);
846
+ }
847
+ function isDeclarationHonored(prop, value) {
848
+ const name = prop.toLowerCase();
849
+ if (!isPropertyName(name)) return false;
850
+ const v = value.trim();
851
+ if (v === "inherit") return PROPERTIES[name].inherits;
852
+ let warned = false;
853
+ return readProperty(name, v, () => {
854
+ warned = true;
855
+ }) !== void 0 && !warned;
856
+ }
857
+
858
+ // src/supports.ts
859
+ function evaluateSupports(condition2, opts = {}) {
860
+ try {
861
+ return evaluateCondition(condition2, {
862
+ paren: (inner) => {
863
+ const d = parseDeclaration(inner);
864
+ return d != null && isDeclarationHonored(d.prop, d.value);
865
+ },
866
+ fn: (name, args) => name === "selector" && (opts.selector?.(args) ?? false)
867
+ }) === true;
868
+ } catch (e) {
869
+ if (e instanceof ConditionSyntaxError) return false;
870
+ throw e;
871
+ }
872
+ }
873
+
874
+ // src/cascade.ts
875
+ var EMPTY_STYLE = {};
876
+ function identEnd(s, i) {
877
+ while (i < s.length) {
878
+ const ch = s[i];
879
+ if (ch === "\\") i += 2;
880
+ else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
881
+ else break;
882
+ }
883
+ return i;
884
+ }
388
885
  var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
389
886
  var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
390
887
  function compareSpecificity(a, b) {
@@ -444,19 +941,76 @@ function specificity(selector) {
444
941
  }
445
942
  return [a, b, c];
446
943
  }
447
- function parseStylesheet(text, firstOrder = 0) {
944
+ var CascadeLayers = class {
945
+ children = /* @__PURE__ */ new Map();
946
+ anonymous = 0;
947
+ /** Declare `name` (possibly dotted) inside `parent`, keeping any earlier position; returns its full name. */
948
+ declare(parent, name) {
949
+ let full = parent;
950
+ for (const segment of name.split(".")) {
951
+ const child = full ? `${full}.${segment}` : segment;
952
+ const siblings = this.children.get(full) ?? [];
953
+ if (!siblings.includes(child)) {
954
+ siblings.push(child);
955
+ this.children.set(full, siblings);
956
+ }
957
+ full = child;
958
+ }
959
+ return full;
960
+ }
961
+ /** A fresh layer inside `parent` that nothing can reopen. */
962
+ declareAnonymous(parent) {
963
+ return this.declare(parent, `\0${this.anonymous++}`);
964
+ }
965
+ /**
966
+ * Each layer's rank for normal declarations, higher winning: a layer's
967
+ * sublayers rank below its own rules, in declaration order, and unlayered
968
+ * rules (`''`) rank above every layer.
969
+ */
970
+ ranks() {
971
+ const out = /* @__PURE__ */ new Map();
972
+ const visit = (name) => {
973
+ for (const child of this.children.get(name) ?? []) visit(child);
974
+ out.set(name, out.size);
975
+ };
976
+ visit("");
977
+ return out;
978
+ }
979
+ };
980
+ function parseStylesheet(text, firstOrder = 0, ctx = {}, layers = new CascadeLayers()) {
448
981
  const src = stripComments(text).replace(/<!\[CDATA\[|\]\]>|<!--|-->/g, " ");
449
982
  const rules = [];
450
- let order = firstOrder;
451
- let i = 0;
452
- let preludeStart = 0;
453
- while (i < src.length) {
983
+ parseRuleList(src, 0, src.length, "", ctx, rules, { order: firstOrder, importsAllowed: true, layers });
984
+ return rules;
985
+ }
986
+ var IDENT2 = "-?[a-zA-Z_\\u0080-\\uffff][\\w\\u0080-\\uffff-]*";
987
+ var LAYER_NAME = new RegExp(`^${IDENT2}(?:\\.${IDENT2})*$`);
988
+ function layerNames(prelude) {
989
+ if (!prelude.trim()) return [];
990
+ const names = splitTopLevel(prelude, ",").map((n) => n.trim());
991
+ return names.every((n) => LAYER_NAME.test(n)) ? names : null;
992
+ }
993
+ var AT_RULE = /^@([\w-]+)\s*([\s\S]*)$/;
994
+ function parseRuleList(src, from, to, layer, ctx, rules, state) {
995
+ let i = from;
996
+ let preludeStart = from;
997
+ while (i < to) {
454
998
  const ch = src[i];
455
999
  if (ch === '"' || ch === "'") {
456
1000
  i = skipString(src, i);
457
1001
  continue;
458
1002
  }
459
1003
  if (ch === ";") {
1004
+ const text = src.slice(preludeStart, i).trim();
1005
+ const statement = AT_RULE.exec(text);
1006
+ const name = statement?.[1].toLowerCase();
1007
+ if (name === "import" && state.importsAllowed) {
1008
+ ctx.onWarn?.(`@import ${statement?.[2]} is not fetched; its rules do not apply`);
1009
+ } else if (name === "layer") {
1010
+ for (const n of layerNames(statement?.[2] ?? "") ?? []) state.layers.declare(layer, n);
1011
+ } else if (text && name !== "charset") {
1012
+ state.importsAllowed = false;
1013
+ }
460
1014
  i++;
461
1015
  preludeStart = i;
462
1016
  continue;
@@ -466,41 +1020,77 @@ function parseStylesheet(text, firstOrder = 0) {
466
1020
  continue;
467
1021
  }
468
1022
  const prelude = src.slice(preludeStart, i).trim();
469
- const end = skipBlock(src, i);
470
- if (!prelude.startsWith("@") && prelude) {
1023
+ const end = Math.min(skipBlock(src, i), to);
1024
+ const at = AT_RULE.exec(prelude);
1025
+ state.importsAllowed = false;
1026
+ if (at) {
1027
+ const name = at[1].toLowerCase();
1028
+ const names = name === "layer" ? layerNames(at[2]) : null;
1029
+ if (names && names.length <= 1) {
1030
+ const inner = names.length ? state.layers.declare(layer, names[0]) : state.layers.declareAnonymous(layer);
1031
+ parseRuleList(src, i + 1, end - 1, inner, ctx, rules, state);
1032
+ } else if (conditionHolds(name, at[2], ctx)) {
1033
+ parseRuleList(src, i + 1, end - 1, layer, ctx, rules, state);
1034
+ }
1035
+ } else if (prelude) {
471
1036
  const declarations = parseDeclarations(src.slice(i + 1, end - 1));
472
1037
  for (const raw of splitTopLevel(prelude, ",")) {
473
1038
  const selector = raw.trim();
474
1039
  if (selector) {
475
- rules.push({ selector, declarations, specificity: specificity(selector), order: order++ });
1040
+ rules.push({ selector, declarations, specificity: specificity(selector), order: state.order++, layer });
476
1041
  }
477
1042
  }
478
1043
  }
479
1044
  i = end;
480
1045
  preludeStart = end;
481
1046
  }
482
- return rules;
483
1047
  }
484
- function appliesAsCss(style) {
1048
+ function conditionHolds(name, condition2, ctx) {
1049
+ if (name === "media") return evaluateMediaQuery(condition2, ctx.media ?? DEFAULT_MEDIA_ENVIRONMENT);
1050
+ if (name === "supports") return evaluateSupports(condition2, { selector: ctx.selector });
1051
+ return false;
1052
+ }
1053
+ function appliesAsCss(style, ctx) {
485
1054
  const type = style.getAttribute("type")?.trim().toLowerCase();
486
1055
  if (type && type !== "text/css") return false;
487
- const media = style.getAttribute("media")?.trim().toLowerCase();
488
- if (!media) return true;
489
- return media.split(",").some((m) => m.trim() === "all" || m.trim() === "screen");
1056
+ return evaluateMediaQuery(style.getAttribute("media") ?? "", ctx.media ?? DEFAULT_MEDIA_ENVIRONMENT);
490
1057
  }
491
1058
  var sheetCache = /* @__PURE__ */ new WeakMap();
1059
+ function bindStylesheets(doc, media, onWarn) {
1060
+ sheetCache.set(doc, collectRules(doc, media, onWarn));
1061
+ }
492
1062
  function documentRules(doc) {
493
1063
  const cached = sheetCache.get(doc);
494
1064
  if (cached) return cached;
1065
+ const root = doc.documentElement;
1066
+ const rules = collectRules(doc, root ? mediaEnvironmentFor(root) : DEFAULT_MEDIA_ENVIRONMENT);
1067
+ sheetCache.set(doc, rules);
1068
+ return rules;
1069
+ }
1070
+ function collectRules(doc, media, onWarn) {
1071
+ const ctx = { media, onWarn, selector: (s) => isValidSelector(doc, s) };
1072
+ const layers = new CascadeLayers();
495
1073
  const rules = [];
496
1074
  const styles = doc.getElementsByTagNameNS("*", "style");
497
1075
  for (let i = 0; i < styles.length; i++) {
498
- if (!appliesAsCss(styles[i])) continue;
499
- rules.push(...parseStylesheet(styles[i].textContent ?? "", rules.length));
1076
+ if (!appliesAsCss(styles[i], ctx)) continue;
1077
+ rules.push(...parseStylesheet(styles[i].textContent ?? "", rules.length, ctx, layers));
1078
+ }
1079
+ const ranks = layers.ranks();
1080
+ return rules.map((r) => ({ ...r, rank: ranks.get(r.layer) ?? 0 })).sort((x, y) => x.rank - y.rank || compareCascade(x, y));
1081
+ }
1082
+ function compareCascade(x, y) {
1083
+ return compareSpecificity(x.specificity, y.specificity) || x.order - y.order;
1084
+ }
1085
+ function isValidSelector(doc, selector) {
1086
+ const root = doc.documentElement;
1087
+ if (!root || !selector.trim()) return false;
1088
+ try {
1089
+ root.matches(selector);
1090
+ return true;
1091
+ } catch {
1092
+ return false;
500
1093
  }
501
- rules.sort((x, y) => compareSpecificity(x.specificity, y.specificity) || x.order - y.order);
502
- sheetCache.set(doc, rules);
503
- return rules;
504
1094
  }
505
1095
  function matches(el, selector) {
506
1096
  try {
@@ -517,8 +1107,9 @@ function authorDeclarations(el) {
517
1107
  const matched = rules.filter((r) => matches(el, r.selector));
518
1108
  const inline = parseDeclarations(el.getAttribute("style") ?? "");
519
1109
  const out = /* @__PURE__ */ new Map();
520
- for (const important of [false, true]) {
521
- for (const r of matched) {
1110
+ const importantOrder = [...matched].sort((x, y) => y.rank - x.rank || compareCascade(x, y));
1111
+ for (const [important, ordered] of [[false, matched], [true, importantOrder]]) {
1112
+ for (const r of ordered) {
522
1113
  for (const d of r.declarations) if (d.important === important) out.set(d.prop, d.value);
523
1114
  }
524
1115
  for (const d of inline) if (d.important === important) out.set(d.prop, d.value);
@@ -531,18 +1122,25 @@ function ownProp(el, prop) {
531
1122
  }
532
1123
  function deriveStyle(parent, el) {
533
1124
  let next = null;
534
- for (const prop of INHERITABLE) {
535
- const own = ownProp(el, prop);
536
- if (own == null || own === "inherit") continue;
1125
+ for (const prop of INHERITED_PROPERTIES) {
1126
+ const own2 = ownProp(el, prop);
1127
+ if (own2 == null || own2 === "inherit" || prop === "color" && own2.trim().toLowerCase() === "currentcolor") continue;
537
1128
  if (!next) next = { ...parent };
538
- next[prop] = own;
1129
+ next[prop] = own2;
539
1130
  }
540
1131
  return next ?? parent;
541
1132
  }
542
1133
  function resolveCurrentColor(raw, style) {
543
1134
  if (raw == null) return null;
544
- if (raw.trim().toLowerCase() === "currentcolor") return style["color"] ?? "#000000";
545
- return raw;
1135
+ if (raw.trim().toLowerCase() !== "currentcolor") return raw;
1136
+ const color = readProperty("color", style["color"]);
1137
+ return color == null || color.toLowerCase() === "currentcolor" ? "#000000" : color;
1138
+ }
1139
+ function ownValue(el, prop, warn) {
1140
+ return readProperty(prop, ownProp(el, prop), warn);
1141
+ }
1142
+ function styleValue(style, prop, warn) {
1143
+ return readProperty(prop, style[prop], warn);
546
1144
  }
547
1145
 
548
1146
  // src/elements.ts
@@ -578,7 +1176,11 @@ function patternXml(id, paint, onWarn) {
578
1176
  `width="${trimNumber(geometry.size)}"`,
579
1177
  `height="${trimNumber(geometry.size)}"`
580
1178
  ];
581
- if (origin.x !== 0 || origin.y !== 0) {
1179
+ const [a, b, c, d] = paint.transform ?? [1, 0, 0, 1];
1180
+ const placement = formatMatrix([a, b, c, d, origin.x, origin.y]);
1181
+ if (paint.transform && !isIdentity([a, b, c, d, 0, 0]) && placement) {
1182
+ attrs.push(`patternTransform="${placement}"`);
1183
+ } else if (origin.x !== 0 || origin.y !== 0) {
582
1184
  attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
583
1185
  }
584
1186
  attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
@@ -638,8 +1240,11 @@ function readPattern(el, onWarn) {
638
1240
  }
639
1241
  const x = parseFloat(el.getAttribute("x") ?? "0");
640
1242
  const y = parseFloat(el.getAttribute("y") ?? "0");
1243
+ const m = parseTransform(el.getAttribute("patternTransform"), onWarn);
1244
+ const origin = applyMatrix(m, x, y);
641
1245
  const paint = { fill: "pattern", pattern: spec };
642
- if (x !== 0 || y !== 0) paint.origin = { x, y };
1246
+ if (origin.x !== 0 || origin.y !== 0) paint.origin = origin;
1247
+ if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
643
1248
  return paint;
644
1249
  }
645
1250
  function escapeAttr(s) {
@@ -647,8 +1252,8 @@ function escapeAttr(s) {
647
1252
  }
648
1253
  function serializePathD(path) {
649
1254
  if (path.kind === "rect") {
650
- const { x, y, width, height } = path;
651
- return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
1255
+ const { x, y, width: width2, height: height2 } = path;
1256
+ return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
652
1257
  }
653
1258
  const cmds = path.commands;
654
1259
  const xs = path.coords;
@@ -756,11 +1361,11 @@ function inheritedAttr(el, elements, name, seen = /* @__PURE__ */ new Set()) {
756
1361
  return inheritedAttr(ref, elements, name, seen);
757
1362
  }
758
1363
  function inheritedStops(el, elements, onWarn, seen = /* @__PURE__ */ new Set()) {
759
- const own = readStops(el, onWarn);
760
- if (own.length > 0) return own;
1364
+ const own2 = readStops(el, onWarn);
1365
+ if (own2.length > 0) return own2;
761
1366
  seen.add(el);
762
1367
  const ref = hrefTarget(el, elements);
763
- if (!ref || seen.has(ref)) return own;
1368
+ if (!ref || seen.has(ref)) return own2;
764
1369
  return inheritedStops(ref, elements, onWarn, seen);
765
1370
  }
766
1371
  function parseRatio(raw) {
@@ -776,11 +1381,9 @@ function readStops(el, onWarn) {
776
1381
  const offsetRaw = parseRatio(c.getAttribute("offset") ?? "0");
777
1382
  const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;
778
1383
  const colorAttr = ownProp(c, "stop-color") ?? "#000000";
779
- const parsed = parsePaintAttr(colorAttr);
780
- if (parsed && parsed.kind === "solid") {
781
- const opacityAttr2 = ownProp(c, "stop-opacity");
782
- const own = opacityAttr2 != null ? parseRatio(opacityAttr2) : NaN;
783
- const alpha = Number.isFinite(own) ? own : parsed.alpha;
1384
+ const parsed = readProperty("stop-color", colorAttr, onWarn);
1385
+ if (parsed) {
1386
+ const alpha = ownValue(c, "stop-opacity") ?? parsed.alpha;
784
1387
  stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
785
1388
  } else {
786
1389
  onWarn?.(`gradient stop has unrecognized stop-color: ${colorAttr}`);
@@ -913,6 +1516,10 @@ var PaintServerRegistry = class {
913
1516
  if (hasNativeSvgForm(paint)) return `url(#${id})`;
914
1517
  return `url(#${id}) ${paintFallback(paint)}`;
915
1518
  }
1519
+ /** The paint kind of each registered paint, once, in first-use order. */
1520
+ paintKinds() {
1521
+ return [...new Set(this.order.map((paint) => paint.fill ?? "solid"))];
1522
+ }
916
1523
  /** Whether any registered paint serializes outside SVG's own vocabulary, so
917
1524
  * the root has to declare {@link WEASEL_NS}. */
918
1525
  usesPrivateNamespace() {
@@ -995,9 +1602,10 @@ function paintServerXml(id, paint, onWarn) {
995
1602
  if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
996
1603
  const builtin = gradientXml(id, paint);
997
1604
  if (builtin) return builtin;
998
- const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
1605
+ const kind = getPaintKind(paint.fill);
1606
+ const custom = kind?.toSvg?.(id, paint);
999
1607
  if (custom) return custom;
1000
- onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
1608
+ onWarn?.(kind ? `${paint.fill} fill has no vector form \u2014 omitted from <defs>` : `${paint.fill} fill is not registered \u2014 omitted from <defs>; a kind loaded on demand needs \`await warmSvg(nodes)\` before a synchronous export`);
1001
1609
  return "";
1002
1610
  }
1003
1611
  function gradientUnitsAttr(units) {
@@ -1076,6 +1684,7 @@ function parseSvg(svg, opts = {}) {
1076
1684
  if (!root || root.tagName.toLowerCase() !== "svg") {
1077
1685
  return { nodes: [], warnings: ["root element is not <svg>"] };
1078
1686
  }
1687
+ bindStylesheets(doc, mediaEnvironmentFor(root, opts.media), onWarn);
1079
1688
  const namespaces = opts.namespaces ?? {};
1080
1689
  const uriToPrefix = /* @__PURE__ */ new Map();
1081
1690
  for (const [prefix, uri] of Object.entries(namespaces)) {
@@ -1093,10 +1702,10 @@ function parseSvg(svg, opts = {}) {
1093
1702
  if (documentMeta) result.documentMeta = documentMeta;
1094
1703
  const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
1095
1704
  if (viewBox) result.viewBox = viewBox;
1096
- const width = parseRootLength(root.getAttribute("width"), "width", onWarn);
1097
- if (width != null) result.width = width;
1098
- const height = parseRootLength(root.getAttribute("height"), "height", onWarn);
1099
- if (height != null) result.height = height;
1705
+ const width2 = parseRootLength(root.getAttribute("width"), "width", onWarn);
1706
+ if (width2 != null) result.width = width2;
1707
+ const height2 = parseRootLength(root.getAttribute("height"), "height", onWarn);
1708
+ if (height2 != null) result.height = height2;
1100
1709
  for (let i = 0; i < root.children.length; i++) {
1101
1710
  const c = root.children[i];
1102
1711
  if (c.tagName.toLowerCase() === "title") {
@@ -1261,7 +1870,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
1261
1870
  const childCtm = multiply(ctm, local);
1262
1871
  const childStyle = deriveStyle(style, el);
1263
1872
  const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
1264
- const opacity2 = readOpacityAttr(el, "opacity");
1873
+ const opacity2 = ownValue(el, "opacity");
1265
1874
  const group = { kind: "group", children };
1266
1875
  const clip = clipFor(el, childCtm, clips, onWarn);
1267
1876
  if (clip) group.clip = clip;
@@ -1323,11 +1932,10 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
1323
1932
  }
1324
1933
  }
1325
1934
  const leafStyle = deriveStyle(style, el);
1326
- const fill = readPaint(leafStyle, "fill", "#000000", gradients, onWarn);
1935
+ const fill = readPaint2(leafStyle, "fill", "#000000", gradients, onWarn);
1327
1936
  const stroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
1328
- const opacity = readOpacityAttr(el, "opacity");
1329
- const fillRuleRaw = leafStyle["fill-rule"] ?? null;
1330
- if (fillRuleRaw === "evenodd" && path.kind === "polygon") {
1937
+ const opacity = ownValue(el, "opacity");
1938
+ if (styleValue(leafStyle, "fill-rule") === "evenodd" && path.kind === "polygon") {
1331
1939
  path = { ...path, fillRule: "evenodd" };
1332
1940
  }
1333
1941
  const node = { kind: "path", path, fill };
@@ -1393,10 +2001,10 @@ function polygonToRectIfAxisAligned(p) {
1393
2001
  if (y0 !== y1 || x1 !== x2 || y2 !== y3 || x3 !== x0) return null;
1394
2002
  const x = Math.min(x0, x1);
1395
2003
  const y = Math.min(y0, y2);
1396
- const width = Math.abs(x1 - x0);
1397
- const height = Math.abs(y2 - y1);
1398
- if (width === 0 || height === 0) return null;
1399
- return { kind: "rect", x, y, width, height };
2004
+ const width2 = Math.abs(x1 - x0);
2005
+ const height2 = Math.abs(y2 - y1);
2006
+ if (width2 === 0 || height2 === 0) return null;
2007
+ return { kind: "rect", x, y, width: width2, height: height2 };
1400
2008
  }
1401
2009
  function pathAabb(path) {
1402
2010
  if (path.kind === "rect") {
@@ -1404,22 +2012,21 @@ function pathAabb(path) {
1404
2012
  }
1405
2013
  return boundsOfPath(path);
1406
2014
  }
1407
- function readPaint(style, attr, defaultColor, gradients, onWarn) {
2015
+ function readPaint2(style, attr, defaultColor, gradients, onWarn) {
1408
2016
  const raw = resolveCurrentColor(style[attr] ?? null, style);
1409
- const opacityRaw = style[`${attr}-opacity`] ?? null;
1410
- const opacity = opacityRaw != null ? clamp01(parseFloat(opacityRaw)) : void 0;
2017
+ const opacity = styleValue(style, `${attr}-opacity`, onWarn);
1411
2018
  if (raw == null) {
1412
2019
  if (attr === "stroke") return { kind: "none" };
1413
2020
  const out2 = { kind: "solid", color: defaultColor };
1414
2021
  if (opacity != null) out2.opacity = opacity;
1415
2022
  return out2;
1416
2023
  }
1417
- const parsed = parsePaintAttr(raw);
2024
+ const parsed = readProperty(attr, raw, onWarn);
1418
2025
  if (!parsed) {
1419
2026
  onWarn(`unrecognized ${attr} value: ${raw}`);
1420
2027
  return { kind: "solid", color: defaultColor };
1421
2028
  }
1422
- if (parsed.kind === "none") return { kind: "none" };
2029
+ if (parsed.kind === "none" || parsed.kind === "context") return { kind: "none" };
1423
2030
  if (parsed.kind === "ref") {
1424
2031
  const paint = gradients.get(parsed.id);
1425
2032
  if (!paint) {
@@ -1443,59 +2050,30 @@ function readStroke(style, gradients, onWarn, nonScaling = false) {
1443
2050
  const inheritedStroke = style["stroke"] ?? null;
1444
2051
  const inheritedWidth = style["stroke-width"] ?? null;
1445
2052
  if (inheritedStroke == null && inheritedWidth == null) return void 0;
1446
- const paint = readPaint(style, "stroke", "#000000", gradients, onWarn);
2053
+ const paint = readPaint2(style, "stroke", "#000000", gradients, onWarn);
1447
2054
  if (paint.kind === "none") return void 0;
1448
- const n = inheritedWidth != null ? parseFloat(inheritedWidth) : 1;
2055
+ const n = styleValue(style, "stroke-width", onWarn) ?? 1;
1449
2056
  const stroke = { paint, width: nonScaling ? { px: n } : n };
1450
- const opacityRaw = style["stroke-opacity"] ?? null;
1451
- if (opacityRaw != null) {
1452
- const a = clamp01(parseFloat(opacityRaw));
1453
- if (Number.isFinite(a)) stroke.opacity = a;
1454
- }
1455
- const cap = style["stroke-linecap"] ?? null;
1456
- if (cap === "butt" || cap === "round" || cap === "square") {
1457
- stroke.cap = cap;
1458
- } else if (cap != null) {
1459
- onWarn(`unsupported stroke-linecap: ${cap}`);
1460
- }
1461
- const join = style["stroke-linejoin"] ?? null;
1462
- if (join === "miter" || join === "round" || join === "bevel") {
1463
- stroke.join = join;
1464
- } else if (join === "arcs" || join === "miter-clip") {
1465
- onWarn(`stroke-linejoin "${join}" not supported; falling back to miter`);
1466
- stroke.join = "miter";
1467
- } else if (join != null) {
1468
- onWarn(`unsupported stroke-linejoin: ${join}`);
1469
- }
1470
- const dashAttr = style["stroke-dasharray"] ?? null;
1471
- if (dashAttr != null && dashAttr.trim() !== "" && dashAttr.trim() !== "none") {
1472
- const parsed = parseDashArray(dashAttr);
1473
- if (parsed) stroke.dash = parsed;
1474
- else onWarn(`unrecognized stroke-dasharray: ${dashAttr}`);
1475
- }
1476
- const miterAttr = style["stroke-miterlimit"] ?? null;
1477
- if (miterAttr != null) {
1478
- const m = parseFloat(miterAttr);
1479
- if (Number.isFinite(m) && m >= 1) stroke.miterLimit = m;
1480
- else onWarn(`unrecognized stroke-miterlimit: ${miterAttr}`);
1481
- }
2057
+ const opacity = styleValue(style, "stroke-opacity", onWarn);
2058
+ if (opacity != null) stroke.opacity = opacity;
2059
+ const cap = styleValue(style, "stroke-linecap", onWarn);
2060
+ if (cap) stroke.cap = cap;
2061
+ const join = styleValue(style, "stroke-linejoin", onWarn);
2062
+ if (join) stroke.join = join;
2063
+ const dash = styleValue(style, "stroke-dasharray", onWarn);
2064
+ if (dash) stroke.dash = dash;
2065
+ const miterLimit = styleValue(style, "stroke-miterlimit", onWarn);
2066
+ if (miterLimit != null) stroke.miterLimit = miterLimit;
1482
2067
  for (const [attr, field] of [
1483
2068
  ["marker-start", "markerStart"],
1484
2069
  ["marker-mid", "markerMid"],
1485
2070
  ["marker-end", "markerEnd"]
1486
2071
  ]) {
1487
- const id = parseMarkerRef(style[attr] ?? null);
1488
- if (id !== void 0) stroke[field] = id;
2072
+ const id = styleValue(style, attr, onWarn);
2073
+ if (id) stroke[field] = id;
1489
2074
  }
1490
2075
  return stroke;
1491
2076
  }
1492
- function parseMarkerRef(raw) {
1493
- if (!raw) return void 0;
1494
- const v = raw.trim();
1495
- if (v === "" || v === "none") return void 0;
1496
- const m = /^url\(\s*#([^)\s]+)\s*\)/.exec(v);
1497
- return m ? m[1] : void 0;
1498
- }
1499
2077
  var STROKE_KEYS = [
1500
2078
  "stroke",
1501
2079
  "stroke-width",
@@ -1509,7 +2087,7 @@ var STROKE_KEYS = [
1509
2087
  "marker-end"
1510
2088
  ];
1511
2089
  function isNonScalingStroke(el) {
1512
- return ownProp(el, "vector-effect") === "non-scaling-stroke";
2090
+ return ownValue(el, "vector-effect") === "non-scaling-stroke";
1513
2091
  }
1514
2092
  function ownStrokeStyle(el) {
1515
2093
  const out = {};
@@ -1539,66 +2117,6 @@ function coreStroke(stroke) {
1539
2117
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
1540
2118
  return out;
1541
2119
  }
1542
- function parseDashArray(s) {
1543
- const tokens = s.trim().split(/[\s,]+/).filter(Boolean);
1544
- if (tokens.length === 0) return null;
1545
- const nums = [];
1546
- for (const t of tokens) {
1547
- const n = parseFloat(t);
1548
- if (!Number.isFinite(n) || n < 0) return null;
1549
- nums.push(n);
1550
- }
1551
- return nums.length % 2 === 1 ? [...nums, ...nums] : nums;
1552
- }
1553
- function readOpacityAttr(el, name) {
1554
- const raw = ownProp(el, name);
1555
- if (raw == null) return void 0;
1556
- const n = parseFloat(raw);
1557
- return Number.isFinite(n) ? clamp01(n) : void 0;
1558
- }
1559
- function clamp01(n) {
1560
- return n < 0 ? 0 : n > 1 ? 1 : n;
1561
- }
1562
- function parseLetterSpacing(raw, onWarn) {
1563
- const n = parseFloat(raw);
1564
- if (!Number.isFinite(n)) return void 0;
1565
- const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
1566
- if (unit && unit !== "px") {
1567
- onWarn(`letter-spacing "${raw}" uses unit "${unit}", which is not converted; treated as ${n} world units`);
1568
- }
1569
- return n;
1570
- }
1571
- function parseTextDecoration(raw) {
1572
- if (raw == null) return {};
1573
- const tokens = raw.trim().toLowerCase().split(/\s+/);
1574
- const out = {};
1575
- if (tokens.includes("underline")) out.underline = true;
1576
- if (tokens.includes("line-through")) out.strikethrough = true;
1577
- if (tokens.includes("overline")) out.overline = true;
1578
- return out;
1579
- }
1580
- function parseBaselineShift(raw, onWarn) {
1581
- const keyword = raw.trim().toLowerCase();
1582
- if (keyword === "super" || keyword === "sub") return { script: keyword };
1583
- const n = parseFloat(raw);
1584
- if (!Number.isFinite(n)) return {};
1585
- const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
1586
- if (unit === "%") return { baselineShift: n / 100 };
1587
- if (unit && unit !== "em") {
1588
- onWarn(`baseline-shift "${raw}" uses unit "${unit}", which is not converted; treated as ${n} em`);
1589
- }
1590
- return n === 0 ? {} : { baselineShift: n };
1591
- }
1592
- function parseRunFontSize(raw, onWarn) {
1593
- const n = parseFloat(raw);
1594
- if (!Number.isFinite(n)) return {};
1595
- const unit = /^-?[\d.]+([a-z%]+)$/i.exec(raw.trim())?.[1]?.toLowerCase();
1596
- if (unit === "%") return { fontScale: n / 100 };
1597
- if (unit && unit !== "px") {
1598
- onWarn(`font-size "${raw}" uses unit "${unit}", which is not converted; treated as ${n} world units`);
1599
- }
1600
- return { fontSize: n };
1601
- }
1602
2120
  var XLINK_NS2 = "http://www.w3.org/1999/xlink";
1603
2121
  function parseImageElement(el, ctm, onWarn) {
1604
2122
  const href = imageHref(el);
@@ -1636,7 +2154,7 @@ function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
1636
2154
  width: Math.abs(x1 - x0),
1637
2155
  height: Math.abs(y1 - y0)
1638
2156
  };
1639
- const opacity = readOpacityAttr(ownerEl, "opacity");
2157
+ const opacity = ownValue(ownerEl, "opacity");
1640
2158
  if (opacity != null) node.opacity = opacity;
1641
2159
  const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
1642
2160
  if (!isIdentity(localTransform)) {
@@ -1741,8 +2259,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1741
2259
  const dominantBaseline = el.getAttribute("dominant-baseline");
1742
2260
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
1743
2261
  const topY = explicitTopAnchor ? ay : ay - fontSize;
1744
- const ownShift = ownProp(el, "baseline-shift");
1745
- const inheritedShift = ownShift != null ? parseBaselineShift(ownShift, onWarn) : {};
2262
+ const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
1746
2263
  const runs = [];
1747
2264
  for (let i = 0; i < el.childNodes.length; i++) {
1748
2265
  const child = el.childNodes[i];
@@ -1766,16 +2283,16 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1766
2283
  const plain = runs.map((r) => r.text).join("");
1767
2284
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
1768
2285
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
1769
- const width = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
2286
+ const width2 = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
1770
2287
  const lines = (plain.match(/\n/g)?.length ?? 0) + 1;
1771
- const height = Number.isFinite(dataH) ? dataH : fontSize * lineHeight * lines;
1772
- const opacity = readOpacityAttr(el, "opacity");
2288
+ const height2 = Number.isFinite(dataH) ? dataH : fontSize * lineHeight * lines;
2289
+ const opacity = ownValue(el, "opacity");
1773
2290
  const node = {
1774
2291
  kind: "text",
1775
- x: ax - anchorOffset(textStyle, width),
2292
+ x: ax - anchorOffset(textStyle, width2),
1776
2293
  y: topY,
1777
- width,
1778
- height,
2294
+ width: width2,
2295
+ height: height2,
1779
2296
  text: plain
1780
2297
  };
1781
2298
  const hasStyling = runs.some(
@@ -1783,6 +2300,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1783
2300
  );
1784
2301
  if (hasStyling) node.runs = runs;
1785
2302
  if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
2303
+ if (el.getAttribute("data-weasel-align") === "justify") textStyle.align = "justify";
1786
2304
  const va = el.getAttribute("data-weasel-vertical-align");
1787
2305
  if (va === "center" || va === "bottom") node.verticalAlign = va;
1788
2306
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
@@ -1807,47 +2325,36 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1807
2325
  }
1808
2326
  return node;
1809
2327
  }
1810
- var TEXT_TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
1811
- function parseTextTransform(raw) {
1812
- const v = raw?.trim().toLowerCase();
1813
- return v !== void 0 && TEXT_TRANSFORMS.has(v) ? v : void 0;
1814
- }
1815
2328
  function readTspanRun(el, gradients, style, onWarn) {
1816
2329
  const text = el.textContent ?? "";
1817
2330
  const run = { text };
1818
- const fw = ownProp(el, "font-weight");
1819
- if (fw === "bold" || fw === "700" || fw === "bolder") run.bold = true;
1820
- const fs = ownProp(el, "font-style");
1821
- if (fs === "italic" || fs === "oblique") run.italic = true;
1822
- const ff = ownProp(el, "font-family");
2331
+ const fw = ownValue(el, "font-weight", onWarn);
2332
+ if (fw === "bold" || fw === "bolder" || fw === 700) run.bold = true;
2333
+ else if (fw === "normal") run.fontWeight = 400;
2334
+ else if (typeof fw === "number") run.fontWeight = fw;
2335
+ if (ownValue(el, "font-style", onWarn) === "italic") run.italic = true;
2336
+ const ff = ownValue(el, "font-family", onWarn);
1823
2337
  if (ff) run.fontFamily = ff;
1824
- const sz = ownProp(el, "font-size");
1825
- if (sz != null) {
1826
- const size = parseRunFontSize(sz, onWarn);
1827
- if (size.fontSize != null) run.fontSize = size.fontSize;
1828
- if (size.fontScale != null) run.fontScale = size.fontScale;
1829
- }
1830
- const ls = ownProp(el, "letter-spacing");
1831
- if (ls != null) {
1832
- const n = parseLetterSpacing(ls, onWarn);
1833
- if (n != null) run.letterSpacing = n;
1834
- }
1835
- const bs = ownProp(el, "baseline-shift");
1836
- if (bs != null) {
1837
- const shift = parseBaselineShift(bs, onWarn);
1838
- if (shift.script != null) run.script = shift.script;
1839
- if (shift.baselineShift != null) run.baselineShift = shift.baselineShift;
1840
- }
1841
- const decoration = parseTextDecoration(ownProp(el, "text-decoration"));
1842
- if (decoration.underline) run.underline = true;
1843
- if (decoration.strikethrough) run.strikethrough = true;
1844
- if (decoration.overline) run.overline = true;
1845
- const transform = parseTextTransform(ownProp(el, "text-transform"));
2338
+ const size = ownValue(el, "font-size", onWarn);
2339
+ if (size?.fontSize != null) run.fontSize = size.fontSize;
2340
+ if (size?.fontScale != null) run.fontScale = size.fontScale;
2341
+ const ls = ownValue(el, "letter-spacing", onWarn);
2342
+ if (ls != null) run.letterSpacing = ls;
2343
+ const shift = ownValue(el, "baseline-shift", onWarn);
2344
+ if (shift?.script != null) run.script = shift.script;
2345
+ if (shift?.baselineShift != null) run.baselineShift = shift.baselineShift;
2346
+ const decoration = ownValue(el, "text-decoration", onWarn);
2347
+ if (decoration?.underline) run.underline = true;
2348
+ if (decoration?.strikethrough) run.strikethrough = true;
2349
+ if (decoration?.overline) run.overline = true;
2350
+ const transform = ownValue(el, "text-transform", onWarn);
1846
2351
  if (transform) run.textTransform = transform;
2352
+ const variant = ownValue(el, "font-variant", onWarn);
2353
+ if (variant) run.fontVariantCaps = variant;
1847
2354
  const tspanStyle = deriveStyle(style, el);
1848
2355
  const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
1849
2356
  if (fillAttr) {
1850
- const parsed = parsePaintAttr(fillAttr);
2357
+ const parsed = readProperty("fill", fillAttr);
1851
2358
  if (parsed?.kind === "solid") {
1852
2359
  run.fill = { fill: "solid", color: parsed.color };
1853
2360
  } else if (parsed?.kind === "ref") {
@@ -1862,44 +2369,41 @@ function readTspanRun(el, gradients, style, onWarn) {
1862
2369
  }
1863
2370
  function readTextStyle(style, onWarn) {
1864
2371
  const out = {};
1865
- const sz = style["font-size"];
1866
- if (sz != null) {
1867
- const n = parseFloat(sz);
1868
- if (Number.isFinite(n)) out.fontSize = n;
2372
+ const size = styleValue(style, "font-size", onWarn);
2373
+ if (size?.fontSize != null) out.fontSize = size.fontSize;
2374
+ else if (size?.fontScale != null) {
2375
+ onWarn(`font-size "${style["font-size"]}" on <text> is resolved against the 16px default`);
2376
+ out.fontSize = 16 * size.fontScale;
1869
2377
  }
1870
- const ff = style["font-family"];
2378
+ const ff = styleValue(style, "font-family", onWarn);
1871
2379
  if (ff) out.fontFamily = ff;
1872
- const fw = style["font-weight"];
1873
- if (fw != null) {
1874
- const n = parseFloat(fw);
1875
- out.fontWeight = Number.isFinite(n) ? n : fw;
1876
- }
1877
- const fs = style["font-style"];
1878
- if (fs === "italic" || fs === "normal") out.fontStyle = fs;
1879
- if (style["direction"] === "rtl") out.direction = "rtl";
2380
+ const fw = styleValue(style, "font-weight", onWarn);
2381
+ if (fw != null) out.fontWeight = fw;
2382
+ const fs = styleValue(style, "font-style", onWarn);
2383
+ if (fs) out.fontStyle = fs;
2384
+ if (styleValue(style, "direction") === "rtl") out.direction = "rtl";
1880
2385
  const ltr = out.direction !== "rtl";
1881
- const anchor = style["text-anchor"] ?? (ltr ? void 0 : "start");
2386
+ const anchor = styleValue(style, "text-anchor") ?? (ltr ? void 0 : "start");
1882
2387
  if (anchor === "start") out.align = ltr ? "left" : "right";
1883
2388
  else if (anchor === "middle") out.align = "center";
1884
2389
  else if (anchor === "end") out.align = ltr ? "right" : "left";
1885
- const ls = style["letter-spacing"];
1886
- if (ls != null) {
1887
- const n = parseLetterSpacing(ls, onWarn);
1888
- if (n != null) out.letterSpacing = n;
1889
- }
1890
- const decoration = parseTextDecoration(style["text-decoration"] ?? null);
1891
- if (decoration.underline) out.underline = true;
1892
- if (decoration.strikethrough) out.strikethrough = true;
1893
- if (decoration.overline) out.overline = true;
1894
- const transform = parseTextTransform(style["text-transform"] ?? null);
2390
+ const ls = styleValue(style, "letter-spacing", onWarn);
2391
+ if (ls != null) out.letterSpacing = ls;
2392
+ const decoration = styleValue(style, "text-decoration", onWarn);
2393
+ if (decoration?.underline) out.underline = true;
2394
+ if (decoration?.strikethrough) out.strikethrough = true;
2395
+ if (decoration?.overline) out.overline = true;
2396
+ const transform = styleValue(style, "text-transform", onWarn);
1895
2397
  if (transform && transform !== "none") out.textTransform = transform;
2398
+ const variant = styleValue(style, "font-variant", onWarn);
2399
+ if (variant === "small-caps") out.fontVariantCaps = variant;
1896
2400
  return out;
1897
2401
  }
1898
2402
  function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
1899
2403
  const out = {};
1900
2404
  const fillRaw = resolveCurrentColor(style["fill"] ?? null, style);
1901
2405
  if (fillRaw) {
1902
- const parsed = parsePaintAttr(fillRaw);
2406
+ const parsed = readProperty("fill", fillRaw);
1903
2407
  if (parsed?.kind === "solid") {
1904
2408
  out.fill = { fill: "solid", color: parsed.color };
1905
2409
  } else if (parsed?.kind === "ref") {
@@ -1929,8 +2433,8 @@ function ingestMarkers(root, nodes, gradients, onWarn) {
1929
2433
  if (id && !defs.has(id)) defs.set(id, all[i]);
1930
2434
  }
1931
2435
  const minted = /* @__PURE__ */ new Map();
1932
- const resolve = (stroke, width) => {
1933
- const w = width === void 0 ? 1 : typeof width === "number" ? width : width.px;
2436
+ const resolve = (stroke, width2) => {
2437
+ const w = width2 === void 0 ? 1 : typeof width2 === "number" ? width2 : width2.px;
1934
2438
  for (const [field, role, attr] of MARKER_FIELDS) {
1935
2439
  const id = stroke[field];
1936
2440
  if (typeof id !== "string" || getMarker(id) !== void 0) continue;
@@ -2013,13 +2517,12 @@ function markerOrient(raw) {
2013
2517
  return { orient: rad, reverseAtStart: false };
2014
2518
  }
2015
2519
  function markerPaint(style, attr, gradients, onWarn) {
2016
- const raw = style[attr]?.trim();
2017
- if (raw === "context-stroke") return "line";
2018
- if (raw === "context-fill") {
2019
- onWarn(`a marker's ${attr}="context-fill" is read as the line's stroke paint`);
2520
+ const context = readProperty(attr, style[attr]);
2521
+ if (context?.kind === "context") {
2522
+ if (context.of === "fill") onWarn(`a marker's ${attr}="context-fill" is read as the line's stroke paint`);
2020
2523
  return "line";
2021
2524
  }
2022
- const paint = readPaint(style, attr, "#000000", gradients, onWarn);
2525
+ const paint = readPaint2(style, attr, "#000000", gradients, onWarn);
2023
2526
  if (paint.kind === "none") return "none";
2024
2527
  if (paint.kind === "gradient") return paint.paint;
2025
2528
  return paint.opacity != null ? { ...solid(paint.color), opacity: paint.opacity } : solid(paint.color);
@@ -2347,19 +2850,19 @@ function svgPaintOpacity(paint) {
2347
2850
  function opacityAttr(name, v) {
2348
2851
  return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
2349
2852
  }
2350
- function strokeWidthAttrs(width) {
2351
- if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
2352
- return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
2853
+ function strokeWidthAttrs(width2) {
2854
+ if (typeof width2 === "number") return [`stroke-width="${trimNumber(width2)}"`];
2855
+ return [`stroke-width="${trimNumber(width2.px)}"`, 'vector-effect="non-scaling-stroke"'];
2353
2856
  }
2354
2857
  function coreStrokeAttrs(stroke, registry, warn) {
2355
2858
  const paint = stroke?.paint;
2356
2859
  if (!stroke || !paint) return [];
2357
- const width = stroke.width ?? 1;
2358
- if (!((typeof width === "object" ? width.px : width) > 0)) return [];
2860
+ const width2 = stroke.width ?? 1;
2861
+ if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
2359
2862
  warnStrokeAlign(stroke.align, warn);
2360
2863
  const attrs = [`stroke="${"color" in paint ? paint.color : registry.ref(paint)}"`];
2361
2864
  attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
2362
- attrs.push(...strokeWidthAttrs(width));
2865
+ attrs.push(...strokeWidthAttrs(width2));
2363
2866
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
2364
2867
  if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
2365
2868
  if (stroke.dash && stroke.dash.length > 0) {
@@ -2460,12 +2963,14 @@ function textXml(node, registry, namespaces, warn) {
2460
2963
  if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
2461
2964
  if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
2462
2965
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
2966
+ if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
2463
2967
  const direction = style?.direction ?? "ltr";
2464
2968
  if (direction === "rtl") attrs.push('direction="rtl"');
2465
2969
  if (style?.align != null || direction === "rtl") {
2466
2970
  const align = style?.align ?? "left";
2467
- const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
2971
+ const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
2468
2972
  if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
2973
+ if (align === "justify") attrs.push('data-weasel-align="justify"');
2469
2974
  }
2470
2975
  if (style?.letterSpacing != null && style.letterSpacing !== 0) {
2471
2976
  attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
@@ -2500,17 +3005,25 @@ function textXml(node, registry, namespaces, warn) {
2500
3005
  const deg = node.rotation * 180 / Math.PI;
2501
3006
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
2502
3007
  }
2503
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry, warn)).join("") : escapeText(node.text);
3008
+ const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
2504
3009
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
2505
3010
  const metaEls = metaElementsXml(node.meta, namespaces);
2506
3011
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
2507
3012
  }
2508
- function runXml(run, registry, warn) {
3013
+ function scriptSizeFace(run, style) {
3014
+ if (run.script == null || run.baselineShift == null) return void 0;
3015
+ const face = resolveRunFace(run, resolveTextStyle(style));
3016
+ return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3017
+ }
3018
+ function runXml(run, style, registry, warn) {
2509
3019
  const attrs = [];
2510
- if (run.bold) attrs.push(`font-weight="700"`);
3020
+ if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
3021
+ else if (run.bold) attrs.push(`font-weight="700"`);
2511
3022
  if (run.italic) attrs.push(`font-style="italic"`);
3023
+ if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
2512
3024
  if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
2513
- const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
3025
+ const face = scriptSizeFace(run, style);
3026
+ const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
2514
3027
  const scale = run.fontScale ?? presetScale;
2515
3028
  if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
2516
3029
  else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
@@ -2561,6 +3074,31 @@ function pathBounds(path) {
2561
3074
  const b = boundsOfPath(path);
2562
3075
  return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
2563
3076
  }
3077
+ function svgNeeds(nodes) {
3078
+ const registry = new PaintServerRegistry();
3079
+ registerPaintServers(nodes, registry);
3080
+ const fonts = /* @__PURE__ */ new Map();
3081
+ const walk = (list) => {
3082
+ for (const n of list) {
3083
+ if (n.kind === "group") walk(n.children);
3084
+ else if (n.kind === "text") {
3085
+ for (const run of n.runs ?? []) {
3086
+ const face = scriptSizeFace(run, n.style);
3087
+ if (face) fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3088
+ }
3089
+ }
3090
+ }
3091
+ };
3092
+ walk(nodes);
3093
+ return { fonts: [...fonts.values()], paintKinds: registry.paintKinds() };
3094
+ }
3095
+ function warmSvg(nodes) {
3096
+ const needs = svgNeeds(nodes);
3097
+ return Promise.all([
3098
+ warmFonts(needs.fonts),
3099
+ warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
3100
+ ]).then(() => void 0);
3101
+ }
2564
3102
  var CASCADE_OFFSET_PX = 24;
2565
3103
  var VIEWPORT_FIT = 0.9;
2566
3104
  function readFileText(file) {
@@ -2641,8 +3179,8 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
2641
3179
  drafts.push({ kind: "leaf", id, parentId, pose, data: out });
2642
3180
  return pose;
2643
3181
  };
2644
- const visit = (n, parentId, inherited) => {
2645
- const k = inherited * (n.opacity ?? 1);
3182
+ const visit = (n, parentId, inherited2) => {
3183
+ const k = inherited2 * (n.opacity ?? 1);
2646
3184
  if (n.kind === "group") {
2647
3185
  const draft = {
2648
3186
  kind: "container",
@@ -2800,12 +3338,12 @@ function svgPaintFromKit(fill, box) {
2800
3338
  }
2801
3339
  function svgStrokeFromKit(stroke, box) {
2802
3340
  if (!stroke?.paint) return void 0;
2803
- const width = stroke.width ?? 1;
2804
- if (!((typeof width === "object" ? width.px : width) > 0)) return void 0;
3341
+ const width2 = stroke.width ?? 1;
3342
+ if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return void 0;
2805
3343
  const opacity = stroke.paint.opacity;
2806
3344
  return {
2807
3345
  paint: svgPaintFromKit(stroke.paint, box),
2808
- width,
3346
+ width: width2,
2809
3347
  ...opacity != null ? { opacity } : {},
2810
3348
  ...stroke.cap !== void 0 ? { cap: stroke.cap } : {},
2811
3349
  ...stroke.join !== void 0 ? { join: stroke.join } : {},
@@ -2900,6 +3438,6 @@ function svgNodesFromKit(tree, options = {}) {
2900
3438
  return out;
2901
3439
  }
2902
3440
 
2903
- export { IDENTITY_MATRIX, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
3441
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
2904
3442
  //# sourceMappingURL=index.js.map
2905
3443
  //# sourceMappingURL=index.js.map