@weasel-js/svg 1.13.0 → 1.14.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.d.ts +30 -5
- package/dist/index.js +818 -362
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
|
|
1
|
+
import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, worldCoords, splitSubpaths, joinPaths } from '@weasel-js/geom';
|
|
2
2
|
export { pathToD as serializePathD } from '@weasel-js/geom';
|
|
3
|
-
import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, INHERIT, clipOutline, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, DEFAULT_TEXT_FILL, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
|
|
3
|
+
import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, localizeWorldPoses, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, definesFrame, INHERIT, clipOutline, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, halfLeadingFor, numericWeight, strokeToPath, booleanGroupFields, toRuns, resolveRunFace, derivedDepOf, documentOf, booleanOpOf, DEFAULT_TEXT_FILL, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isImagePatternPaint, isTilePaint, tileSpecOf } from '@weasel-js/core';
|
|
4
4
|
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
5
5
|
|
|
6
6
|
// src/types.ts
|
|
@@ -1296,9 +1296,153 @@ function collectElementsByTag(root, tags) {
|
|
|
1296
1296
|
for (let i = 0; i < all.length; i++) consider(all[i]);
|
|
1297
1297
|
return out;
|
|
1298
1298
|
}
|
|
1299
|
+
var near = (v, target) => Math.abs(v - target) <= 1e-9;
|
|
1300
|
+
function placeBox(box, m, stretch) {
|
|
1301
|
+
const [a, b, c, d] = m;
|
|
1302
|
+
const scale = Math.sqrt(Math.abs(a * d - b * c));
|
|
1303
|
+
const eps = 1e-4 * Math.hypot(a, b, c, d);
|
|
1304
|
+
const uniform = Math.abs(a - d) <= eps;
|
|
1305
|
+
if (Math.abs(b) <= eps && Math.abs(c) <= eps && (stretch || uniform && a > 0)) {
|
|
1306
|
+
const p0 = applyMatrix(m, box.x, box.y);
|
|
1307
|
+
const p1 = applyMatrix(m, box.x + box.width, box.y + box.height);
|
|
1308
|
+
return {
|
|
1309
|
+
x: Math.min(p0.x, p1.x),
|
|
1310
|
+
y: Math.min(p0.y, p1.y),
|
|
1311
|
+
width: Math.abs(p1.x - p0.x),
|
|
1312
|
+
height: Math.abs(p1.y - p0.y),
|
|
1313
|
+
rotation: 0,
|
|
1314
|
+
scale,
|
|
1315
|
+
flipX: a < 0,
|
|
1316
|
+
flipY: d < 0
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const p = Math.hypot(a, b);
|
|
1320
|
+
const q = (a * c + b * d) / p;
|
|
1321
|
+
const r = (a * d - b * c) / p;
|
|
1322
|
+
const settle = (v) => Math.abs(v - 1) <= 1e-5 ? 1 : v;
|
|
1323
|
+
const sx = settle(stretch ? p : scale);
|
|
1324
|
+
const sy = settle(stretch ? Math.abs(r) : scale);
|
|
1325
|
+
const flipY = stretch && r < 0;
|
|
1326
|
+
const ry = flipY ? -sy : sy;
|
|
1327
|
+
const linear = [p / sx, 0, q / ry, r / ry];
|
|
1328
|
+
const center = applyMatrix(m, box.x + box.width / 2, box.y + box.height / 2);
|
|
1329
|
+
const width2 = box.width * sx;
|
|
1330
|
+
const height2 = box.height * sy;
|
|
1331
|
+
const placed = {
|
|
1332
|
+
x: center.x - width2 / 2,
|
|
1333
|
+
y: center.y - height2 / 2,
|
|
1334
|
+
width: width2,
|
|
1335
|
+
height: height2,
|
|
1336
|
+
rotation: Math.atan2(b, a),
|
|
1337
|
+
scale: settle(scale),
|
|
1338
|
+
flipX: false,
|
|
1339
|
+
flipY
|
|
1340
|
+
};
|
|
1341
|
+
if (!near(linear[0], 1) || !near(linear[2], 0) || !near(linear[3], 1)) placed.transform = linear;
|
|
1342
|
+
return placed;
|
|
1343
|
+
}
|
|
1344
|
+
function placeNode(node, tag, m, stretch, onWarn) {
|
|
1345
|
+
if (isIdentity(m)) return 1;
|
|
1346
|
+
const cx = node.x + node.width / 2;
|
|
1347
|
+
const cy = node.y + node.height / 2;
|
|
1348
|
+
if (!invert(m)) {
|
|
1349
|
+
onWarn(`<${tag}> sits under a singular transform; kept its size`);
|
|
1350
|
+
const center = applyMatrix(m, cx, cy);
|
|
1351
|
+
node.x = center.x - node.width / 2;
|
|
1352
|
+
node.y = center.y - node.height / 2;
|
|
1353
|
+
return 1;
|
|
1354
|
+
}
|
|
1355
|
+
const turn = decomposeRotation(m, cx, cy);
|
|
1356
|
+
if (turn != null) {
|
|
1357
|
+
node.rotation = turn;
|
|
1358
|
+
return 1;
|
|
1359
|
+
}
|
|
1360
|
+
const placed = placeBox(node, m, stretch);
|
|
1361
|
+
node.x = placed.x;
|
|
1362
|
+
node.y = placed.y;
|
|
1363
|
+
node.width = placed.width;
|
|
1364
|
+
node.height = placed.height;
|
|
1365
|
+
if (Math.abs(placed.rotation) > 1e-9) node.rotation = placed.rotation;
|
|
1366
|
+
if (placed.flipX) node.flipX = !node.flipX;
|
|
1367
|
+
if (placed.flipY) node.flipY = !node.flipY;
|
|
1368
|
+
if (placed.transform) node.transform = placed.transform;
|
|
1369
|
+
return placed.scale;
|
|
1370
|
+
}
|
|
1371
|
+
function boxTransformAttr(node) {
|
|
1372
|
+
if (poseLinearOf(node)) {
|
|
1373
|
+
const frame = poseFrameOf(node);
|
|
1374
|
+
const m = frame && formatMatrix(frame);
|
|
1375
|
+
return m ? `transform="${m}"` : null;
|
|
1376
|
+
}
|
|
1377
|
+
if (!node.rotation) return null;
|
|
1378
|
+
const cx = node.x + node.width / 2;
|
|
1379
|
+
const cy = node.y + node.height / 2;
|
|
1380
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
1381
|
+
return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/viewBox.ts
|
|
1385
|
+
function parseViewBoxAttr(raw) {
|
|
1386
|
+
if (raw == null) return void 0;
|
|
1387
|
+
const parts = raw.trim().split(/[\s,]+/).filter(Boolean);
|
|
1388
|
+
if (parts.length !== 4) return void 0;
|
|
1389
|
+
const nums = parts.map(parseFloat);
|
|
1390
|
+
if (nums.some((n) => !Number.isFinite(n))) return void 0;
|
|
1391
|
+
return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/readImage.ts
|
|
1395
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
1396
|
+
function parseImageElement(el, ctm, onWarn) {
|
|
1397
|
+
const href = imageHref(el);
|
|
1398
|
+
if (!href) {
|
|
1399
|
+
onWarn("<image> without href; dropped");
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
const node = imageNodeAt(el, el, href, ctm, onWarn);
|
|
1403
|
+
if (el.hasAttribute("preserveAspectRatio") && el.getAttribute("preserveAspectRatio") !== "none") {
|
|
1404
|
+
onWarn("<image> preserveAspectRatio is not modeled; the box is taken literally");
|
|
1405
|
+
}
|
|
1406
|
+
return node;
|
|
1407
|
+
}
|
|
1408
|
+
function imageHref(el) {
|
|
1409
|
+
return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS, "href") ?? el.getAttribute("xlink:href");
|
|
1410
|
+
}
|
|
1411
|
+
function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
|
|
1412
|
+
const rawX = readLength(boxEl, "x", "x", 0, onWarn);
|
|
1413
|
+
const rawY = readLength(boxEl, "y", "y", 0, onWarn);
|
|
1414
|
+
const rawW = readLength(boxEl, "width", "x", 0, onWarn);
|
|
1415
|
+
const rawH = readLength(boxEl, "height", "y", 0, onWarn);
|
|
1416
|
+
const node = { kind: "image", href, x: rawX, y: rawY, width: rawW, height: rawH };
|
|
1417
|
+
const opacity = ownValue(ownerEl, "opacity");
|
|
1418
|
+
if (opacity != null) node.opacity = opacity;
|
|
1419
|
+
const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
|
|
1420
|
+
placeNode(node, "image", multiply(ctm, localTransform), true, onWarn);
|
|
1421
|
+
return node;
|
|
1422
|
+
}
|
|
1423
|
+
function parseCroppedImage(g, ctm, onWarn) {
|
|
1424
|
+
const viewport = [...g.children].find((c) => c.tagName.toLowerCase() === "svg");
|
|
1425
|
+
const img = viewport && [...viewport.children].find((c) => c.tagName.toLowerCase() === "image");
|
|
1426
|
+
const href = img && imageHref(img);
|
|
1427
|
+
const source = viewport && parseViewBoxAttr(viewport.getAttribute("viewBox"));
|
|
1428
|
+
if (!img || !href || !source) return null;
|
|
1429
|
+
const node = imageNodeAt(viewport, g, href, ctm, onWarn);
|
|
1430
|
+
if (source.x !== 0 || source.y !== 0 || source.width !== 1 || source.height !== 1) {
|
|
1431
|
+
node.source = source;
|
|
1432
|
+
}
|
|
1433
|
+
const flip = parseTransform(img.getAttribute("transform"), onWarn);
|
|
1434
|
+
if (flip[0] < 0) toggle(node, "flipX");
|
|
1435
|
+
if (flip[3] < 0) toggle(node, "flipY");
|
|
1436
|
+
return node;
|
|
1437
|
+
}
|
|
1438
|
+
function toggle(node, key) {
|
|
1439
|
+
if (node[key]) delete node[key];
|
|
1440
|
+
else node[key] = true;
|
|
1441
|
+
}
|
|
1299
1442
|
|
|
1300
1443
|
// src/patterns.ts
|
|
1301
1444
|
function patternXml(id, paint, onWarn) {
|
|
1445
|
+
if (isImagePatternPaint(paint)) return imagePatternXml(id, paint, onWarn);
|
|
1302
1446
|
if (!isTilePaint(paint)) {
|
|
1303
1447
|
onWarn?.("texture fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
|
|
1304
1448
|
return "";
|
|
@@ -1306,12 +1450,30 @@ function patternXml(id, paint, onWarn) {
|
|
|
1306
1450
|
const spec = tileSpecOf(paint);
|
|
1307
1451
|
const geometry = tileGeometry(spec);
|
|
1308
1452
|
const body = geometry.shapes.map(shapeXml).join("");
|
|
1453
|
+
const attrs = [
|
|
1454
|
+
...patternBoxAttrs(id, geometry.size, geometry.size, paint),
|
|
1455
|
+
`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`
|
|
1456
|
+
];
|
|
1457
|
+
return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
|
|
1458
|
+
}
|
|
1459
|
+
function imagePatternXml(id, paint, onWarn) {
|
|
1460
|
+
const { width: width2, height: height2 } = paint;
|
|
1461
|
+
if (!(width2 > 0) || !(height2 > 0)) {
|
|
1462
|
+
onWarn?.("image fill names no tile width and height, which an SVG pattern needs \u2014 omitted from <defs>");
|
|
1463
|
+
return "";
|
|
1464
|
+
}
|
|
1465
|
+
const w = trimNumber(width2);
|
|
1466
|
+
const h = trimNumber(height2);
|
|
1467
|
+
const body = `<image href="${escapeAttr(paint.src)}" width="${w}" height="${h}" preserveAspectRatio="none"/>`;
|
|
1468
|
+
return `<pattern ${patternBoxAttrs(id, width2, height2, paint).join(" ")}>${body}</pattern>`;
|
|
1469
|
+
}
|
|
1470
|
+
function patternBoxAttrs(id, width2, height2, paint) {
|
|
1309
1471
|
const origin = paint.origin ?? { x: 0, y: 0 };
|
|
1310
1472
|
const attrs = [
|
|
1311
1473
|
`id="${id}"`,
|
|
1312
1474
|
'patternUnits="userSpaceOnUse"',
|
|
1313
|
-
`width="${trimNumber(
|
|
1314
|
-
`height="${trimNumber(
|
|
1475
|
+
`width="${trimNumber(width2)}"`,
|
|
1476
|
+
`height="${trimNumber(height2)}"`
|
|
1315
1477
|
];
|
|
1316
1478
|
const [a, b, c, d] = paint.transform ?? [1, 0, 0, 1];
|
|
1317
1479
|
const placement = formatMatrix([a, b, c, d, origin.x, origin.y]);
|
|
@@ -1320,8 +1482,7 @@ function patternXml(id, paint, onWarn) {
|
|
|
1320
1482
|
} else if (origin.x !== 0 || origin.y !== 0) {
|
|
1321
1483
|
attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
|
|
1322
1484
|
}
|
|
1323
|
-
attrs
|
|
1324
|
-
return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
|
|
1485
|
+
return attrs;
|
|
1325
1486
|
}
|
|
1326
1487
|
function tilePreviewSvg(spec, background) {
|
|
1327
1488
|
const geometry = tileGeometry(spec);
|
|
@@ -1360,6 +1521,8 @@ function collectPatterns(svg, onWarn) {
|
|
|
1360
1521
|
var PATTERN_TAGS = /* @__PURE__ */ new Set(["pattern"]);
|
|
1361
1522
|
function readPattern(el, onWarn) {
|
|
1362
1523
|
const raw = el.getAttribute("data-weasel-tile");
|
|
1524
|
+
const image = raw ? null : readImagePattern(el);
|
|
1525
|
+
if (image) return image;
|
|
1363
1526
|
if (!raw) {
|
|
1364
1527
|
onWarn?.(`<pattern id="${el.getAttribute("id")}"> has no data-weasel-tile \u2014 unsupported pattern, dropped`);
|
|
1365
1528
|
return null;
|
|
@@ -1375,21 +1538,40 @@ function readPattern(el, onWarn) {
|
|
|
1375
1538
|
onWarn?.(`<pattern id="${el.getAttribute("id")}"> data-weasel-tile is not a tile spec \u2014 dropped`);
|
|
1376
1539
|
return null;
|
|
1377
1540
|
}
|
|
1541
|
+
return { ...spec, ...readPlacement(el, onWarn) };
|
|
1542
|
+
}
|
|
1543
|
+
function readImagePattern(el) {
|
|
1544
|
+
const children = Array.from(el.children);
|
|
1545
|
+
const img = children[0];
|
|
1546
|
+
if (children.length !== 1 || img.localName !== "image") return null;
|
|
1547
|
+
if (el.getAttribute("patternUnits") !== "userSpaceOnUse") return null;
|
|
1548
|
+
if (el.hasAttribute("viewBox") || el.hasAttribute("patternContentUnits")) return null;
|
|
1549
|
+
const src = imageHref(img);
|
|
1550
|
+
const width2 = parseFloat(el.getAttribute("width") ?? "");
|
|
1551
|
+
const height2 = parseFloat(el.getAttribute("height") ?? "");
|
|
1552
|
+
if (!src || !(width2 > 0) || !(height2 > 0)) return null;
|
|
1553
|
+
const at = (name, fallback) => parseFloat(img.getAttribute(name) ?? String(fallback));
|
|
1554
|
+
if (at("x", 0) !== 0 || at("y", 0) !== 0 || at("width", width2) !== width2 || at("height", height2) !== height2) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
return { kind: "image", src, width: width2, height: height2, ...readPlacement(el) };
|
|
1558
|
+
}
|
|
1559
|
+
function readPlacement(el, onWarn) {
|
|
1378
1560
|
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1379
1561
|
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1380
1562
|
const m = parseTransform(el.getAttribute("patternTransform"), onWarn);
|
|
1381
1563
|
const origin = applyMatrix(m, x, y);
|
|
1382
|
-
const
|
|
1383
|
-
if (origin.x !== 0 || origin.y !== 0)
|
|
1384
|
-
if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0]))
|
|
1385
|
-
return
|
|
1564
|
+
const placement = {};
|
|
1565
|
+
if (origin.x !== 0 || origin.y !== 0) placement.origin = origin;
|
|
1566
|
+
if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) placement.transform = [m[0], m[1], m[2], m[3]];
|
|
1567
|
+
return placement;
|
|
1386
1568
|
}
|
|
1387
1569
|
|
|
1388
1570
|
// src/gradients.ts
|
|
1389
1571
|
var WEASEL_NS = "urn:weasel-js:svg";
|
|
1390
1572
|
var WEASEL_NS_PREFIX = "wzl";
|
|
1391
1573
|
function nativeSvgKind(kind) {
|
|
1392
|
-
return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "texture" || isTileKind(kind);
|
|
1574
|
+
return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "texture" || kind === "image" || isTileKind(kind);
|
|
1393
1575
|
}
|
|
1394
1576
|
function hasNativeSvgForm(paint) {
|
|
1395
1577
|
return nativeSvgKind(paint.kind ?? "solid");
|
|
@@ -1423,7 +1605,7 @@ var GRADIENT_TAGS = /* @__PURE__ */ new Set([
|
|
|
1423
1605
|
"meshgradient",
|
|
1424
1606
|
`${WEASEL_NS_PREFIX}:meshgradient`
|
|
1425
1607
|
]);
|
|
1426
|
-
var
|
|
1608
|
+
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
1427
1609
|
function collectGradients(svg, onWarn) {
|
|
1428
1610
|
const out = /* @__PURE__ */ new Map();
|
|
1429
1611
|
const elements = collectElementsByTag(svg, GRADIENT_TAGS);
|
|
@@ -1450,7 +1632,7 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
|
|
|
1450
1632
|
}
|
|
1451
1633
|
}
|
|
1452
1634
|
function hrefTarget(el, elements) {
|
|
1453
|
-
const raw = el.getAttribute("href") ?? el.getAttributeNS(
|
|
1635
|
+
const raw = el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
|
|
1454
1636
|
if (!raw || !raw.startsWith("#")) return null;
|
|
1455
1637
|
return elements.get(raw.slice(1)) ?? null;
|
|
1456
1638
|
}
|
|
@@ -1774,7 +1956,7 @@ function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
|
|
|
1774
1956
|
const k = KAPPA * rad;
|
|
1775
1957
|
const x2 = x + width2;
|
|
1776
1958
|
const y2 = y + height2;
|
|
1777
|
-
const b = new PathBuilder();
|
|
1959
|
+
const b = new PathBuilder().autoOrigin();
|
|
1778
1960
|
b.moveTo(x + rad, y);
|
|
1779
1961
|
b.lineTo(x2 - rad, y);
|
|
1780
1962
|
b.curveTo(x2 - rad + k, y, x2, y + rad - k, x2, y + rad);
|
|
@@ -1790,7 +1972,7 @@ function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
|
|
|
1790
1972
|
function ellipseToPath(cx, cy, rx, ry, m = IDENTITY_MATRIX) {
|
|
1791
1973
|
const kx = KAPPA * rx;
|
|
1792
1974
|
const ky = KAPPA * ry;
|
|
1793
|
-
const b = new PathBuilder();
|
|
1975
|
+
const b = new PathBuilder().autoOrigin();
|
|
1794
1976
|
b.moveTo(cx + rx, cy);
|
|
1795
1977
|
b.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
|
|
1796
1978
|
b.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
|
|
@@ -1803,7 +1985,7 @@ function circleToPath(cx, cy, r, m = IDENTITY_MATRIX) {
|
|
|
1803
1985
|
return ellipseToPath(cx, cy, r, r, m);
|
|
1804
1986
|
}
|
|
1805
1987
|
function lineToPath(x1, y1, x2, y2, m = IDENTITY_MATRIX) {
|
|
1806
|
-
const b = new PathBuilder();
|
|
1988
|
+
const b = new PathBuilder().autoOrigin();
|
|
1807
1989
|
b.moveTo(x1, y1);
|
|
1808
1990
|
b.lineTo(x2, y2);
|
|
1809
1991
|
return transformPath(b.build(), m);
|
|
@@ -1821,7 +2003,7 @@ function polylineToPath(points, m = IDENTITY_MATRIX) {
|
|
|
1821
2003
|
if (points.length === 0) {
|
|
1822
2004
|
return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
|
|
1823
2005
|
}
|
|
1824
|
-
const b = new PathBuilder();
|
|
2006
|
+
const b = new PathBuilder().autoOrigin();
|
|
1825
2007
|
b.moveTo(points[0].x, points[0].y);
|
|
1826
2008
|
for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
|
|
1827
2009
|
return transformPath(b.build(), m);
|
|
@@ -1830,7 +2012,7 @@ function polygonToPath(points, m = IDENTITY_MATRIX) {
|
|
|
1830
2012
|
if (points.length === 0) {
|
|
1831
2013
|
return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
|
|
1832
2014
|
}
|
|
1833
|
-
const b = new PathBuilder();
|
|
2015
|
+
const b = new PathBuilder().autoOrigin();
|
|
1834
2016
|
b.moveTo(points[0].x, points[0].y);
|
|
1835
2017
|
for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
|
|
1836
2018
|
b.close();
|
|
@@ -1894,7 +2076,7 @@ function polygonToRectIfAxisAligned(p) {
|
|
|
1894
2076
|
if (cmds[0] !== PATH_M) return null;
|
|
1895
2077
|
if (cmds[1] !== PATH_L || cmds[2] !== PATH_L || cmds[3] !== PATH_L) return null;
|
|
1896
2078
|
if (cmds[4] !== PATH_Z) return null;
|
|
1897
|
-
const c = p
|
|
2079
|
+
const c = worldCoords(p);
|
|
1898
2080
|
if (c.length !== 8) return null;
|
|
1899
2081
|
const x0 = c[0], y0 = c[1];
|
|
1900
2082
|
const x1 = c[2], y1 = c[3];
|
|
@@ -1908,16 +2090,6 @@ function polygonToRectIfAxisAligned(p) {
|
|
|
1908
2090
|
if (width2 === 0 || height2 === 0) return null;
|
|
1909
2091
|
return { kind: "rect", x, y, width: width2, height: height2 };
|
|
1910
2092
|
}
|
|
1911
|
-
|
|
1912
|
-
// src/viewBox.ts
|
|
1913
|
-
function parseViewBoxAttr(raw) {
|
|
1914
|
-
if (raw == null) return void 0;
|
|
1915
|
-
const parts = raw.trim().split(/[\s,]+/).filter(Boolean);
|
|
1916
|
-
if (parts.length !== 4) return void 0;
|
|
1917
|
-
const nums = parts.map(parseFloat);
|
|
1918
|
-
if (nums.some((n) => !Number.isFinite(n))) return void 0;
|
|
1919
|
-
return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
|
|
1920
|
-
}
|
|
1921
2093
|
function collectClipPaths(root, onWarn) {
|
|
1922
2094
|
const out = /* @__PURE__ */ new Map();
|
|
1923
2095
|
for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
|
|
@@ -2157,139 +2329,6 @@ function withVariableWidth(el, stroke) {
|
|
|
2157
2329
|
}
|
|
2158
2330
|
return out;
|
|
2159
2331
|
}
|
|
2160
|
-
var near = (v, target) => Math.abs(v - target) <= 1e-9;
|
|
2161
|
-
function placeBox(box, m, stretch) {
|
|
2162
|
-
const [a, b, c, d] = m;
|
|
2163
|
-
const scale = Math.sqrt(Math.abs(a * d - b * c));
|
|
2164
|
-
const eps = 1e-4 * Math.hypot(a, b, c, d);
|
|
2165
|
-
const uniform = Math.abs(a - d) <= eps;
|
|
2166
|
-
if (Math.abs(b) <= eps && Math.abs(c) <= eps && (stretch || uniform && a > 0)) {
|
|
2167
|
-
const p0 = applyMatrix(m, box.x, box.y);
|
|
2168
|
-
const p1 = applyMatrix(m, box.x + box.width, box.y + box.height);
|
|
2169
|
-
return {
|
|
2170
|
-
x: Math.min(p0.x, p1.x),
|
|
2171
|
-
y: Math.min(p0.y, p1.y),
|
|
2172
|
-
width: Math.abs(p1.x - p0.x),
|
|
2173
|
-
height: Math.abs(p1.y - p0.y),
|
|
2174
|
-
rotation: 0,
|
|
2175
|
-
scale,
|
|
2176
|
-
flipX: a < 0,
|
|
2177
|
-
flipY: d < 0
|
|
2178
|
-
};
|
|
2179
|
-
}
|
|
2180
|
-
const p = Math.hypot(a, b);
|
|
2181
|
-
const q = (a * c + b * d) / p;
|
|
2182
|
-
const r = (a * d - b * c) / p;
|
|
2183
|
-
const settle = (v) => Math.abs(v - 1) <= 1e-5 ? 1 : v;
|
|
2184
|
-
const sx = settle(stretch ? p : scale);
|
|
2185
|
-
const sy = settle(stretch ? Math.abs(r) : scale);
|
|
2186
|
-
const flipY = stretch && r < 0;
|
|
2187
|
-
const ry = flipY ? -sy : sy;
|
|
2188
|
-
const linear = [p / sx, 0, q / ry, r / ry];
|
|
2189
|
-
const center = applyMatrix(m, box.x + box.width / 2, box.y + box.height / 2);
|
|
2190
|
-
const width2 = box.width * sx;
|
|
2191
|
-
const height2 = box.height * sy;
|
|
2192
|
-
const placed = {
|
|
2193
|
-
x: center.x - width2 / 2,
|
|
2194
|
-
y: center.y - height2 / 2,
|
|
2195
|
-
width: width2,
|
|
2196
|
-
height: height2,
|
|
2197
|
-
rotation: Math.atan2(b, a),
|
|
2198
|
-
scale: settle(scale),
|
|
2199
|
-
flipX: false,
|
|
2200
|
-
flipY
|
|
2201
|
-
};
|
|
2202
|
-
if (!near(linear[0], 1) || !near(linear[2], 0) || !near(linear[3], 1)) placed.transform = linear;
|
|
2203
|
-
return placed;
|
|
2204
|
-
}
|
|
2205
|
-
function placeNode(node, tag, m, stretch, onWarn) {
|
|
2206
|
-
if (isIdentity(m)) return 1;
|
|
2207
|
-
const cx = node.x + node.width / 2;
|
|
2208
|
-
const cy = node.y + node.height / 2;
|
|
2209
|
-
if (!invert(m)) {
|
|
2210
|
-
onWarn(`<${tag}> sits under a singular transform; kept its size`);
|
|
2211
|
-
const center = applyMatrix(m, cx, cy);
|
|
2212
|
-
node.x = center.x - node.width / 2;
|
|
2213
|
-
node.y = center.y - node.height / 2;
|
|
2214
|
-
return 1;
|
|
2215
|
-
}
|
|
2216
|
-
const turn = decomposeRotation(m, cx, cy);
|
|
2217
|
-
if (turn != null) {
|
|
2218
|
-
node.rotation = turn;
|
|
2219
|
-
return 1;
|
|
2220
|
-
}
|
|
2221
|
-
const placed = placeBox(node, m, stretch);
|
|
2222
|
-
node.x = placed.x;
|
|
2223
|
-
node.y = placed.y;
|
|
2224
|
-
node.width = placed.width;
|
|
2225
|
-
node.height = placed.height;
|
|
2226
|
-
if (Math.abs(placed.rotation) > 1e-9) node.rotation = placed.rotation;
|
|
2227
|
-
if (placed.flipX) node.flipX = !node.flipX;
|
|
2228
|
-
if (placed.flipY) node.flipY = !node.flipY;
|
|
2229
|
-
if (placed.transform) node.transform = placed.transform;
|
|
2230
|
-
return placed.scale;
|
|
2231
|
-
}
|
|
2232
|
-
function boxTransformAttr(node) {
|
|
2233
|
-
if (poseLinearOf(node)) {
|
|
2234
|
-
const frame = poseFrameOf(node);
|
|
2235
|
-
const m = frame && formatMatrix(frame);
|
|
2236
|
-
return m ? `transform="${m}"` : null;
|
|
2237
|
-
}
|
|
2238
|
-
if (!node.rotation) return null;
|
|
2239
|
-
const cx = node.x + node.width / 2;
|
|
2240
|
-
const cy = node.y + node.height / 2;
|
|
2241
|
-
const deg = node.rotation * 180 / Math.PI;
|
|
2242
|
-
return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
// src/readImage.ts
|
|
2246
|
-
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
2247
|
-
function parseImageElement(el, ctm, onWarn) {
|
|
2248
|
-
const href = imageHref(el);
|
|
2249
|
-
if (!href) {
|
|
2250
|
-
onWarn("<image> without href; dropped");
|
|
2251
|
-
return null;
|
|
2252
|
-
}
|
|
2253
|
-
const node = imageNodeAt(el, el, href, ctm, onWarn);
|
|
2254
|
-
if (el.hasAttribute("preserveAspectRatio") && el.getAttribute("preserveAspectRatio") !== "none") {
|
|
2255
|
-
onWarn("<image> preserveAspectRatio is not modeled; the box is taken literally");
|
|
2256
|
-
}
|
|
2257
|
-
return node;
|
|
2258
|
-
}
|
|
2259
|
-
function imageHref(el) {
|
|
2260
|
-
return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
|
|
2261
|
-
}
|
|
2262
|
-
function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
|
|
2263
|
-
const rawX = readLength(boxEl, "x", "x", 0, onWarn);
|
|
2264
|
-
const rawY = readLength(boxEl, "y", "y", 0, onWarn);
|
|
2265
|
-
const rawW = readLength(boxEl, "width", "x", 0, onWarn);
|
|
2266
|
-
const rawH = readLength(boxEl, "height", "y", 0, onWarn);
|
|
2267
|
-
const node = { kind: "image", href, x: rawX, y: rawY, width: rawW, height: rawH };
|
|
2268
|
-
const opacity = ownValue(ownerEl, "opacity");
|
|
2269
|
-
if (opacity != null) node.opacity = opacity;
|
|
2270
|
-
const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
|
|
2271
|
-
placeNode(node, "image", multiply(ctm, localTransform), true, onWarn);
|
|
2272
|
-
return node;
|
|
2273
|
-
}
|
|
2274
|
-
function parseCroppedImage(g, ctm, onWarn) {
|
|
2275
|
-
const viewport = [...g.children].find((c) => c.tagName.toLowerCase() === "svg");
|
|
2276
|
-
const img = viewport && [...viewport.children].find((c) => c.tagName.toLowerCase() === "image");
|
|
2277
|
-
const href = img && imageHref(img);
|
|
2278
|
-
const source = viewport && parseViewBoxAttr(viewport.getAttribute("viewBox"));
|
|
2279
|
-
if (!img || !href || !source) return null;
|
|
2280
|
-
const node = imageNodeAt(viewport, g, href, ctm, onWarn);
|
|
2281
|
-
if (source.x !== 0 || source.y !== 0 || source.width !== 1 || source.height !== 1) {
|
|
2282
|
-
node.source = source;
|
|
2283
|
-
}
|
|
2284
|
-
const flip = parseTransform(img.getAttribute("transform"), onWarn);
|
|
2285
|
-
if (flip[0] < 0) toggle(node, "flipX");
|
|
2286
|
-
if (flip[3] < 0) toggle(node, "flipY");
|
|
2287
|
-
return node;
|
|
2288
|
-
}
|
|
2289
|
-
function toggle(node, key) {
|
|
2290
|
-
if (node[key]) delete node[key];
|
|
2291
|
-
else node[key] = true;
|
|
2292
|
-
}
|
|
2293
2332
|
function svgDirection(style, text) {
|
|
2294
2333
|
return paragraphDirection(text, style?.direction ?? "auto");
|
|
2295
2334
|
}
|
|
@@ -2301,6 +2340,7 @@ function anchorOffset(style, width2, text) {
|
|
|
2301
2340
|
// src/tspanLines.ts
|
|
2302
2341
|
var LINES_ATTR = "data-weasel-lines";
|
|
2303
2342
|
var LINE_HEIGHT_ATTR = "data-weasel-line-height";
|
|
2343
|
+
var writtenByWeasel = (text) => text.hasAttribute("data-weasel-width");
|
|
2304
2344
|
var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
|
|
2305
2345
|
function linesXml(pieces, x, pitch) {
|
|
2306
2346
|
let out = "";
|
|
@@ -2329,25 +2369,55 @@ var DEFAULT_LINE_HEIGHT = 1.2;
|
|
|
2329
2369
|
var LineTracker = class {
|
|
2330
2370
|
constructor(text, onWarn) {
|
|
2331
2371
|
this.onWarn = onWarn;
|
|
2332
|
-
this.
|
|
2333
|
-
this.at = readLength(text, "y", "y", 0, onWarn);
|
|
2372
|
+
this.at = this.lineY = readLength(text, "y", "y", 0, onWarn);
|
|
2334
2373
|
}
|
|
2335
2374
|
onWarn;
|
|
2336
|
-
|
|
2337
|
-
|
|
2375
|
+
/** The current line's baseline, in user units. */
|
|
2376
|
+
lineY;
|
|
2377
|
+
/** Where the pen is, in user units. */
|
|
2338
2378
|
at;
|
|
2339
2379
|
pitches = [];
|
|
2380
|
+
/** The first line's baseline, once a glyph has landed on it. */
|
|
2381
|
+
first;
|
|
2382
|
+
/** Whether the text opens with blank lines it states, below the text's `y`. */
|
|
2383
|
+
keepsTop = false;
|
|
2384
|
+
/** How far the pen sits above the current line's baseline, in user units. */
|
|
2385
|
+
get rise() {
|
|
2386
|
+
return this.lineY - this.at;
|
|
2387
|
+
}
|
|
2388
|
+
/** Move the pen to `y`, when given, and then down by `dy`. */
|
|
2389
|
+
move(y, dy) {
|
|
2390
|
+
this.at = (y ?? this.at) + dy;
|
|
2391
|
+
}
|
|
2392
|
+
/** Say that `glyph` sits at the pen. The first glyph that is not white
|
|
2393
|
+
* space settles the first line where it sits, wherever the text's `y` was:
|
|
2394
|
+
* a line is placed by its glyphs. */
|
|
2395
|
+
land(glyph) {
|
|
2396
|
+
if (this.first !== void 0 || /^\s$/u.test(glyph)) return;
|
|
2397
|
+
if (!this.keepsTop) this.lineY = this.at;
|
|
2398
|
+
this.first = this.lineY;
|
|
2399
|
+
}
|
|
2400
|
+
/** The baseline the box's first line sits on: the text's `y` where the text
|
|
2401
|
+
* opens with blank lines it states, otherwise where the first glyph landed.
|
|
2402
|
+
* `undefined` before any glyph has. */
|
|
2403
|
+
get firstBaseline() {
|
|
2404
|
+
return this.keepsTop ? void 0 : this.first;
|
|
2405
|
+
}
|
|
2340
2406
|
/** How many lines down `tspan` opens a new line, or 0 when it stays on the
|
|
2341
|
-
* current one.
|
|
2342
|
-
*
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2407
|
+
* current one. A tspan opens one when it has its own `x` and puts the pen
|
|
2408
|
+
* off the line's baseline; without an `x` it only moves its glyphs off the
|
|
2409
|
+
* line. `inherited` is the enclosing `<text>`'s `y` and `dy` for the
|
|
2410
|
+
* tspan's first glyph, used where the tspan has none of its own. Call once
|
|
2411
|
+
* per tspan, in order. `afterText` is whether any text precedes it: the
|
|
2412
|
+
* first line's own placement is not a pitch. */
|
|
2413
|
+
startsLine(tspan, afterText, inherited2 = {}) {
|
|
2414
|
+
const from = this.lineY;
|
|
2415
|
+
const y = tspan.hasAttribute("y") ? readLength(tspan, "y", "y", this.at, this.onWarn) : inherited2.y;
|
|
2416
|
+
const dy = tspan.hasAttribute("dy") ? readLength(tspan, "dy", "y", 0, this.onWarn) : inherited2.dy ?? 0;
|
|
2417
|
+
this.move(y, dy);
|
|
2418
|
+
if (!afterText && this.first === void 0 && statesLines(tspan)) this.keepsTop = true;
|
|
2419
|
+
if (!tspan.hasAttribute("x") || Math.abs(this.at - from) < 1e-9) return 0;
|
|
2420
|
+
this.lineY = this.at;
|
|
2351
2421
|
const stated = Number(tspan.getAttribute(LINES_ATTR) ?? NaN);
|
|
2352
2422
|
const lines = Number.isInteger(stated) && stated >= 0 ? stated : 1;
|
|
2353
2423
|
if (afterText && lines > 0 && this.at > from) this.pitches.push((this.at - from) / lines);
|
|
@@ -2355,17 +2425,18 @@ var LineTracker = class {
|
|
|
2355
2425
|
}
|
|
2356
2426
|
/**
|
|
2357
2427
|
* The line height the lines were pitched at, as a multiple of `fontSize`:
|
|
2358
|
-
* `
|
|
2359
|
-
* model holds one line height, so
|
|
2360
|
-
* keeps the block's height.
|
|
2428
|
+
* `stated` for a single line or for lines pitched as it says, otherwise
|
|
2429
|
+
* `undefined` at the default pitch. The model holds one line height, so
|
|
2430
|
+
* uneven pitches give their mean, which keeps the block's height.
|
|
2361
2431
|
*/
|
|
2362
|
-
lineHeight(fontSize) {
|
|
2432
|
+
lineHeight(fontSize, stated) {
|
|
2363
2433
|
const { pitches } = this;
|
|
2364
|
-
if (pitches.length === 0 || !(fontSize > 0)) return
|
|
2434
|
+
if (pitches.length === 0 || !(fontSize > 0)) return stated;
|
|
2365
2435
|
const mean = pitches.reduce((sum, p) => sum + p, 0) / pitches.length;
|
|
2366
2436
|
if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
|
|
2367
2437
|
this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
|
|
2368
2438
|
}
|
|
2439
|
+
if (stated !== void 0 && Math.abs(mean - stated * fontSize) <= 1e-3 * fontSize) return stated;
|
|
2369
2440
|
const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
|
|
2370
2441
|
return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
|
|
2371
2442
|
}
|
|
@@ -2374,19 +2445,6 @@ var holdsOnlyText = (el) => !Array.from(el.childNodes).some(
|
|
|
2374
2445
|
(child) => child.nodeType === 1
|
|
2375
2446
|
/* ELEMENT_NODE */
|
|
2376
2447
|
);
|
|
2377
|
-
function warnUnreadPositions(el, onWarn) {
|
|
2378
|
-
const tag = el.tagName.toLowerCase();
|
|
2379
|
-
for (const name of ["x", "y"]) {
|
|
2380
|
-
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2381
|
-
if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
|
|
2382
|
-
}
|
|
2383
|
-
for (const name of tag === "tspan" || holdsOnlyText(el) ? ["rotate"] : ["dx", "rotate"]) {
|
|
2384
|
-
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2385
|
-
if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
|
|
2386
|
-
onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
2448
|
function sameStyle(a, b) {
|
|
2391
2449
|
const style = (run) => JSON.stringify(
|
|
2392
2450
|
Object.entries(run).filter(([k, v]) => k !== "text" && v !== void 0).sort(([j], [k]) => j < k ? -1 : 1)
|
|
@@ -2478,21 +2536,101 @@ function laidLinesXml(pieces, layout, anchor) {
|
|
|
2478
2536
|
out += rangeXml(pieces, offsets, from, to, lead);
|
|
2479
2537
|
});
|
|
2480
2538
|
}
|
|
2481
|
-
const trailing = breaksIn(text.slice(prevEnd));
|
|
2482
|
-
const last = layout.laid.lines[layout.laid.lines.length - 1];
|
|
2483
|
-
if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
|
|
2484
|
-
return out;
|
|
2539
|
+
const trailing = breaksIn(text.slice(prevEnd));
|
|
2540
|
+
const last = layout.laid.lines[layout.laid.lines.length - 1];
|
|
2541
|
+
if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
|
|
2542
|
+
return out;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/textWhitespace.ts
|
|
2546
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
2547
|
+
function preservesSpace(el) {
|
|
2548
|
+
for (let e = el; e; e = e.parentElement) {
|
|
2549
|
+
const v = e.getAttributeNS(XML_NS, "space") ?? e.getAttribute("xml:space");
|
|
2550
|
+
if (v === "preserve") return true;
|
|
2551
|
+
if (v === "default") return false;
|
|
2552
|
+
}
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
function collapseRunWhitespace(runs) {
|
|
2556
|
+
let atStart = true;
|
|
2557
|
+
let lastWasSpace = false;
|
|
2558
|
+
for (const run of runs) {
|
|
2559
|
+
let out = "";
|
|
2560
|
+
for (const ch of run.text.replace(/\n/g, "").replace(/[\t\r]/g, " ")) {
|
|
2561
|
+
if (ch === " ") {
|
|
2562
|
+
if (atStart || lastWasSpace) continue;
|
|
2563
|
+
out += " ";
|
|
2564
|
+
lastWasSpace = true;
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
out += ch;
|
|
2568
|
+
lastWasSpace = false;
|
|
2569
|
+
atStart = false;
|
|
2570
|
+
}
|
|
2571
|
+
run.text = out;
|
|
2572
|
+
}
|
|
2573
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
2574
|
+
if (runs[i].text === "") continue;
|
|
2575
|
+
if (runs[i].text.endsWith(" ")) runs[i].text = runs[i].text.slice(0, -1);
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2578
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
2579
|
+
if (runs[i].text === "") runs.splice(i, 1);
|
|
2580
|
+
}
|
|
2485
2581
|
}
|
|
2582
|
+
|
|
2583
|
+
// src/glyphShifts.ts
|
|
2486
2584
|
var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2585
|
+
var collapses = (el, text = el.textContent ?? "") => COLLAPSIBLE.test(text) && !preservesSpace(el);
|
|
2586
|
+
function readList(el, name, onWarn) {
|
|
2587
|
+
const raw = el.getAttribute(name)?.trim();
|
|
2588
|
+
if (!raw) return [];
|
|
2589
|
+
const axis = name.endsWith("x") ? "x" : "y";
|
|
2590
|
+
return raw.split(/[\s,]+/).map((v) => resolveLength(v, el, name, axis, 0, onWarn));
|
|
2591
|
+
}
|
|
2592
|
+
function pensOf(line, rtl) {
|
|
2593
|
+
const pens = line.cells.map((c) => c.x);
|
|
2594
|
+
if (rtl) for (const g of line.glyphs) pens[g.cell] = g.x + g.advance;
|
|
2595
|
+
return pens;
|
|
2596
|
+
}
|
|
2597
|
+
function cellsOf(run, style, rtl) {
|
|
2598
|
+
const resolved = resolveTextStyle(style);
|
|
2599
|
+
const { lines } = layoutRuns(resolveRuns([run], resolved), {
|
|
2600
|
+
maxWidth: Infinity,
|
|
2601
|
+
lineHeight: resolved.lineHeight,
|
|
2602
|
+
align: rtl ? "right" : "left",
|
|
2603
|
+
direction: rtl ? "rtl" : "ltr",
|
|
2604
|
+
tabSize: resolved.tabSize
|
|
2605
|
+
});
|
|
2606
|
+
const line = lines[0];
|
|
2607
|
+
if (lines.length !== 1 || !line || line.cells.length !== Array.from(run.text).length) return null;
|
|
2608
|
+
return { cells: line.cells, line, width: line.x1 - line.x0 };
|
|
2609
|
+
}
|
|
2610
|
+
var allRead = (cells, rtl) => cells.every((c) => c.level % 2 === (rtl ? 1 : 0));
|
|
2611
|
+
function penPositions(run, style, rtl) {
|
|
2612
|
+
const laid = cellsOf(run, style, rtl);
|
|
2613
|
+
if (!laid || !(laid.width > 0) || !allRead(laid.cells, rtl)) return null;
|
|
2614
|
+
const pens = pensOf(laid.line, rtl);
|
|
2615
|
+
return pens.map((p) => p - pens[0]);
|
|
2616
|
+
}
|
|
2617
|
+
function foldPositions(lists, run, style, rtl, onWarn) {
|
|
2618
|
+
const { dx, x } = lists;
|
|
2619
|
+
const pens = x.length > 1 ? penPositions(run, style, rtl) : null;
|
|
2620
|
+
if (x.length > 1 && !pens) {
|
|
2621
|
+
onWarn(`<${lists.tag} x="${lists.raw.x?.trim()}"> positions each glyph; only the first value is read`);
|
|
2622
|
+
}
|
|
2623
|
+
const placed = pens ? Math.min(x.length, pens.length) : 0;
|
|
2624
|
+
const out = Array.from({ length: Math.max(dx.length, placed) }, (_, i) => {
|
|
2625
|
+
const own2 = dx[i] ?? 0;
|
|
2626
|
+
if (i === 0 || i >= placed) return own2;
|
|
2627
|
+
return x[i] + own2 - (x[i - 1] + (dx[i - 1] ?? 0)) - (pens[i] - pens[i - 1]);
|
|
2628
|
+
});
|
|
2629
|
+
return out.some((v) => v !== 0) ? out : null;
|
|
2492
2630
|
}
|
|
2493
2631
|
var spacingOf = (run, inherited2) => resolveScreenLength(run.letterSpacing ?? inherited2 ?? 0, 1);
|
|
2494
|
-
function spread(run, extra,
|
|
2495
|
-
const base = spacingOf(run,
|
|
2632
|
+
function spread(run, extra, style) {
|
|
2633
|
+
const base = spacingOf(run, style.letterSpacing);
|
|
2496
2634
|
const out = [];
|
|
2497
2635
|
let last;
|
|
2498
2636
|
Array.from(run.text).forEach((glyph, i) => {
|
|
@@ -2503,71 +2641,388 @@ function spread(run, extra, inherited2) {
|
|
|
2503
2641
|
});
|
|
2504
2642
|
return out;
|
|
2505
2643
|
}
|
|
2506
|
-
function
|
|
2644
|
+
function placeRun(lists, run, lines, style, onWarn) {
|
|
2507
2645
|
const line = lines[lines.length - 1];
|
|
2508
|
-
const
|
|
2509
|
-
|
|
2646
|
+
const names = ["dx", "x"].filter((n) => n === "dx" ? lists.dx.some((v) => v !== 0) : lists.x.length > 1);
|
|
2647
|
+
const named = `<${lists.tag} ${names.map((n) => `${n}="${lists.raw[n]}"`).join(" ")}>`;
|
|
2648
|
+
if (names.length > 0 && collapses(lists.el, run.text)) {
|
|
2649
|
+
onWarn(`${named} is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2650
|
+
line.push(run);
|
|
2651
|
+
return 0;
|
|
2652
|
+
}
|
|
2653
|
+
const rtl = style.direction === "rtl";
|
|
2654
|
+
const shifts = names.length > 0 ? foldPositions(lists, run, style, rtl, onWarn) : null;
|
|
2655
|
+
const laid = shifts && rtl ? cellsOf(run, style, rtl) : null;
|
|
2656
|
+
if (shifts && rtl && !(laid && allRead(laid.cells, rtl))) {
|
|
2657
|
+
onWarn(`${named} is not read: in right-to-left text it is read only where a bidi engine says every glyph reads right to left`);
|
|
2510
2658
|
line.push(run);
|
|
2511
2659
|
return 0;
|
|
2512
2660
|
}
|
|
2513
|
-
|
|
2514
|
-
const tag = tspan.tagName.toLowerCase();
|
|
2515
|
-
if (COLLAPSIBLE.test(run.text)) {
|
|
2516
|
-
onWarn(`<${tag} dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2661
|
+
if (!shifts) {
|
|
2517
2662
|
line.push(run);
|
|
2518
2663
|
return 0;
|
|
2519
2664
|
}
|
|
2665
|
+
const dx = rtl ? shifts.map((v) => -v) : shifts;
|
|
2520
2666
|
let lead = 0;
|
|
2521
2667
|
let before = line.length - 1;
|
|
2522
2668
|
while (before >= 0 && line[before].text === "") before--;
|
|
2523
2669
|
if (dx[0] !== 0 && before >= 0) {
|
|
2524
2670
|
const glyphs = Array.from(line[before].text);
|
|
2525
|
-
line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]],
|
|
2671
|
+
line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]], style));
|
|
2526
2672
|
} else if (dx[0] !== 0 && lines.length === 1) {
|
|
2527
|
-
lead =
|
|
2673
|
+
lead = shifts[0];
|
|
2528
2674
|
} else if (dx[0] !== 0) {
|
|
2529
|
-
onWarn(
|
|
2675
|
+
onWarn(`${named} opens a line; the line starts at the box edge, and the rest of the list is read`);
|
|
2530
2676
|
}
|
|
2531
|
-
line.push(...spread(run, dx.slice(1),
|
|
2677
|
+
line.push(...spread(run, dx.slice(1), style));
|
|
2532
2678
|
return lead;
|
|
2533
2679
|
}
|
|
2680
|
+
function placeTspanRun(tspan, run, lines, style, onWarn, inherited2 = { dx: [], raw: null }) {
|
|
2681
|
+
const own2 = inherited2.merged ? [] : readList(tspan, "dx", onWarn);
|
|
2682
|
+
const borrows = own2.length < inherited2.dx.length;
|
|
2683
|
+
const theirs = borrows && own2.length === 0;
|
|
2684
|
+
return placeRun({
|
|
2685
|
+
el: tspan,
|
|
2686
|
+
tag: theirs ? "text" : tspan.tagName.toLowerCase(),
|
|
2687
|
+
dx: borrows ? [...own2, ...inherited2.dx.slice(own2.length)] : own2,
|
|
2688
|
+
x: inherited2.merged ? [] : readList(tspan, "x", onWarn),
|
|
2689
|
+
raw: { dx: theirs ? inherited2.raw : tspan.getAttribute("dx"), x: tspan.getAttribute("x") }
|
|
2690
|
+
}, run, lines, style, onWarn);
|
|
2691
|
+
}
|
|
2692
|
+
function mayOpenLine(tspan, ownX, onWarn) {
|
|
2693
|
+
if (!tspan || ownX.length === 0) return false;
|
|
2694
|
+
return tspan.hasAttribute("y") || (readList(tspan, "dy", onWarn)[0] ?? 0) !== 0;
|
|
2695
|
+
}
|
|
2696
|
+
function measureLine(runs, glyphs, style, rtl) {
|
|
2697
|
+
const resolved = resolveTextStyle(style);
|
|
2698
|
+
const { lines } = layoutRuns(resolveRuns(runs, resolved), {
|
|
2699
|
+
maxWidth: Infinity,
|
|
2700
|
+
lineHeight: resolved.lineHeight,
|
|
2701
|
+
align: rtl ? "right" : "left",
|
|
2702
|
+
direction: rtl ? "rtl" : "ltr",
|
|
2703
|
+
tabSize: resolved.tabSize
|
|
2704
|
+
});
|
|
2705
|
+
const line = lines.length === 1 ? lines[0] : null;
|
|
2706
|
+
if (!line || line.cells.length !== glyphs || !(line.x1 > line.x0)) return "no loaded face measures its glyphs";
|
|
2707
|
+
if (!allRead(line.cells, rtl)) {
|
|
2708
|
+
return "in right-to-left text it is read only where a bidi engine says every glyph reads right to left";
|
|
2709
|
+
}
|
|
2710
|
+
return line;
|
|
2711
|
+
}
|
|
2712
|
+
function foldTextPositions(x, dx, pieces, style, onWarn) {
|
|
2713
|
+
const segments = [];
|
|
2714
|
+
const xs = [];
|
|
2715
|
+
const dxs = [];
|
|
2716
|
+
for (let at2 = 0, k = 0; at2 < x.length && k < pieces.length; k++) {
|
|
2717
|
+
const { tspan, run } = pieces[k];
|
|
2718
|
+
const ownX = tspan ? readList(tspan, "x", onWarn) : [];
|
|
2719
|
+
const ownDx = tspan ? readList(tspan, "dx", onWarn) : [];
|
|
2720
|
+
if (segments.length === 0 || mayOpenLine(tspan, ownX, onWarn)) segments.push({ runs: [], start: xs.length });
|
|
2721
|
+
const n = Array.from(run.text).length;
|
|
2722
|
+
for (let i = 0; i < n; i++) {
|
|
2723
|
+
xs.push(i < ownX.length ? ownX[i] : x[at2 + i]);
|
|
2724
|
+
dxs.push(i < ownDx.length ? ownDx[i] : dx[at2 + i] ?? 0);
|
|
2725
|
+
}
|
|
2726
|
+
segments[segments.length - 1].runs.push(run);
|
|
2727
|
+
at2 += n;
|
|
2728
|
+
}
|
|
2729
|
+
const rtl = style.direction === "rtl";
|
|
2730
|
+
const glyphs = xs.length;
|
|
2731
|
+
const pens = [];
|
|
2732
|
+
for (const [s, { runs, start }] of segments.entries()) {
|
|
2733
|
+
const line = measureLine(runs, (segments[s + 1]?.start ?? glyphs) - start, style, rtl);
|
|
2734
|
+
if (typeof line === "string") return line;
|
|
2735
|
+
pens.push(...pensOf(line, rtl));
|
|
2736
|
+
}
|
|
2737
|
+
const starts = new Set(segments.map((s) => s.start));
|
|
2738
|
+
const at = [];
|
|
2739
|
+
for (let i = 0; i < glyphs; i++) {
|
|
2740
|
+
const own2 = xs[i];
|
|
2741
|
+
at.push(own2 !== void 0 ? own2 + dxs[i] : at[i - 1] + pens[i] - pens[i - 1] + dxs[i]);
|
|
2742
|
+
}
|
|
2743
|
+
const out = Array.from({ length: Math.max(dx.length, glyphs) }, (_, i) => {
|
|
2744
|
+
if (i >= glyphs) return dx[i] ?? 0;
|
|
2745
|
+
if (i === 0) return at[0] - x[0];
|
|
2746
|
+
if (starts.has(i)) return dxs[i];
|
|
2747
|
+
return at[i] - at[i - 1] - (pens[i] - pens[i - 1]);
|
|
2748
|
+
});
|
|
2749
|
+
return { dx: out, reached: glyphs };
|
|
2750
|
+
}
|
|
2751
|
+
var TextShifts = class {
|
|
2752
|
+
constructor(text, onWarn, pieces = [], style = {}) {
|
|
2753
|
+
this.text = text;
|
|
2754
|
+
this.onWarn = onWarn;
|
|
2755
|
+
this.raw = text.getAttribute("dx");
|
|
2756
|
+
const dx = readList(text, "dx", onWarn);
|
|
2757
|
+
const x = readList(text, "x", onWarn);
|
|
2758
|
+
const collapsible = collapses(text);
|
|
2759
|
+
const unread = dx.some((v) => v !== 0) && collapsible;
|
|
2760
|
+
if (unread) {
|
|
2761
|
+
onWarn(`<text dx="${this.raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2762
|
+
}
|
|
2763
|
+
this.dx = unread ? [] : dx;
|
|
2764
|
+
if (x.length < 2) return;
|
|
2765
|
+
const folded = collapsible ? "its text has white space SVG collapses, which moves the glyphs the list names" : foldTextPositions(x, this.dx, pieces, style, onWarn);
|
|
2766
|
+
if (typeof folded === "string") {
|
|
2767
|
+
onWarn(`<text x="${text.getAttribute("x")?.trim()}"> positions each glyph; only the first value is read: ${folded}`);
|
|
2768
|
+
} else {
|
|
2769
|
+
this.dx = folded.dx;
|
|
2770
|
+
this.merged = folded.reached;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
text;
|
|
2774
|
+
onWarn;
|
|
2775
|
+
dx;
|
|
2776
|
+
raw;
|
|
2777
|
+
at = 0;
|
|
2778
|
+
/** How many glyphs the text's `x` list was merged over with their tspans' own lists. */
|
|
2779
|
+
merged = 0;
|
|
2780
|
+
/** The values over the next `run`'s glyphs, empty when they move none. */
|
|
2781
|
+
take(run) {
|
|
2782
|
+
const n = Array.from(run.text).length;
|
|
2783
|
+
const out = this.dx.slice(this.at, this.at + n);
|
|
2784
|
+
const merged = this.at < this.merged;
|
|
2785
|
+
this.at += n;
|
|
2786
|
+
return { dx: out.some((v) => v !== 0) ? out : [], raw: this.raw, ...merged ? { merged } : {} };
|
|
2787
|
+
}
|
|
2788
|
+
/** Add a bare text run of the `<text>` to `lines`, spaced by its stretch of the list. */
|
|
2789
|
+
place(run, lines, style) {
|
|
2790
|
+
const { dx } = this.take(run);
|
|
2791
|
+
if (dx.length === 0) {
|
|
2792
|
+
lines[lines.length - 1].push(run);
|
|
2793
|
+
return 0;
|
|
2794
|
+
}
|
|
2795
|
+
return placeRun({ el: this.text, tag: "text", dx, x: [], raw: { dx: this.raw, x: null } }, run, lines, style, this.onWarn);
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2534
2798
|
|
|
2535
|
-
// src/
|
|
2536
|
-
|
|
2537
|
-
|
|
2799
|
+
// src/glyphTurns.ts
|
|
2800
|
+
function turnsOf(el, onWarn) {
|
|
2801
|
+
const raw = el.getAttribute("rotate")?.trim();
|
|
2802
|
+
if (!raw) return [];
|
|
2803
|
+
const list = raw.split(/[\s,]+/).map(Number);
|
|
2804
|
+
if (list.some((v) => !Number.isFinite(v))) {
|
|
2805
|
+
onWarn(`<${el.tagName.toLowerCase()} rotate="${raw}"> is not read: it is not a list of numbers`);
|
|
2806
|
+
return [];
|
|
2807
|
+
}
|
|
2808
|
+
if (list.some((v) => v !== list[0]) && collapses(el)) {
|
|
2809
|
+
onWarn(`<${el.tagName.toLowerCase()} rotate="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2810
|
+
return [];
|
|
2811
|
+
}
|
|
2812
|
+
return list;
|
|
2813
|
+
}
|
|
2814
|
+
var valueAt = (list, i) => list.length === 0 ? void 0 : list[Math.min(i, list.length - 1)];
|
|
2815
|
+
var TextTurns = class {
|
|
2816
|
+
list;
|
|
2817
|
+
at = 0;
|
|
2818
|
+
constructor(text, onWarn) {
|
|
2819
|
+
this.list = turnsOf(text, onWarn);
|
|
2820
|
+
}
|
|
2821
|
+
/** The turns of the next `n` glyphs, the `own` list of their `<tspan>`
|
|
2822
|
+
* winning where it has one. */
|
|
2823
|
+
take(n, own2 = []) {
|
|
2824
|
+
const out = Array.from({ length: n }, (_, i) => valueAt(own2, i) ?? valueAt(this.list, this.at + i) ?? 0);
|
|
2825
|
+
this.at += n;
|
|
2826
|
+
return out;
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
function turnRuns(line, turns) {
|
|
2830
|
+
if (turns.every((t) => t === 0)) return;
|
|
2831
|
+
let start = line.length;
|
|
2832
|
+
let n = turns.length;
|
|
2833
|
+
while (n > 0 && start > 0) n -= Array.from(line[--start].text).length;
|
|
2834
|
+
const out = [];
|
|
2835
|
+
let g = Math.min(n, 0);
|
|
2836
|
+
for (const run of line.slice(start)) {
|
|
2837
|
+
if (run.text === "") {
|
|
2838
|
+
out.push(run);
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
const { rotate: own2, ...upright } = run;
|
|
2842
|
+
let last;
|
|
2843
|
+
for (const glyph of Array.from(run.text)) {
|
|
2844
|
+
const turn = g < 0 ? own2 ?? 0 : turns[g] ?? 0;
|
|
2845
|
+
g++;
|
|
2846
|
+
if (turn === last) out[out.length - 1].text += glyph;
|
|
2847
|
+
else out.push(turn === 0 ? { ...upright, text: glyph } : { ...upright, text: glyph, rotate: turn });
|
|
2848
|
+
last = turn;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
line.splice(start, line.length - start, ...out);
|
|
2852
|
+
}
|
|
2853
|
+
var NO_MOVES = { y: [], dy: [] };
|
|
2854
|
+
function movesOf(el, onWarn) {
|
|
2855
|
+
const y = readList(el, "y", onWarn);
|
|
2856
|
+
const dy = readList(el, "dy", onWarn);
|
|
2857
|
+
const perGlyph = y.length > 1 || dy.slice(1).some((v) => v !== 0);
|
|
2858
|
+
if (perGlyph && collapses(el)) {
|
|
2859
|
+
const named = ["y", "dy"].filter((n) => el.hasAttribute(n)).map((n) => `${n}="${el.getAttribute(n)}"`);
|
|
2860
|
+
onWarn(`<${el.tagName.toLowerCase()} ${named.join(" ")}> is read only to its first value: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2861
|
+
return { y: y.slice(0, 1), dy: dy.slice(0, 1) };
|
|
2862
|
+
}
|
|
2863
|
+
return { y, dy };
|
|
2864
|
+
}
|
|
2865
|
+
var TextMoves = class {
|
|
2866
|
+
moves;
|
|
2867
|
+
at = 0;
|
|
2868
|
+
constructor(text, onWarn) {
|
|
2869
|
+
const { y, dy } = movesOf(text, onWarn);
|
|
2870
|
+
this.moves = { y: [void 0, ...y.slice(1)], dy: [writtenByWeasel(text) ? 0 : dy[0] ?? 0, ...dy.slice(1)] };
|
|
2871
|
+
}
|
|
2872
|
+
/** The values over the next `n` glyphs. */
|
|
2873
|
+
take(n) {
|
|
2874
|
+
const out = { y: this.moves.y.slice(this.at, this.at + n), dy: this.moves.dy.slice(this.at, this.at + n) };
|
|
2875
|
+
this.at += n;
|
|
2876
|
+
return out;
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
function risesOver(glyphs, own2, inherited2, tracker, placed) {
|
|
2880
|
+
const out = [];
|
|
2881
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
2882
|
+
if (i > 0 || !placed) {
|
|
2883
|
+
tracker.move(i < own2.y.length ? own2.y[i] : inherited2.y[i], (i < own2.dy.length ? own2.dy[i] : inherited2.dy[i]) ?? 0);
|
|
2884
|
+
}
|
|
2885
|
+
tracker.land(glyphs[i]);
|
|
2886
|
+
out.push(tracker.rise);
|
|
2887
|
+
}
|
|
2888
|
+
return out;
|
|
2889
|
+
}
|
|
2890
|
+
function ownShift(run, style) {
|
|
2891
|
+
if (run.baselineShift != null) return run.baselineShift;
|
|
2892
|
+
if (!run.script) return 0;
|
|
2893
|
+
const face = resolveRunFace(run, resolveTextStyle(style));
|
|
2894
|
+
return scriptMetricsFor(face.fontFamily, face.fontWeight, face.fontStyle)[run.script].shift;
|
|
2895
|
+
}
|
|
2896
|
+
function raiseRuns(line, rises, fontSize, style) {
|
|
2897
|
+
if (!(fontSize > 0) || rises.every((r) => Math.abs(r) < 1e-9)) return;
|
|
2898
|
+
let start = line.length;
|
|
2899
|
+
for (let n = rises.length; n > 0 && start > 0; ) n -= Array.from(line[--start].text).length;
|
|
2900
|
+
const out = [];
|
|
2901
|
+
let g = 0;
|
|
2902
|
+
for (const run of line.slice(start)) {
|
|
2903
|
+
if (run.text === "") {
|
|
2904
|
+
out.push(run);
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
let last;
|
|
2908
|
+
for (const glyph of Array.from(run.text)) {
|
|
2909
|
+
const em = Math.round((rises[g++] ?? 0) / fontSize * 1e6) / 1e6;
|
|
2910
|
+
if (em === last) out[out.length - 1].text += glyph;
|
|
2911
|
+
else out.push(em === 0 ? { ...run, text: glyph } : { ...run, text: glyph, baselineShift: ownShift(run, style) + em });
|
|
2912
|
+
last = em;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
line.splice(start, line.length - start, ...out);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// src/textLang.ts
|
|
2919
|
+
var XML_NS2 = "http://www.w3.org/XML/1998/namespace";
|
|
2920
|
+
function ownLang(el) {
|
|
2921
|
+
return el.getAttributeNS(XML_NS2, "lang") ?? el.getAttribute("xml:lang") ?? el.getAttribute("lang");
|
|
2922
|
+
}
|
|
2923
|
+
function inheritedLang(el) {
|
|
2538
2924
|
for (let e = el; e; e = e.parentElement) {
|
|
2539
|
-
const v =
|
|
2540
|
-
if (v
|
|
2541
|
-
if (v === "default") return false;
|
|
2925
|
+
const v = ownLang(e);
|
|
2926
|
+
if (v !== null) return v.trim() || void 0;
|
|
2542
2927
|
}
|
|
2543
|
-
return
|
|
2928
|
+
return void 0;
|
|
2544
2929
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2930
|
+
|
|
2931
|
+
// src/readRuns.ts
|
|
2932
|
+
var glyphCount = (text) => Array.from(text).length;
|
|
2933
|
+
function readRuns(el, tracker, inheritedShift, textStyle, gradients, leafStyle, onWarn) {
|
|
2934
|
+
const runLines = [[]];
|
|
2935
|
+
const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
|
|
2936
|
+
const moves = new TextMoves(el, onWarn);
|
|
2937
|
+
const turns = new TextTurns(el, onWarn);
|
|
2938
|
+
const turn = (text, own2) => turnRuns(runLines[runLines.length - 1], turns.take(glyphCount(text), own2));
|
|
2939
|
+
const raise = (text, own2, inherited2, placed) => raiseRuns(runLines[runLines.length - 1], risesOver(Array.from(text), own2, inherited2, tracker, placed), fontSize, textStyle);
|
|
2940
|
+
let leading = 0;
|
|
2941
|
+
let lead = 0;
|
|
2942
|
+
if (holdsOnlyText(el) && (el.hasAttribute("dx") || el.hasAttribute("x"))) {
|
|
2943
|
+
const t = el.textContent ?? "";
|
|
2944
|
+
if (t) {
|
|
2945
|
+
lead = placeTspanRun(el, { text: t, ...inheritedShift }, runLines, textStyle, onWarn);
|
|
2946
|
+
raise(t, NO_MOVES, moves.take(glyphCount(t)), false);
|
|
2947
|
+
turn(t);
|
|
2948
|
+
}
|
|
2949
|
+
} else {
|
|
2950
|
+
const pieces = Array.from(el.childNodes).flatMap((child) => {
|
|
2951
|
+
if (child.nodeType === 1 && child.tagName.toLowerCase() === "tspan") {
|
|
2952
|
+
const sp = child;
|
|
2953
|
+
return [{ tspan: sp, run: readTspanRun(sp, gradients, leafStyle, onWarn) }];
|
|
2954
|
+
}
|
|
2955
|
+
if (child.nodeType === 1) onWarn(`<text> child <${child.tagName}> not supported; flattening text content`);
|
|
2956
|
+
else if (child.nodeType !== 3) return [];
|
|
2957
|
+
const t = child.textContent ?? "";
|
|
2958
|
+
return t ? [{ tspan: null, run: { text: t, ...inheritedShift } }] : [];
|
|
2959
|
+
});
|
|
2960
|
+
const shifts = new TextShifts(el, onWarn, pieces, textStyle);
|
|
2961
|
+
for (const { tspan: sp, run } of pieces) {
|
|
2962
|
+
if (!sp) {
|
|
2963
|
+
lead += shifts.place(run, runLines, textStyle);
|
|
2964
|
+
raise(run.text, NO_MOVES, moves.take(glyphCount(run.text)), false);
|
|
2965
|
+
turn(run.text);
|
|
2555
2966
|
continue;
|
|
2556
2967
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2968
|
+
const inherited2 = moves.take(glyphCount(run.text));
|
|
2969
|
+
const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
|
|
2970
|
+
const lines = tracker.startsLine(sp, afterText, { y: inherited2.y[0], dy: inherited2.dy[0] });
|
|
2971
|
+
if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
|
|
2972
|
+
else for (let n = 0; n < lines; n++) runLines.push([]);
|
|
2973
|
+
lead += placeTspanRun(sp, run, runLines, textStyle, onWarn, shifts.take(run));
|
|
2974
|
+
raise(run.text, movesOf(sp, onWarn), inherited2, true);
|
|
2975
|
+
turn(run.text, turnsOf(sp, onWarn));
|
|
2560
2976
|
}
|
|
2561
|
-
run.text = out;
|
|
2562
|
-
}
|
|
2563
|
-
for (let i = runs.length - 1; i >= 0; i--) {
|
|
2564
|
-
if (runs[i].text === "") continue;
|
|
2565
|
-
if (runs[i].text.endsWith(" ")) runs[i].text = runs[i].text.slice(0, -1);
|
|
2566
|
-
break;
|
|
2567
2977
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2978
|
+
if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
|
|
2979
|
+
return { runs: joinLines(runLines, leading), lead };
|
|
2980
|
+
}
|
|
2981
|
+
function readTspanRun(el, gradients, style, onWarn) {
|
|
2982
|
+
const text = el.textContent ?? "";
|
|
2983
|
+
const run = { text };
|
|
2984
|
+
const fw = ownValue(el, "font-weight", onWarn);
|
|
2985
|
+
if (fw === "bold" || fw === "bolder" || fw === 700) run.bold = true;
|
|
2986
|
+
else if (fw === "normal") run.fontWeight = 400;
|
|
2987
|
+
else if (typeof fw === "number") run.fontWeight = fw;
|
|
2988
|
+
if (ownValue(el, "font-style", onWarn) === "italic") run.italic = true;
|
|
2989
|
+
const ff = ownValue(el, "font-family", onWarn);
|
|
2990
|
+
if (ff) run.fontFamily = ff;
|
|
2991
|
+
const size = ownValue(el, "font-size", onWarn);
|
|
2992
|
+
if (size?.fontSize != null) run.fontSize = size.fontSize;
|
|
2993
|
+
if (size?.fontScale != null) run.fontScale = size.fontScale;
|
|
2994
|
+
const tspanStyle = deriveStyle(style, el);
|
|
2995
|
+
const ls = ownValue(el, "letter-spacing", onWarn, fontSizeOf(tspanStyle));
|
|
2996
|
+
if (ls != null) run.letterSpacing = ls;
|
|
2997
|
+
const shift = ownValue(el, "baseline-shift", onWarn);
|
|
2998
|
+
if (shift?.script != null) run.script = shift.script;
|
|
2999
|
+
if (shift?.baselineShift != null) run.baselineShift = shift.baselineShift;
|
|
3000
|
+
const decoration = ownValue(el, "text-decoration", onWarn);
|
|
3001
|
+
if (decoration?.underline) run.underline = true;
|
|
3002
|
+
if (decoration?.strikethrough) run.strikethrough = true;
|
|
3003
|
+
if (decoration?.overline) run.overline = true;
|
|
3004
|
+
const transform = ownValue(el, "text-transform", onWarn);
|
|
3005
|
+
if (transform) run.textTransform = transform;
|
|
3006
|
+
const variant = ownValue(el, "font-variant", onWarn);
|
|
3007
|
+
if (variant) run.fontVariantCaps = variant;
|
|
3008
|
+
const lang = ownLang(el)?.trim();
|
|
3009
|
+
if (lang) run.lang = lang;
|
|
3010
|
+
const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
|
|
3011
|
+
if (fillAttr) {
|
|
3012
|
+
const parsed = readProperty("fill", fillAttr);
|
|
3013
|
+
if (parsed?.kind === "none") {
|
|
3014
|
+
run.fill = null;
|
|
3015
|
+
} else if (parsed?.kind === "solid") {
|
|
3016
|
+
run.fill = { paint: { kind: "solid", color: parsed.color } };
|
|
3017
|
+
} else if (parsed?.kind === "ref") {
|
|
3018
|
+
const paint = gradients.get(parsed.id);
|
|
3019
|
+
if (paint) run.fill = { paint };
|
|
3020
|
+
else if (parsed.fallback) run.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
|
|
3021
|
+
}
|
|
2570
3022
|
}
|
|
3023
|
+
const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
|
|
3024
|
+
if (stroke) run.stroke = stroke;
|
|
3025
|
+
return run;
|
|
2571
3026
|
}
|
|
2572
3027
|
|
|
2573
3028
|
// src/fillPaint.ts
|
|
@@ -2629,22 +3084,25 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2629
3084
|
return Number.isFinite(n) ? n : fallback;
|
|
2630
3085
|
};
|
|
2631
3086
|
const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
|
|
2632
|
-
warnUnreadPositions(el, onWarn);
|
|
2633
3087
|
const ax = readLength(el, "x", "x", 0, onWarn);
|
|
2634
3088
|
const ay = readLength(el, "y", "y", 0, onWarn);
|
|
2635
3089
|
const leafStyle = deriveStyle(style, el);
|
|
2636
3090
|
const textStyle = readTextStyle(leafStyle, onWarn);
|
|
3091
|
+
const lang = inheritedLang(el);
|
|
3092
|
+
if (lang) textStyle.lang = lang;
|
|
2637
3093
|
const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
|
|
2638
3094
|
const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
|
|
2639
3095
|
const dominantBaseline = el.getAttribute("dominant-baseline");
|
|
2640
3096
|
const laid = el.hasAttribute(LAID_ATTR);
|
|
2641
3097
|
const explicitTopAnchor = laid || dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
2642
|
-
const topY = explicitTopAnchor ? ay : ay - fontSize;
|
|
2643
3098
|
const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
|
|
2644
3099
|
const tracker = new LineTracker(el, onWarn);
|
|
2645
|
-
const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle
|
|
3100
|
+
const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle, gradients, leafStyle, onWarn);
|
|
2646
3101
|
const plain = runs.map((r) => r.text).join("");
|
|
2647
|
-
const
|
|
3102
|
+
const firstY = laid ? ay : tracker.firstBaseline ?? ay;
|
|
3103
|
+
const topY = explicitTopAnchor ? firstY : firstY - fontSize;
|
|
3104
|
+
const stated = numberAttr(el, LINE_HEIGHT_ATTR);
|
|
3105
|
+
const pitched = laid ? stated : tracker.lineHeight(fontSize, stated);
|
|
2648
3106
|
if (pitched !== void 0) textStyle.lineHeight = pitched;
|
|
2649
3107
|
const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
|
|
2650
3108
|
const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
|
|
@@ -2665,7 +3123,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2665
3123
|
(r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
|
|
2666
3124
|
);
|
|
2667
3125
|
if (hasStyling) node.runs = runs;
|
|
2668
|
-
if (textStyle.direction === void 0 && !el
|
|
3126
|
+
if (textStyle.direction === void 0 && !writtenByWeasel(el)) {
|
|
2669
3127
|
textStyle.direction = "ltr";
|
|
2670
3128
|
}
|
|
2671
3129
|
if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
|
|
@@ -2681,85 +3139,6 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2681
3139
|
placeTextNode(node, multiply(ctm, localTransform), onWarn);
|
|
2682
3140
|
return node;
|
|
2683
3141
|
}
|
|
2684
|
-
function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafStyle, onWarn) {
|
|
2685
|
-
const runLines = [[]];
|
|
2686
|
-
let leading = 0;
|
|
2687
|
-
let lead = 0;
|
|
2688
|
-
const push = (run) => {
|
|
2689
|
-
runLines[runLines.length - 1].push(run);
|
|
2690
|
-
};
|
|
2691
|
-
if (holdsOnlyText(el) && el.hasAttribute("dx")) {
|
|
2692
|
-
const t = el.textContent ?? "";
|
|
2693
|
-
if (t) lead = placeTspanRun(el, { text: t, ...inheritedShift }, runLines, letterSpacing, onWarn);
|
|
2694
|
-
} else for (let i = 0; i < el.childNodes.length; i++) {
|
|
2695
|
-
const child = el.childNodes[i];
|
|
2696
|
-
if (child.nodeType === 3) {
|
|
2697
|
-
const t = child.textContent ?? "";
|
|
2698
|
-
if (!t) continue;
|
|
2699
|
-
push({ text: t, ...inheritedShift });
|
|
2700
|
-
} else if (child.nodeType === 1) {
|
|
2701
|
-
const sp = child;
|
|
2702
|
-
if (sp.tagName.toLowerCase() !== "tspan") {
|
|
2703
|
-
onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
|
|
2704
|
-
const t = sp.textContent ?? "";
|
|
2705
|
-
if (t) push({ text: t, ...inheritedShift });
|
|
2706
|
-
continue;
|
|
2707
|
-
}
|
|
2708
|
-
warnUnreadPositions(sp, onWarn);
|
|
2709
|
-
const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
|
|
2710
|
-
const lines = tracker.startsLine(sp, afterText);
|
|
2711
|
-
if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
|
|
2712
|
-
else for (let n = 0; n < lines; n++) runLines.push([]);
|
|
2713
|
-
lead += placeTspanRun(sp, readTspanRun(sp, gradients, leafStyle, onWarn), runLines, letterSpacing, onWarn);
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
|
|
2717
|
-
return { runs: joinLines(runLines, leading), lead };
|
|
2718
|
-
}
|
|
2719
|
-
function readTspanRun(el, gradients, style, onWarn) {
|
|
2720
|
-
const text = el.textContent ?? "";
|
|
2721
|
-
const run = { text };
|
|
2722
|
-
const fw = ownValue(el, "font-weight", onWarn);
|
|
2723
|
-
if (fw === "bold" || fw === "bolder" || fw === 700) run.bold = true;
|
|
2724
|
-
else if (fw === "normal") run.fontWeight = 400;
|
|
2725
|
-
else if (typeof fw === "number") run.fontWeight = fw;
|
|
2726
|
-
if (ownValue(el, "font-style", onWarn) === "italic") run.italic = true;
|
|
2727
|
-
const ff = ownValue(el, "font-family", onWarn);
|
|
2728
|
-
if (ff) run.fontFamily = ff;
|
|
2729
|
-
const size = ownValue(el, "font-size", onWarn);
|
|
2730
|
-
if (size?.fontSize != null) run.fontSize = size.fontSize;
|
|
2731
|
-
if (size?.fontScale != null) run.fontScale = size.fontScale;
|
|
2732
|
-
const tspanStyle = deriveStyle(style, el);
|
|
2733
|
-
const ls = ownValue(el, "letter-spacing", onWarn, fontSizeOf(tspanStyle));
|
|
2734
|
-
if (ls != null) run.letterSpacing = ls;
|
|
2735
|
-
const shift = ownValue(el, "baseline-shift", onWarn);
|
|
2736
|
-
if (shift?.script != null) run.script = shift.script;
|
|
2737
|
-
if (shift?.baselineShift != null) run.baselineShift = shift.baselineShift;
|
|
2738
|
-
const decoration = ownValue(el, "text-decoration", onWarn);
|
|
2739
|
-
if (decoration?.underline) run.underline = true;
|
|
2740
|
-
if (decoration?.strikethrough) run.strikethrough = true;
|
|
2741
|
-
if (decoration?.overline) run.overline = true;
|
|
2742
|
-
const transform = ownValue(el, "text-transform", onWarn);
|
|
2743
|
-
if (transform) run.textTransform = transform;
|
|
2744
|
-
const variant = ownValue(el, "font-variant", onWarn);
|
|
2745
|
-
if (variant) run.fontVariantCaps = variant;
|
|
2746
|
-
const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
|
|
2747
|
-
if (fillAttr) {
|
|
2748
|
-
const parsed = readProperty("fill", fillAttr);
|
|
2749
|
-
if (parsed?.kind === "none") {
|
|
2750
|
-
run.fill = null;
|
|
2751
|
-
} else if (parsed?.kind === "solid") {
|
|
2752
|
-
run.fill = { paint: { kind: "solid", color: parsed.color } };
|
|
2753
|
-
} else if (parsed?.kind === "ref") {
|
|
2754
|
-
const paint = gradients.get(parsed.id);
|
|
2755
|
-
if (paint) run.fill = { paint };
|
|
2756
|
-
else if (parsed.fallback) run.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
|
|
2760
|
-
if (stroke) run.stroke = stroke;
|
|
2761
|
-
return run;
|
|
2762
|
-
}
|
|
2763
3142
|
function readTextStyle(style, onWarn) {
|
|
2764
3143
|
const out = {};
|
|
2765
3144
|
const size = styleValue(style, "font-size", onWarn);
|
|
@@ -2982,20 +3361,9 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
|
|
|
2982
3361
|
if (parts.some((p) => JSON.stringify(p.style) !== JSON.stringify(first))) {
|
|
2983
3362
|
onWarn(`<marker id="${id}"> paints its parts differently; all take the first part's paint`);
|
|
2984
3363
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
coordCount += p.path.coords.length;
|
|
2989
|
-
}
|
|
2990
|
-
const commands = new Uint8Array(commandCount);
|
|
2991
|
-
const coords = new Float32Array(coordCount);
|
|
2992
|
-
let ci = 0, pi = 0;
|
|
2993
|
-
for (const p of parts) {
|
|
2994
|
-
commands.set(p.path.commands, ci);
|
|
2995
|
-
ci += p.path.commands.length;
|
|
2996
|
-
coords.set(p.path.coords, pi);
|
|
2997
|
-
pi += p.path.coords.length;
|
|
2998
|
-
}
|
|
3364
|
+
const joined = joinPaths(parts.map((p) => p.path));
|
|
3365
|
+
const commands = joined.commands;
|
|
3366
|
+
const coords = worldCoords(joined);
|
|
2999
3367
|
const fillRule = parts[0].path.fillRule ?? "nonzero";
|
|
3000
3368
|
const inset = markerInsetAttr(el);
|
|
3001
3369
|
const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
|
|
@@ -3049,6 +3417,27 @@ function groupPaint(el, from, style, ctm, gradients, onWarn) {
|
|
|
3049
3417
|
if (stroke) paint.stroke = strokeThroughMatrix(stroke, ctm);
|
|
3050
3418
|
return { paint: { ...paint, ...inheritsFrom(el, from) }, children };
|
|
3051
3419
|
}
|
|
3420
|
+
var BOOLEAN_ATTR = "data-weasel-boolean";
|
|
3421
|
+
var OPS = /* @__PURE__ */ new Set(["union", "intersect", "subtract", "exclude"]);
|
|
3422
|
+
function booleanAttrOf(el) {
|
|
3423
|
+
const op = el.tagName.toLowerCase() === "g" ? el.getAttribute(BOOLEAN_ATTR) : null;
|
|
3424
|
+
return op !== null && OPS.has(op) ? { boolean: op } : {};
|
|
3425
|
+
}
|
|
3426
|
+
function derivedPathIn(tree, id) {
|
|
3427
|
+
if (tree.derivedPathOf) return tree.derivedPathOf(id);
|
|
3428
|
+
if (tree.overrides === void 0) return null;
|
|
3429
|
+
return derivedDepOf(documentOf(tree), id)?.path ?? null;
|
|
3430
|
+
}
|
|
3431
|
+
function asBooleanLeaf(node, path, place) {
|
|
3432
|
+
if (path === null || path.kind === "polygon" && path.commands.length === 0) {
|
|
3433
|
+
return { ...node, data: { ...node.data, path: void 0 } };
|
|
3434
|
+
}
|
|
3435
|
+
return { ...node, data: { ...node.data, path }, pose: place(boundsOfPath(path)) };
|
|
3436
|
+
}
|
|
3437
|
+
function booleanGroupSvg(self, operands, data, paint) {
|
|
3438
|
+
const held = { kind: "group", children: operands, boolean: booleanOpOf(data), hidden: true };
|
|
3439
|
+
return { kind: "group", children: self ? [self, held] : [held], ...paint };
|
|
3440
|
+
}
|
|
3052
3441
|
|
|
3053
3442
|
// src/parse.ts
|
|
3054
3443
|
var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
|
|
@@ -3073,10 +3462,7 @@ function parseSvg(svg, opts = {}) {
|
|
|
3073
3462
|
}
|
|
3074
3463
|
bindStylesheets(doc, mediaEnvironmentFor(root, opts.media), onWarn);
|
|
3075
3464
|
const namespaces = opts.namespaces ?? {};
|
|
3076
|
-
const uriToPrefix =
|
|
3077
|
-
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
3078
|
-
uriToPrefix.set(uri, prefix);
|
|
3079
|
-
}
|
|
3465
|
+
const uriToPrefix = new Map(Object.entries(namespaces).map(([prefix, uri]) => [uri, prefix]));
|
|
3080
3466
|
const documentMeta = collectDocumentMeta(root, uriToPrefix);
|
|
3081
3467
|
const gradients = collectGradients(root, onWarn);
|
|
3082
3468
|
for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
|
|
@@ -3190,7 +3576,7 @@ function parseChildren(parent, ctm, style, from, gradients, clips, onWarn, uriTo
|
|
|
3190
3576
|
const ns = el.namespaceURI;
|
|
3191
3577
|
if (ns && ns !== "http://www.w3.org/2000/svg" || isStrokeOutline(el)) continue;
|
|
3192
3578
|
const hidden = isHiddenElement(el, style);
|
|
3193
|
-
if (hidden && !keepHidden) continue;
|
|
3579
|
+
if (hidden && !keepHidden && booleanAttrOf(el).boolean === void 0) continue;
|
|
3194
3580
|
const node = unwrapStrokeGroup(el, parseElement(el, ctm, style, from, gradients, clips, onWarn, uriToPrefix, keepHidden));
|
|
3195
3581
|
if (!node) continue;
|
|
3196
3582
|
const parsed = Array.isArray(node) ? node : [node];
|
|
@@ -3299,7 +3685,7 @@ function parseGroup(el, ctm, style, from, gradients, clips, onWarn, uriToPrefix,
|
|
|
3299
3685
|
const paint = groupPaint(el, from, childStyle, childCtm, gradients, onWarn);
|
|
3300
3686
|
const children = parseChildren(el, childCtm, childStyle, paint.children, gradients, clips, onWarn, uriToPrefix, keepHidden);
|
|
3301
3687
|
const opacity = ownValue(el, "opacity");
|
|
3302
|
-
const group = { kind: "group", children, ...paint.paint };
|
|
3688
|
+
const group = { kind: "group", children, ...paint.paint, ...booleanAttrOf(el) };
|
|
3303
3689
|
const clip = clipFor(el, childCtm, clips, onWarn);
|
|
3304
3690
|
if (clip) group.clip = clip;
|
|
3305
3691
|
if (opacity != null) group.opacity = opacity;
|
|
@@ -3410,7 +3796,8 @@ var closedStroke = (stroke) => stroke && capsEnds(stroke) ? withoutEndMarkers(st
|
|
|
3410
3796
|
var isClosed = (part) => splitByClosure(part).open === null;
|
|
3411
3797
|
function samePath(a, b) {
|
|
3412
3798
|
if (a === b) return true;
|
|
3413
|
-
|
|
3799
|
+
const ac = worldCoords(a), bc = worldCoords(b);
|
|
3800
|
+
return a.commands.length === b.commands.length && ac.length === bc.length && a.commands.every((c, i) => c === b.commands[i]) && ac.every((v, i) => v === bc[i]);
|
|
3414
3801
|
}
|
|
3415
3802
|
function strokeParts(path, stroke, filled, closed, open) {
|
|
3416
3803
|
const none = { kind: "none" };
|
|
@@ -3598,11 +3985,20 @@ function fitLeafXml(xml, inherits, painted) {
|
|
|
3598
3985
|
}
|
|
3599
3986
|
function textXml(node, registry, namespaces, warn, given) {
|
|
3600
3987
|
const layout = given && layoutMeasured(given, node.text) ? given : null;
|
|
3988
|
+
const resolved = resolveTextStyle(node.style);
|
|
3601
3989
|
const x = node.x + anchorOffset(node.style, node.width, node.text);
|
|
3990
|
+
const leading = layout ? void 0 : halfLeadingFor(
|
|
3991
|
+
resolved.fontFamily,
|
|
3992
|
+
numericWeight(resolved.fontWeight),
|
|
3993
|
+
resolved.fontStyle,
|
|
3994
|
+
resolved.fontSize,
|
|
3995
|
+
resolved.lineHeight
|
|
3996
|
+
);
|
|
3602
3997
|
const attrs = [
|
|
3603
3998
|
`x="${trimNumber(x)}"`,
|
|
3604
3999
|
`y="${trimNumber(node.y)}"`,
|
|
3605
4000
|
layout ? `${LAID_ATTR}="lines"` : `dominant-baseline="text-before-edge"`,
|
|
4001
|
+
...leading ? [`dy="${trimNumber(leading)}"`] : [],
|
|
3606
4002
|
// SVG's default whitespace handling would collapse indentation and runs
|
|
3607
4003
|
// of spaces away on re-import.
|
|
3608
4004
|
`xml:space="preserve"`,
|
|
@@ -3615,6 +4011,7 @@ function textXml(node, registry, namespaces, warn, given) {
|
|
|
3615
4011
|
if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
|
|
3616
4012
|
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
3617
4013
|
if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
|
|
4014
|
+
if (style?.lang) attrs.push(`xml:lang="${escapeAttr(style.lang)}"`);
|
|
3618
4015
|
const direction = svgDirection(style, node.text);
|
|
3619
4016
|
if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
|
|
3620
4017
|
let anchor = "start";
|
|
@@ -3642,7 +4039,7 @@ function textXml(node, registry, namespaces, warn, given) {
|
|
|
3642
4039
|
attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
|
|
3643
4040
|
if (!layout) warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
|
|
3644
4041
|
}
|
|
3645
|
-
if (
|
|
4042
|
+
if (style?.lineHeight != null) attrs.push(`${LINE_HEIGHT_ATTR}="${trimNumber(style.lineHeight)}"`);
|
|
3646
4043
|
if (node.fill !== void 0) {
|
|
3647
4044
|
const paint = node.fill?.paint;
|
|
3648
4045
|
if (!paint) attrs.push('fill="none"');
|
|
@@ -3658,7 +4055,6 @@ function textXml(node, registry, namespaces, warn, given) {
|
|
|
3658
4055
|
const frame = boxTransformAttr(node);
|
|
3659
4056
|
if (frame) attrs.push(frame);
|
|
3660
4057
|
const pieces = node.runs && node.runs.length > 0 ? node.runs.map((r) => ({ text: r.text, attrs: runAttrs(r, style, registry, warn) })) : [{ text: node.text, attrs: null }];
|
|
3661
|
-
const resolved = resolveTextStyle(style);
|
|
3662
4058
|
const body = layout ? laidLinesXml(pieces, layout, { anchor, rtl: direction === "rtl" }) : linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
|
|
3663
4059
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3664
4060
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
@@ -3676,6 +4072,7 @@ function runAttrs(run, style, registry, warn) {
|
|
|
3676
4072
|
if (run.italic) attrs.push(`font-style="italic"`);
|
|
3677
4073
|
if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
|
|
3678
4074
|
if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
|
|
4075
|
+
if (run.lang) attrs.push(`xml:lang="${escapeAttr(run.lang)}"`);
|
|
3679
4076
|
const face = scriptSizeFace(run, style);
|
|
3680
4077
|
const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
|
|
3681
4078
|
const scale = run.fontScale ?? presetScale;
|
|
@@ -3699,6 +4096,7 @@ function runAttrs(run, style, registry, warn) {
|
|
|
3699
4096
|
}
|
|
3700
4097
|
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
3701
4098
|
if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
|
|
4099
|
+
if (run.rotate) attrs.push(`rotate="${trimNumber(run.rotate)}"`);
|
|
3702
4100
|
return attrs;
|
|
3703
4101
|
}
|
|
3704
4102
|
function textDecorationValue(underline, strikethrough, overline) {
|
|
@@ -3869,6 +4267,7 @@ function groupXml(node, registry, namespaces, warn, layoutOf, painted = NOTHING_
|
|
|
3869
4267
|
if (node.opacity != null && node.opacity !== 1) {
|
|
3870
4268
|
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3871
4269
|
}
|
|
4270
|
+
if (node.boolean) attrs.push(`${BOOLEAN_ATTR}="${node.boolean}"`);
|
|
3872
4271
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3873
4272
|
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
3874
4273
|
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn, layoutOf, below)).join("");
|
|
@@ -4030,6 +4429,12 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
|
|
|
4030
4429
|
const takes = takesOf(n, from, k);
|
|
4031
4430
|
if (n.kind === "group") {
|
|
4032
4431
|
const [own2, kids, ...more] = n.children;
|
|
4432
|
+
if (own2 && own2.kind !== "group" && kids?.kind === "group" && kids.boolean) {
|
|
4433
|
+
return visitMark(n, own2, kids, more, parentId, k, takes);
|
|
4434
|
+
}
|
|
4435
|
+
if (own2?.kind === "group" && own2.boolean) {
|
|
4436
|
+
return visitMark(n, emptyResultOf(n), own2, [kids, ...more].filter((c) => c !== void 0), parentId, k, takes, true);
|
|
4437
|
+
}
|
|
4033
4438
|
if (own2 && own2.kind !== "group" && (kids === void 0 || kids.kind === "group") && options.mark?.(n)) {
|
|
4034
4439
|
if (n.clip) return visit(clippingGroup(n), parentId, inherited2, from);
|
|
4035
4440
|
return visitMark(n, own2, kids, more, parentId, k, takes);
|
|
@@ -4059,25 +4464,29 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
|
|
|
4059
4464
|
const { pose, data } = leafDraft(n, takes, k);
|
|
4060
4465
|
return pushLeaf(n, parentId, pose, data);
|
|
4061
4466
|
};
|
|
4062
|
-
const visitMark = (n, own2, kids, more, parentId, k, takes) => {
|
|
4467
|
+
const visitMark = (n, own2, kids, more, parentId, k, takes, empty = false) => {
|
|
4063
4468
|
const inner = groupTakes(n, takes, k);
|
|
4064
4469
|
const pose = visit(own2, parentId, k, inner);
|
|
4065
4470
|
const mark = pose === null ? null : drafts.at(-1);
|
|
4066
4471
|
const under = mark?.id ?? parentId;
|
|
4067
4472
|
if (mark && n.hidden) mark.hidden = true;
|
|
4068
4473
|
if (mark && kids?.clip) mark.clip = "outline";
|
|
4474
|
+
if (mark && kids?.boolean) mark.boolean = kids.boolean;
|
|
4069
4475
|
const poses = [pose];
|
|
4070
4476
|
if (kids) {
|
|
4071
4477
|
const at = drafts.length;
|
|
4072
4478
|
const kk = k * (kids.opacity ?? 1);
|
|
4073
4479
|
const below = groupTakes(kids, takesOf(kids, inner, kk), kk);
|
|
4074
4480
|
for (const c of kids.children) poses.push(visit(c, under, kk, below));
|
|
4075
|
-
if (kids.hidden) {
|
|
4481
|
+
if (kids.hidden && !kids.boolean) {
|
|
4076
4482
|
for (const d of drafts.slice(at)) if (d.parentId === under) d.hidden = true;
|
|
4077
4483
|
}
|
|
4078
4484
|
}
|
|
4079
4485
|
for (const c of more) poses.push(visit(c, under, k, inner));
|
|
4080
|
-
return unionAABB(poses);
|
|
4486
|
+
if (!empty) return unionAABB(poses);
|
|
4487
|
+
const box = unionAABB(poses.slice(1));
|
|
4488
|
+
if (mark && box) mark.pose = box;
|
|
4489
|
+
return box;
|
|
4081
4490
|
};
|
|
4082
4491
|
for (const n of nodes) visit(n, null, 1, NO_TAKES);
|
|
4083
4492
|
return drafts;
|
|
@@ -4153,6 +4562,16 @@ function containerPaint(n, takes, box, k) {
|
|
|
4153
4562
|
...takes.stroke ? { stroke: INHERIT } : stroke ? { stroke: fadeStroke(stroke, k) } : {}
|
|
4154
4563
|
};
|
|
4155
4564
|
}
|
|
4565
|
+
function emptyResultOf(n) {
|
|
4566
|
+
const path = { kind: "polygon", commands: new Uint8Array(0), coords: new Float32Array(0), fillRule: "nonzero" };
|
|
4567
|
+
return {
|
|
4568
|
+
kind: "path",
|
|
4569
|
+
path,
|
|
4570
|
+
fill: n.fill ?? { kind: "none" },
|
|
4571
|
+
...n.stroke ? { stroke: n.stroke } : {},
|
|
4572
|
+
...n.inherits ? { inherits: n.inherits } : {}
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4156
4575
|
function boxOfPath(path) {
|
|
4157
4576
|
const b = boundsOfPath(path);
|
|
4158
4577
|
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
@@ -4180,6 +4599,10 @@ function scaleLeafData(data, scale) {
|
|
|
4180
4599
|
}
|
|
4181
4600
|
return out;
|
|
4182
4601
|
}
|
|
4602
|
+
function asBooleanGroup(op, data) {
|
|
4603
|
+
const { path: _path, ...rest } = data;
|
|
4604
|
+
return { ...booleanGroupFields(), data: { ...rest, boolean: op } };
|
|
4605
|
+
}
|
|
4183
4606
|
async function unpackSvgFiles(files, ctx) {
|
|
4184
4607
|
const layer = ctx.scene.layers[0]?.id ?? "default";
|
|
4185
4608
|
let index = 0;
|
|
@@ -4223,14 +4646,19 @@ async function unpackSvgFiles(files, ctx) {
|
|
|
4223
4646
|
width: p.width * scale,
|
|
4224
4647
|
height: p.height * scale
|
|
4225
4648
|
});
|
|
4649
|
+
const placed = localizeWorldPoses(
|
|
4650
|
+
drafts.map((d) => ({ id: d.id, parent: d.parentId, pose: place(d.pose) })),
|
|
4651
|
+
ctx.scene.composition
|
|
4652
|
+
);
|
|
4226
4653
|
const ops = drafts.map(
|
|
4227
|
-
(d) => createInsertOp({
|
|
4654
|
+
(d, i) => createInsertOp({
|
|
4228
4655
|
node: {
|
|
4229
4656
|
id: d.id,
|
|
4230
4657
|
kind: d.kind,
|
|
4231
4658
|
layer,
|
|
4232
|
-
pose:
|
|
4659
|
+
pose: placed[i].pose,
|
|
4233
4660
|
data: d.kind === "mark" ? scaleLeafData(d.data, scale) : d.data,
|
|
4661
|
+
...d.kind === "mark" && d.boolean ? asBooleanGroup(d.boolean, scaleLeafData(d.data, scale)) : {},
|
|
4234
4662
|
parent: d.parentId,
|
|
4235
4663
|
...d.clip ? { clip: d.clip } : {},
|
|
4236
4664
|
...d.hidden ? { hidden: true } : {}
|
|
@@ -4351,12 +4779,36 @@ function svgTextFromKit(data, pose) {
|
|
|
4351
4779
|
}
|
|
4352
4780
|
function svgNodesFromKit(tree, options = {}) {
|
|
4353
4781
|
const { include, leaf, group, mark } = options;
|
|
4354
|
-
const
|
|
4355
|
-
|
|
4356
|
-
const
|
|
4782
|
+
const compose = tree.composition?.compose;
|
|
4783
|
+
const emit = (id, up, frame) => {
|
|
4784
|
+
const stored = tree.get(id);
|
|
4785
|
+
const node = stored && compose && frame ? { ...stored, pose: compose(frame, stored.pose) } : stored;
|
|
4786
|
+
const own2 = node && definesFrame(node) ? node.pose : frame;
|
|
4787
|
+
const out2 = node && (!include || include(id)) ? lower(id, node, up, own2) : null;
|
|
4357
4788
|
return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
|
|
4358
4789
|
};
|
|
4359
|
-
const
|
|
4790
|
+
const frameAbove = (id) => {
|
|
4791
|
+
if (!compose) return null;
|
|
4792
|
+
const chain = [];
|
|
4793
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
4794
|
+
let at = tree.get(id)?.parent ?? null;
|
|
4795
|
+
while (at !== null && !seen.has(at)) {
|
|
4796
|
+
seen.add(at);
|
|
4797
|
+
const n = tree.get(at);
|
|
4798
|
+
if (!n) break;
|
|
4799
|
+
chain.unshift(n);
|
|
4800
|
+
at = n.parent ?? null;
|
|
4801
|
+
}
|
|
4802
|
+
let frame = null;
|
|
4803
|
+
for (const n of chain) {
|
|
4804
|
+
const pose = frame ? compose(frame, n.pose) : n.pose;
|
|
4805
|
+
if (definesFrame(n)) frame = pose;
|
|
4806
|
+
}
|
|
4807
|
+
return frame;
|
|
4808
|
+
};
|
|
4809
|
+
const lower = (id, stored, up, frame) => {
|
|
4810
|
+
const booleans = stored.consumesChildren === true;
|
|
4811
|
+
const node = booleans ? asBooleanLeaf(stored, derivedPathIn(tree, id), (box) => compose && frame ? compose(frame, box) : box) : stored;
|
|
4360
4812
|
const own2 = node.data;
|
|
4361
4813
|
const isGroup = node.kind === "group";
|
|
4362
4814
|
const self = isGroup ? null : markInherits(
|
|
@@ -4373,7 +4825,7 @@ function svgNodesFromKit(tree, options = {}) {
|
|
|
4373
4825
|
const emitChildren = () => {
|
|
4374
4826
|
const children = [];
|
|
4375
4827
|
for (const c of kids) {
|
|
4376
|
-
const child = emit(c, below);
|
|
4828
|
+
const child = emit(c, below, frame);
|
|
4377
4829
|
if (child) children.push(child);
|
|
4378
4830
|
}
|
|
4379
4831
|
return children;
|
|
@@ -4382,6 +4834,10 @@ function svgNodesFromKit(tree, options = {}) {
|
|
|
4382
4834
|
const copies = node.copiesFromPose?.(node.pose, node) ?? [];
|
|
4383
4835
|
const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
|
|
4384
4836
|
const paint = groupPaintFromKit(own2, below, node.pose);
|
|
4837
|
+
if (booleans) {
|
|
4838
|
+
const g = booleanGroupSvg(self, emitChildren(), own2, paint);
|
|
4839
|
+
return mark ? mark(id, g) : g;
|
|
4840
|
+
}
|
|
4385
4841
|
const copied = () => copies.map((transform) => ({ kind: "group", transform, children: [content()] }));
|
|
4386
4842
|
const withCopies = (g) => copies.length === 0 ? g : { kind: "group", children: [g, ...copied()] };
|
|
4387
4843
|
if (isGroup) {
|
|
@@ -4398,7 +4854,7 @@ function svgNodesFromKit(tree, options = {}) {
|
|
|
4398
4854
|
};
|
|
4399
4855
|
const out = [];
|
|
4400
4856
|
for (const id of options.roots ?? tree.roots) {
|
|
4401
|
-
const n = emit(id, NO_UP_PAINT);
|
|
4857
|
+
const n = emit(id, NO_UP_PAINT, frameAbove(id));
|
|
4402
4858
|
if (n) out.push(n);
|
|
4403
4859
|
}
|
|
4404
4860
|
return out;
|