@weasel-js/svg 1.10.0 → 1.10.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/README.md +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +392 -259
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { identity, multiply, invert,
|
|
2
|
-
import { isTileKind, boundsOfPath, transformPath, isSolidPaint,
|
|
1
|
+
import { identity, multiply, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
|
|
2
|
+
import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, isTilePaint, tileSpecOf } from '@weasel-js/core';
|
|
3
3
|
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
4
4
|
|
|
5
5
|
// src/types.ts
|
|
@@ -1548,9 +1548,9 @@ function warnUnmodeled(el, elements, onWarn) {
|
|
|
1548
1548
|
break;
|
|
1549
1549
|
}
|
|
1550
1550
|
}
|
|
1551
|
-
const
|
|
1552
|
-
if (
|
|
1553
|
-
onWarn?.(`spreadMethod="${
|
|
1551
|
+
const spread2 = inheritedAttr(el, elements, "spreadMethod");
|
|
1552
|
+
if (spread2 === "reflect" || spread2 === "repeat") {
|
|
1553
|
+
onWarn?.(`spreadMethod="${spread2}" is not modeled; the gradient pads`);
|
|
1554
1554
|
}
|
|
1555
1555
|
}
|
|
1556
1556
|
function coord(el, elements, name, axis, fraction, units, onWarn) {
|
|
@@ -2238,6 +2238,31 @@ function anchorOffset(style, width2, text) {
|
|
|
2238
2238
|
}
|
|
2239
2239
|
|
|
2240
2240
|
// src/tspanLines.ts
|
|
2241
|
+
var LINES_ATTR = "data-weasel-lines";
|
|
2242
|
+
var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
|
|
2243
|
+
function linesXml(pieces, x, pitch) {
|
|
2244
|
+
let out = "";
|
|
2245
|
+
let pending = 0;
|
|
2246
|
+
let wrote = false;
|
|
2247
|
+
const tspan = (attrs, text) => `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
|
|
2248
|
+
const lineAttrs = () => {
|
|
2249
|
+
if (pending === 0) return [];
|
|
2250
|
+
const attrs = [`x="${trimNumber(x)}"`, `dy="${trimNumber(pending * pitch)}"`];
|
|
2251
|
+
if (pending > 1 || !wrote) attrs.push(`${LINES_ATTR}="${pending}"`);
|
|
2252
|
+
pending = 0;
|
|
2253
|
+
return attrs;
|
|
2254
|
+
};
|
|
2255
|
+
for (const piece of pieces) {
|
|
2256
|
+
piece.text.split(/[\n\u000B\u000C]/).forEach((line, i) => {
|
|
2257
|
+
if (i > 0) pending++;
|
|
2258
|
+
if (line === "") return;
|
|
2259
|
+
const attrs = [...piece.attrs ?? [], ...lineAttrs()];
|
|
2260
|
+
out += piece.attrs === null && attrs.length === 0 ? escapeText(line) : tspan(attrs, line);
|
|
2261
|
+
wrote = true;
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
return pending > 0 ? out + tspan(lineAttrs(), "") : out;
|
|
2265
|
+
}
|
|
2241
2266
|
var DEFAULT_LINE_HEIGHT = 1.2;
|
|
2242
2267
|
var LineTracker = class {
|
|
2243
2268
|
constructor(text, onWarn) {
|
|
@@ -2250,9 +2275,9 @@ var LineTracker = class {
|
|
|
2250
2275
|
/** Where the current line sits, in user units. */
|
|
2251
2276
|
at;
|
|
2252
2277
|
pitches = [];
|
|
2253
|
-
/**
|
|
2254
|
-
*
|
|
2255
|
-
* placement is not a pitch. */
|
|
2278
|
+
/** How many lines down `tspan` opens a new line, or 0 when it stays on the
|
|
2279
|
+
* current one. Call once per tspan, in order. `afterText` is whether any
|
|
2280
|
+
* text precedes it: the first line's own placement is not a pitch. */
|
|
2256
2281
|
startsLine(tspan, afterText) {
|
|
2257
2282
|
const y = tspan.getAttribute("y");
|
|
2258
2283
|
const movedY = y != null && y !== this.y;
|
|
@@ -2260,10 +2285,11 @@ var LineTracker = class {
|
|
|
2260
2285
|
const from = this.at;
|
|
2261
2286
|
const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
|
|
2262
2287
|
this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
|
|
2263
|
-
if (!tspan.hasAttribute("x")) return
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
|
|
2288
|
+
if (!tspan.hasAttribute("x") || !(movedY || dy !== 0)) return 0;
|
|
2289
|
+
const stated = Number(tspan.getAttribute(LINES_ATTR));
|
|
2290
|
+
const lines = Number.isInteger(stated) && stated > 0 ? stated : 1;
|
|
2291
|
+
if (afterText && this.at > from) this.pitches.push((this.at - from) / lines);
|
|
2292
|
+
return lines;
|
|
2267
2293
|
}
|
|
2268
2294
|
/**
|
|
2269
2295
|
* The line height the lines were pitched at, as a multiple of `fontSize`:
|
|
@@ -2278,7 +2304,7 @@ var LineTracker = class {
|
|
|
2278
2304
|
if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
|
|
2279
2305
|
this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
|
|
2280
2306
|
}
|
|
2281
|
-
const lineHeight = mean / fontSize;
|
|
2307
|
+
const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
|
|
2282
2308
|
return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
|
|
2283
2309
|
}
|
|
2284
2310
|
};
|
|
@@ -2288,35 +2314,88 @@ function warnUnreadPositions(el, onWarn) {
|
|
|
2288
2314
|
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2289
2315
|
if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
|
|
2290
2316
|
}
|
|
2291
|
-
for (const name of ["dx", "rotate"]) {
|
|
2317
|
+
for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
|
|
2292
2318
|
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2293
2319
|
if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
|
|
2294
2320
|
onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
|
|
2295
2321
|
}
|
|
2296
2322
|
}
|
|
2297
2323
|
}
|
|
2298
|
-
function
|
|
2324
|
+
function sameStyle(a, b) {
|
|
2325
|
+
const style = (run) => JSON.stringify(
|
|
2326
|
+
Object.entries(run).filter(([k, v]) => k !== "text" && v !== void 0).sort(([j], [k]) => j < k ? -1 : 1)
|
|
2327
|
+
);
|
|
2328
|
+
return style(a) === style(b);
|
|
2329
|
+
}
|
|
2330
|
+
function joinLines(lines, leading = 0) {
|
|
2299
2331
|
const out = [];
|
|
2300
|
-
let breaks =
|
|
2332
|
+
let breaks = leading;
|
|
2301
2333
|
for (const line of lines) {
|
|
2302
2334
|
if (line.length > 0) {
|
|
2303
|
-
|
|
2304
|
-
|
|
2335
|
+
const [first, ...rest] = line;
|
|
2336
|
+
const head = breaks > 0 ? { ...first, text: "\n".repeat(breaks) + first.text } : first;
|
|
2337
|
+
const last = out[out.length - 1];
|
|
2338
|
+
if (breaks > 0 && last && sameStyle(last, head)) {
|
|
2339
|
+
out[out.length - 1] = { ...last, text: last.text + head.text };
|
|
2340
|
+
} else {
|
|
2341
|
+
out.push(head);
|
|
2342
|
+
}
|
|
2343
|
+
out.push(...rest);
|
|
2305
2344
|
breaks = 0;
|
|
2306
2345
|
}
|
|
2307
2346
|
if (out.length > 0) breaks++;
|
|
2308
2347
|
}
|
|
2309
2348
|
return out;
|
|
2310
2349
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (!
|
|
2315
|
-
const
|
|
2316
|
-
return
|
|
2350
|
+
var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
|
|
2351
|
+
function readDx(el, onWarn) {
|
|
2352
|
+
const raw = el.getAttribute("dx")?.trim();
|
|
2353
|
+
if (!raw) return null;
|
|
2354
|
+
const values = raw.split(/[\s,]+/).map((v) => resolveLength(v, el, "dx", "x", 0, onWarn));
|
|
2355
|
+
return values.some((v) => v !== 0) ? values : null;
|
|
2356
|
+
}
|
|
2357
|
+
var spacingOf = (run, inherited2) => resolveScreenLength(run.letterSpacing ?? inherited2 ?? 0, 1);
|
|
2358
|
+
function spread(run, extra, inherited2) {
|
|
2359
|
+
const base = spacingOf(run, inherited2);
|
|
2360
|
+
const out = [];
|
|
2361
|
+
let last;
|
|
2362
|
+
Array.from(run.text).forEach((glyph, i) => {
|
|
2363
|
+
const more = extra[i] ?? 0;
|
|
2364
|
+
if (out.length > 0 && more === last) out[out.length - 1].text += glyph;
|
|
2365
|
+
else out.push(more === 0 ? { ...run, text: glyph } : { ...run, text: glyph, letterSpacing: base + more });
|
|
2366
|
+
last = more;
|
|
2367
|
+
});
|
|
2368
|
+
return out;
|
|
2369
|
+
}
|
|
2370
|
+
function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
|
|
2371
|
+
const line = lines[lines.length - 1];
|
|
2372
|
+
const dx = readDx(tspan, onWarn);
|
|
2373
|
+
if (!dx) {
|
|
2374
|
+
line.push(run);
|
|
2375
|
+
return 0;
|
|
2376
|
+
}
|
|
2377
|
+
const raw = tspan.getAttribute("dx");
|
|
2378
|
+
if (COLLAPSIBLE.test(run.text)) {
|
|
2379
|
+
onWarn(`<tspan dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2380
|
+
line.push(run);
|
|
2381
|
+
return 0;
|
|
2382
|
+
}
|
|
2383
|
+
let lead = 0;
|
|
2384
|
+
let before = line.length - 1;
|
|
2385
|
+
while (before >= 0 && line[before].text === "") before--;
|
|
2386
|
+
if (dx[0] !== 0 && before >= 0) {
|
|
2387
|
+
const glyphs = Array.from(line[before].text);
|
|
2388
|
+
line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]], inherited2));
|
|
2389
|
+
} else if (dx[0] !== 0 && lines.length === 1) {
|
|
2390
|
+
lead = dx[0];
|
|
2391
|
+
} else if (dx[0] !== 0) {
|
|
2392
|
+
onWarn(`<tspan dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
|
|
2393
|
+
}
|
|
2394
|
+
line.push(...spread(run, dx.slice(1), inherited2));
|
|
2395
|
+
return lead;
|
|
2317
2396
|
}
|
|
2318
2397
|
|
|
2319
|
-
// src/
|
|
2398
|
+
// src/textWhitespace.ts
|
|
2320
2399
|
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
2321
2400
|
function preservesSpace(el) {
|
|
2322
2401
|
for (let e = el; e; e = e.parentElement) {
|
|
@@ -2353,6 +2432,15 @@ function collapseRunWhitespace(runs) {
|
|
|
2353
2432
|
if (runs[i].text === "") runs.splice(i, 1);
|
|
2354
2433
|
}
|
|
2355
2434
|
}
|
|
2435
|
+
|
|
2436
|
+
// src/fillPaint.ts
|
|
2437
|
+
function mapFillPaint(fill, f) {
|
|
2438
|
+
if (!fill.paint) return fill;
|
|
2439
|
+
const paint = f(fill.paint);
|
|
2440
|
+
return paint === fill.paint ? fill : { ...fill, paint };
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/readText.ts
|
|
2356
2444
|
var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
|
|
2357
2445
|
function laidOutWidth(runs, style, plain) {
|
|
2358
2446
|
const resolved = resolveTextStyle(style);
|
|
@@ -2411,33 +2499,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2411
2499
|
const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
2412
2500
|
const topY = explicitTopAnchor ? ay : ay - fontSize;
|
|
2413
2501
|
const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
|
|
2414
|
-
const runLines = [[]];
|
|
2415
2502
|
const tracker = new LineTracker(el, onWarn);
|
|
2416
|
-
const
|
|
2417
|
-
runLines[runLines.length - 1].push(run);
|
|
2418
|
-
};
|
|
2419
|
-
for (let i = 0; i < el.childNodes.length; i++) {
|
|
2420
|
-
const child = el.childNodes[i];
|
|
2421
|
-
if (child.nodeType === 3) {
|
|
2422
|
-
const t = child.textContent ?? "";
|
|
2423
|
-
if (!t) continue;
|
|
2424
|
-
push({ text: t, ...inheritedShift });
|
|
2425
|
-
} else if (child.nodeType === 1) {
|
|
2426
|
-
const sp = child;
|
|
2427
|
-
if (sp.tagName.toLowerCase() !== "tspan") {
|
|
2428
|
-
onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
|
|
2429
|
-
const t = sp.textContent ?? "";
|
|
2430
|
-
if (t) push({ text: t, ...inheritedShift });
|
|
2431
|
-
continue;
|
|
2432
|
-
}
|
|
2433
|
-
warnUnreadPositions(sp, onWarn);
|
|
2434
|
-
const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
|
|
2435
|
-
if (tracker.startsLine(sp, afterText)) runLines.push([]);
|
|
2436
|
-
push(readTspanRun(sp, gradients, leafStyle, onWarn));
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
|
|
2440
|
-
const runs = joinLines(runLines);
|
|
2503
|
+
const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
|
|
2441
2504
|
const plain = runs.map((r) => r.text).join("");
|
|
2442
2505
|
const pitched = tracker.lineHeight(fontSize);
|
|
2443
2506
|
if (pitched !== void 0) textStyle.lineHeight = pitched;
|
|
@@ -2450,7 +2513,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2450
2513
|
const opacity = ownValue(el, "opacity");
|
|
2451
2514
|
const node = {
|
|
2452
2515
|
kind: "text",
|
|
2453
|
-
x: ax - anchorOffset(textStyle, width2, plain),
|
|
2516
|
+
x: ax + lead - anchorOffset(textStyle, width2, plain),
|
|
2454
2517
|
y: topY,
|
|
2455
2518
|
width: width2,
|
|
2456
2519
|
height: height2,
|
|
@@ -2476,6 +2539,38 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2476
2539
|
placeTextNode(node, multiply(ctm, localTransform), onWarn);
|
|
2477
2540
|
return node;
|
|
2478
2541
|
}
|
|
2542
|
+
function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafStyle, onWarn) {
|
|
2543
|
+
const runLines = [[]];
|
|
2544
|
+
let leading = 0;
|
|
2545
|
+
let lead = 0;
|
|
2546
|
+
const push = (run) => {
|
|
2547
|
+
runLines[runLines.length - 1].push(run);
|
|
2548
|
+
};
|
|
2549
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
2550
|
+
const child = el.childNodes[i];
|
|
2551
|
+
if (child.nodeType === 3) {
|
|
2552
|
+
const t = child.textContent ?? "";
|
|
2553
|
+
if (!t) continue;
|
|
2554
|
+
push({ text: t, ...inheritedShift });
|
|
2555
|
+
} else if (child.nodeType === 1) {
|
|
2556
|
+
const sp = child;
|
|
2557
|
+
if (sp.tagName.toLowerCase() !== "tspan") {
|
|
2558
|
+
onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
|
|
2559
|
+
const t = sp.textContent ?? "";
|
|
2560
|
+
if (t) push({ text: t, ...inheritedShift });
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
warnUnreadPositions(sp, onWarn);
|
|
2564
|
+
const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
|
|
2565
|
+
const lines = tracker.startsLine(sp, afterText);
|
|
2566
|
+
if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
|
|
2567
|
+
else for (let n = 0; n < lines; n++) runLines.push([]);
|
|
2568
|
+
lead += placeTspanRun(sp, readTspanRun(sp, gradients, leafStyle, onWarn), runLines, letterSpacing, onWarn);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
|
|
2572
|
+
return { runs: joinLines(runLines, leading), lead };
|
|
2573
|
+
}
|
|
2479
2574
|
function readTspanRun(el, gradients, style, onWarn) {
|
|
2480
2575
|
const text = el.textContent ?? "";
|
|
2481
2576
|
const run = { text };
|
|
@@ -3132,25 +3227,45 @@ function pathBounds(path) {
|
|
|
3132
3227
|
|
|
3133
3228
|
// src/openSubpathFill.ts
|
|
3134
3229
|
var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
|
|
3230
|
+
var marked = (stroke) => capsEnds(stroke) || stroke?.markerMid !== void 0;
|
|
3135
3231
|
function withoutEndMarkers(stroke) {
|
|
3136
3232
|
const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
|
|
3137
3233
|
return rest;
|
|
3138
3234
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3235
|
+
var closedStroke = (stroke) => stroke && capsEnds(stroke) ? withoutEndMarkers(stroke) : stroke;
|
|
3236
|
+
var isClosed = (part) => splitByClosure(part).open === null;
|
|
3237
|
+
function samePath(a, b) {
|
|
3238
|
+
if (a === b) return true;
|
|
3239
|
+
return a.commands.length === b.commands.length && a.coords.length === b.coords.length && a.commands.every((c, i) => c === b.commands[i]) && a.coords.every((v, i) => v === b.coords[i]);
|
|
3240
|
+
}
|
|
3241
|
+
function strokeParts(path, stroke, filled, closed, open) {
|
|
3242
|
+
const none = { kind: "none" };
|
|
3243
|
+
const subs = marked(stroke) ? splitSubpaths(path) : [];
|
|
3244
|
+
if (subs.length > 1) {
|
|
3245
|
+
return subs.map((sub) => ({ path: sub, fill: none, stroke: isClosed(sub) ? closedStroke(stroke) : stroke }));
|
|
3246
|
+
}
|
|
3247
|
+
if (closed && open && filled === closed) {
|
|
3248
|
+
return [{ path: closed, fill: none, stroke: closedStroke(stroke) }, { path: open, fill: none, stroke }];
|
|
3249
|
+
}
|
|
3250
|
+
return [{ path, fill: none, stroke: open ? stroke : closedStroke(stroke) }];
|
|
3251
|
+
}
|
|
3252
|
+
function pathAsSvgDraws(node) {
|
|
3253
|
+
const { path, fill, stroke } = node;
|
|
3254
|
+
if (path.kind !== "polygon") return capsEnds(stroke) ? { ...node, stroke: closedStroke(stroke) } : node;
|
|
3255
|
+
if (path.commands.length === 0) return node;
|
|
3142
3256
|
const { closed, open } = splitByClosure(path);
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3145
|
-
const
|
|
3146
|
-
if (
|
|
3147
|
-
|
|
3257
|
+
const filled = fill.kind === "none" ? null : fillsOpenSubpaths(path) ? path : closed;
|
|
3258
|
+
const parts = strokeParts(path, stroke, filled, closed, open);
|
|
3259
|
+
const pieces = filled ? withFill(parts, filled, fill) : parts;
|
|
3260
|
+
if (pieces.length === 1) {
|
|
3261
|
+
const only = pieces[0];
|
|
3262
|
+
if (only.fill === fill && only.stroke === stroke) return node;
|
|
3263
|
+
const { stroke: _stroke, ...rest } = node;
|
|
3264
|
+
return { ...rest, fill: only.fill, ...only.stroke ? { stroke: only.stroke } : {} };
|
|
3265
|
+
}
|
|
3148
3266
|
const group = {
|
|
3149
3267
|
kind: "group",
|
|
3150
|
-
children:
|
|
3151
|
-
...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
|
|
3152
|
-
...opens.map((part) => piece(part, { kind: "none" }))
|
|
3153
|
-
]
|
|
3268
|
+
children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
|
|
3154
3269
|
};
|
|
3155
3270
|
if (node.rotation != null && node.rotation !== 0) {
|
|
3156
3271
|
const b = pathBounds(path);
|
|
@@ -3161,6 +3276,200 @@ function withOpenSubpathsUnfilled(node) {
|
|
|
3161
3276
|
if (node.meta) group.meta = node.meta;
|
|
3162
3277
|
return group;
|
|
3163
3278
|
}
|
|
3279
|
+
function withFill(parts, filled, fill) {
|
|
3280
|
+
const host = parts.findIndex((p) => samePath(p.path, filled));
|
|
3281
|
+
if (host < 0) return [{ path: filled, fill, stroke: void 0 }, ...parts];
|
|
3282
|
+
return [{ ...parts[host], fill }, ...parts.filter((_, i) => i !== host)];
|
|
3283
|
+
}
|
|
3284
|
+
function warnStrokeAlign(align, warn) {
|
|
3285
|
+
if (align && align !== "center") {
|
|
3286
|
+
warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
3290
|
+
if (paint.kind === "none") return [`${name}="none"`];
|
|
3291
|
+
const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
|
|
3292
|
+
if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
|
|
3293
|
+
return out;
|
|
3294
|
+
}
|
|
3295
|
+
function svgPaintOpacity(paint) {
|
|
3296
|
+
if (paint.kind === "solid") return paint.opacity;
|
|
3297
|
+
return paint.kind === "gradient" ? paint.paint.opacity : void 0;
|
|
3298
|
+
}
|
|
3299
|
+
function opacityAttr(name, v) {
|
|
3300
|
+
return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
|
|
3301
|
+
}
|
|
3302
|
+
function strokeWidthAttrs(width2) {
|
|
3303
|
+
if (typeof width2 === "number") return [`stroke-width="${trimNumber(width2)}"`];
|
|
3304
|
+
return [`stroke-width="${trimNumber(width2.px)}"`, 'vector-effect="non-scaling-stroke"'];
|
|
3305
|
+
}
|
|
3306
|
+
function coreStrokeAttrs(stroke, registry, warn) {
|
|
3307
|
+
const paint = stroke?.paint;
|
|
3308
|
+
if (!stroke || !paint) return [];
|
|
3309
|
+
const width2 = stroke.width ?? 1;
|
|
3310
|
+
if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
|
|
3311
|
+
warnStrokeAlign(stroke.align, warn);
|
|
3312
|
+
const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
|
|
3313
|
+
attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
|
|
3314
|
+
attrs.push(...strokeWidthAttrs(width2));
|
|
3315
|
+
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
3316
|
+
if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
3317
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
3318
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
3319
|
+
}
|
|
3320
|
+
if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
3321
|
+
for (const [field, attr] of [
|
|
3322
|
+
["markerStart", "marker-start"],
|
|
3323
|
+
["markerMid", "marker-mid"],
|
|
3324
|
+
["markerEnd", "marker-end"]
|
|
3325
|
+
]) {
|
|
3326
|
+
const ref = stroke[field];
|
|
3327
|
+
if (ref === void 0) continue;
|
|
3328
|
+
const id = registry.markerId(ref);
|
|
3329
|
+
if (id === void 0) continue;
|
|
3330
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
3331
|
+
}
|
|
3332
|
+
return attrs;
|
|
3333
|
+
}
|
|
3334
|
+
function strokeAttrsFor(stroke, registry, warn) {
|
|
3335
|
+
warnStrokeAlign(stroke.align, warn);
|
|
3336
|
+
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
3337
|
+
attrs.push(...strokeWidthAttrs(stroke.width));
|
|
3338
|
+
attrs.push(...opacityAttr("stroke-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint)));
|
|
3339
|
+
if (stroke.cap) {
|
|
3340
|
+
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
3341
|
+
}
|
|
3342
|
+
if (stroke.join) {
|
|
3343
|
+
attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
3344
|
+
}
|
|
3345
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
3346
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
3347
|
+
}
|
|
3348
|
+
if (stroke.miterLimit != null) {
|
|
3349
|
+
attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
3350
|
+
}
|
|
3351
|
+
for (const [field, attr] of [
|
|
3352
|
+
["markerStart", "marker-start"],
|
|
3353
|
+
["markerMid", "marker-mid"],
|
|
3354
|
+
["markerEnd", "marker-end"]
|
|
3355
|
+
]) {
|
|
3356
|
+
const ref = stroke[field];
|
|
3357
|
+
if (ref === void 0) continue;
|
|
3358
|
+
const id = registry.markerId(ref);
|
|
3359
|
+
if (id === void 0) continue;
|
|
3360
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
3361
|
+
}
|
|
3362
|
+
return attrs;
|
|
3363
|
+
}
|
|
3364
|
+
function textXml(node, registry, namespaces, warn) {
|
|
3365
|
+
const x = node.x + anchorOffset(node.style, node.width, node.text);
|
|
3366
|
+
const attrs = [
|
|
3367
|
+
`x="${trimNumber(x)}"`,
|
|
3368
|
+
`y="${trimNumber(node.y)}"`,
|
|
3369
|
+
`dominant-baseline="text-before-edge"`,
|
|
3370
|
+
// SVG's default whitespace handling would collapse indentation and runs
|
|
3371
|
+
// of spaces away on re-import.
|
|
3372
|
+
`xml:space="preserve"`,
|
|
3373
|
+
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
3374
|
+
`data-weasel-height="${trimNumber(node.height)}"`
|
|
3375
|
+
];
|
|
3376
|
+
const style = node.style;
|
|
3377
|
+
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
|
|
3378
|
+
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
|
|
3379
|
+
if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
|
|
3380
|
+
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
3381
|
+
if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
|
|
3382
|
+
const direction = svgDirection(style, node.text);
|
|
3383
|
+
if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
|
|
3384
|
+
if (style?.align != null || direction === "rtl") {
|
|
3385
|
+
const align = style?.align ?? "left";
|
|
3386
|
+
const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
3387
|
+
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
3388
|
+
if (align === "justify") attrs.push('data-weasel-align="justify"');
|
|
3389
|
+
}
|
|
3390
|
+
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
3391
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
|
|
3392
|
+
}
|
|
3393
|
+
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
3394
|
+
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
3395
|
+
if (style?.script) attrs.push(`baseline-shift="${style.script}"`);
|
|
3396
|
+
if (style?.textTransform && style.textTransform !== "none") {
|
|
3397
|
+
attrs.push(`style="text-transform:${style.textTransform}"`);
|
|
3398
|
+
}
|
|
3399
|
+
if (style?.wrap) {
|
|
3400
|
+
attrs.push('data-weasel-wrap="true"');
|
|
3401
|
+
warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
|
|
3402
|
+
}
|
|
3403
|
+
if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
|
|
3404
|
+
if (node.verticalAlign && node.verticalAlign !== "top") {
|
|
3405
|
+
attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
|
|
3406
|
+
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)`);
|
|
3407
|
+
}
|
|
3408
|
+
if (node.fill !== void 0) {
|
|
3409
|
+
const paint = node.fill?.paint;
|
|
3410
|
+
if (!paint) attrs.push('fill="none"');
|
|
3411
|
+
else {
|
|
3412
|
+
attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
|
|
3413
|
+
attrs.push(...opacityAttr("fill-opacity", paint.opacity));
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
|
|
3417
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
3418
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3419
|
+
}
|
|
3420
|
+
const frame = boxTransformAttr(node);
|
|
3421
|
+
if (frame) attrs.push(frame);
|
|
3422
|
+
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 }];
|
|
3423
|
+
const resolved = resolveTextStyle(style);
|
|
3424
|
+
const body = linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
|
|
3425
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3426
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3427
|
+
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
3428
|
+
}
|
|
3429
|
+
function scriptSizeFace(run, style) {
|
|
3430
|
+
if (run.script == null || run.baselineShift == null) return void 0;
|
|
3431
|
+
const face = resolveRunFace(run, resolveTextStyle(style));
|
|
3432
|
+
return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
|
|
3433
|
+
}
|
|
3434
|
+
function runAttrs(run, style, registry, warn) {
|
|
3435
|
+
const attrs = [];
|
|
3436
|
+
if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
|
|
3437
|
+
else if (run.bold) attrs.push(`font-weight="700"`);
|
|
3438
|
+
if (run.italic) attrs.push(`font-style="italic"`);
|
|
3439
|
+
if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
|
|
3440
|
+
if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
|
|
3441
|
+
const face = scriptSizeFace(run, style);
|
|
3442
|
+
const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
|
|
3443
|
+
const scale = run.fontScale ?? presetScale;
|
|
3444
|
+
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
|
|
3445
|
+
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
3446
|
+
if (run.baselineShift != null) {
|
|
3447
|
+
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
3448
|
+
} else if (run.script) {
|
|
3449
|
+
attrs.push(`baseline-shift="${run.script}"`);
|
|
3450
|
+
}
|
|
3451
|
+
if (run.letterSpacing != null) {
|
|
3452
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
|
|
3453
|
+
}
|
|
3454
|
+
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
3455
|
+
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
3456
|
+
if (run.fill !== void 0) {
|
|
3457
|
+
const paint = run.fill?.paint;
|
|
3458
|
+
if (!paint) attrs.push('fill="none"');
|
|
3459
|
+
else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
|
|
3460
|
+
else attrs.push(`fill="${registry.ref(paint)}"`);
|
|
3461
|
+
}
|
|
3462
|
+
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
3463
|
+
if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
|
|
3464
|
+
return attrs;
|
|
3465
|
+
}
|
|
3466
|
+
function textDecorationValue(underline, strikethrough, overline) {
|
|
3467
|
+
const tokens = [];
|
|
3468
|
+
if (underline) tokens.push("underline");
|
|
3469
|
+
if (strikethrough) tokens.push("line-through");
|
|
3470
|
+
if (overline) tokens.push("overline");
|
|
3471
|
+
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
3472
|
+
}
|
|
3164
3473
|
|
|
3165
3474
|
// src/prettyXml.ts
|
|
3166
3475
|
var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
|
|
@@ -3277,14 +3586,9 @@ function kindXml(node, registry, namespaces, warn) {
|
|
|
3277
3586
|
if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
|
|
3278
3587
|
if (node.kind === "text") return textXml(node, registry, namespaces, warn);
|
|
3279
3588
|
if (node.kind === "image") return imageXml(node, namespaces);
|
|
3280
|
-
const drawn =
|
|
3589
|
+
const drawn = pathAsSvgDraws(node);
|
|
3281
3590
|
return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
|
|
3282
3591
|
}
|
|
3283
|
-
function warnStrokeAlign(align, warn) {
|
|
3284
|
-
if (align && align !== "center") {
|
|
3285
|
-
warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
3592
|
function imageXml(node, namespaces) {
|
|
3289
3593
|
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
3290
3594
|
const outer = [];
|
|
@@ -3354,188 +3658,6 @@ function pathXml(node, registry, namespaces, warn) {
|
|
|
3354
3658
|
}
|
|
3355
3659
|
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
3356
3660
|
}
|
|
3357
|
-
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
3358
|
-
if (paint.kind === "none") return [`${name}="none"`];
|
|
3359
|
-
const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
|
|
3360
|
-
if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
|
|
3361
|
-
return out;
|
|
3362
|
-
}
|
|
3363
|
-
function svgPaintOpacity(paint) {
|
|
3364
|
-
if (paint.kind === "solid") return paint.opacity;
|
|
3365
|
-
return paint.kind === "gradient" ? paint.paint.opacity : void 0;
|
|
3366
|
-
}
|
|
3367
|
-
function opacityAttr(name, v) {
|
|
3368
|
-
return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
|
|
3369
|
-
}
|
|
3370
|
-
function strokeWidthAttrs(width2) {
|
|
3371
|
-
if (typeof width2 === "number") return [`stroke-width="${trimNumber(width2)}"`];
|
|
3372
|
-
return [`stroke-width="${trimNumber(width2.px)}"`, 'vector-effect="non-scaling-stroke"'];
|
|
3373
|
-
}
|
|
3374
|
-
function coreStrokeAttrs(stroke, registry, warn) {
|
|
3375
|
-
const paint = stroke?.paint;
|
|
3376
|
-
if (!stroke || !paint) return [];
|
|
3377
|
-
const width2 = stroke.width ?? 1;
|
|
3378
|
-
if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
|
|
3379
|
-
warnStrokeAlign(stroke.align, warn);
|
|
3380
|
-
const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
|
|
3381
|
-
attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
|
|
3382
|
-
attrs.push(...strokeWidthAttrs(width2));
|
|
3383
|
-
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
3384
|
-
if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
3385
|
-
if (stroke.dash && stroke.dash.length > 0) {
|
|
3386
|
-
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
3387
|
-
}
|
|
3388
|
-
if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
3389
|
-
for (const [field, attr] of [
|
|
3390
|
-
["markerStart", "marker-start"],
|
|
3391
|
-
["markerMid", "marker-mid"],
|
|
3392
|
-
["markerEnd", "marker-end"]
|
|
3393
|
-
]) {
|
|
3394
|
-
const ref = stroke[field];
|
|
3395
|
-
if (ref === void 0) continue;
|
|
3396
|
-
const id = registry.markerId(ref);
|
|
3397
|
-
if (id === void 0) continue;
|
|
3398
|
-
attrs.push(`${attr}="url(#${id})"`);
|
|
3399
|
-
}
|
|
3400
|
-
return attrs;
|
|
3401
|
-
}
|
|
3402
|
-
function strokeAttrsFor(stroke, registry, warn) {
|
|
3403
|
-
warnStrokeAlign(stroke.align, warn);
|
|
3404
|
-
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
3405
|
-
attrs.push(...strokeWidthAttrs(stroke.width));
|
|
3406
|
-
attrs.push(...opacityAttr("stroke-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint)));
|
|
3407
|
-
if (stroke.cap) {
|
|
3408
|
-
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
3409
|
-
}
|
|
3410
|
-
if (stroke.join) {
|
|
3411
|
-
attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
3412
|
-
}
|
|
3413
|
-
if (stroke.dash && stroke.dash.length > 0) {
|
|
3414
|
-
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
3415
|
-
}
|
|
3416
|
-
if (stroke.miterLimit != null) {
|
|
3417
|
-
attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
3418
|
-
}
|
|
3419
|
-
for (const [field, attr] of [
|
|
3420
|
-
["markerStart", "marker-start"],
|
|
3421
|
-
["markerMid", "marker-mid"],
|
|
3422
|
-
["markerEnd", "marker-end"]
|
|
3423
|
-
]) {
|
|
3424
|
-
const ref = stroke[field];
|
|
3425
|
-
if (ref === void 0) continue;
|
|
3426
|
-
const id = registry.markerId(ref);
|
|
3427
|
-
if (id === void 0) continue;
|
|
3428
|
-
attrs.push(`${attr}="url(#${id})"`);
|
|
3429
|
-
}
|
|
3430
|
-
return attrs;
|
|
3431
|
-
}
|
|
3432
|
-
function textXml(node, registry, namespaces, warn) {
|
|
3433
|
-
const attrs = [
|
|
3434
|
-
`x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
|
|
3435
|
-
`y="${trimNumber(node.y)}"`,
|
|
3436
|
-
`dominant-baseline="text-before-edge"`,
|
|
3437
|
-
// The runs model stores real line breaks; SVG's default whitespace
|
|
3438
|
-
// handling would collapse them (and any indentation) away on re-import.
|
|
3439
|
-
`xml:space="preserve"`,
|
|
3440
|
-
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
3441
|
-
`data-weasel-height="${trimNumber(node.height)}"`
|
|
3442
|
-
];
|
|
3443
|
-
const style = node.style;
|
|
3444
|
-
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
|
|
3445
|
-
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
|
|
3446
|
-
if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
|
|
3447
|
-
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
3448
|
-
if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
|
|
3449
|
-
const direction = svgDirection(style, node.text);
|
|
3450
|
-
if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
|
|
3451
|
-
if (style?.align != null || direction === "rtl") {
|
|
3452
|
-
const align = style?.align ?? "left";
|
|
3453
|
-
const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
3454
|
-
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
3455
|
-
if (align === "justify") attrs.push('data-weasel-align="justify"');
|
|
3456
|
-
}
|
|
3457
|
-
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
3458
|
-
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
|
|
3459
|
-
}
|
|
3460
|
-
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
3461
|
-
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
3462
|
-
if (style?.script) attrs.push(`baseline-shift="${style.script}"`);
|
|
3463
|
-
if (style?.textTransform && style.textTransform !== "none") {
|
|
3464
|
-
attrs.push(`style="text-transform:${style.textTransform}"`);
|
|
3465
|
-
}
|
|
3466
|
-
if (style?.wrap) {
|
|
3467
|
-
attrs.push('data-weasel-wrap="true"');
|
|
3468
|
-
warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
|
|
3469
|
-
}
|
|
3470
|
-
if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
|
|
3471
|
-
if (node.verticalAlign && node.verticalAlign !== "top") {
|
|
3472
|
-
attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
|
|
3473
|
-
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)`);
|
|
3474
|
-
}
|
|
3475
|
-
if (node.fill !== void 0) {
|
|
3476
|
-
const paint = node.fill?.paint;
|
|
3477
|
-
if (!paint) attrs.push('fill="none"');
|
|
3478
|
-
else {
|
|
3479
|
-
attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
|
|
3480
|
-
attrs.push(...opacityAttr("fill-opacity", paint.opacity));
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
|
|
3484
|
-
if (node.opacity != null && node.opacity !== 1) {
|
|
3485
|
-
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3486
|
-
}
|
|
3487
|
-
const frame = boxTransformAttr(node);
|
|
3488
|
-
if (frame) attrs.push(frame);
|
|
3489
|
-
const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
|
|
3490
|
-
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3491
|
-
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3492
|
-
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
3493
|
-
}
|
|
3494
|
-
function scriptSizeFace(run, style) {
|
|
3495
|
-
if (run.script == null || run.baselineShift == null) return void 0;
|
|
3496
|
-
const face = resolveRunFace(run, resolveTextStyle(style));
|
|
3497
|
-
return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
|
|
3498
|
-
}
|
|
3499
|
-
function runXml(run, style, registry, warn) {
|
|
3500
|
-
const attrs = [];
|
|
3501
|
-
if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
|
|
3502
|
-
else if (run.bold) attrs.push(`font-weight="700"`);
|
|
3503
|
-
if (run.italic) attrs.push(`font-style="italic"`);
|
|
3504
|
-
if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
|
|
3505
|
-
if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
|
|
3506
|
-
const face = scriptSizeFace(run, style);
|
|
3507
|
-
const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
|
|
3508
|
-
const scale = run.fontScale ?? presetScale;
|
|
3509
|
-
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
|
|
3510
|
-
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
3511
|
-
if (run.baselineShift != null) {
|
|
3512
|
-
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
3513
|
-
} else if (run.script) {
|
|
3514
|
-
attrs.push(`baseline-shift="${run.script}"`);
|
|
3515
|
-
}
|
|
3516
|
-
if (run.letterSpacing != null) {
|
|
3517
|
-
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
|
|
3518
|
-
}
|
|
3519
|
-
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
3520
|
-
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
3521
|
-
if (run.fill !== void 0) {
|
|
3522
|
-
const paint = run.fill?.paint;
|
|
3523
|
-
if (!paint) attrs.push('fill="none"');
|
|
3524
|
-
else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
|
|
3525
|
-
else attrs.push(`fill="${registry.ref(paint)}"`);
|
|
3526
|
-
}
|
|
3527
|
-
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
3528
|
-
if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
|
|
3529
|
-
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
3530
|
-
return `${head}${escapeText(run.text)}</tspan>`;
|
|
3531
|
-
}
|
|
3532
|
-
function textDecorationValue(underline, strikethrough, overline) {
|
|
3533
|
-
const tokens = [];
|
|
3534
|
-
if (underline) tokens.push("underline");
|
|
3535
|
-
if (strikethrough) tokens.push("line-through");
|
|
3536
|
-
if (overline) tokens.push("overline");
|
|
3537
|
-
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
3538
|
-
}
|
|
3539
3661
|
function svgNeeds(nodes) {
|
|
3540
3662
|
const registry = new PaintServerRegistry();
|
|
3541
3663
|
registerPaintServers(nodes, registry);
|
|
@@ -3892,14 +4014,25 @@ function svgNodesFromKit(tree, options = {}) {
|
|
|
3892
4014
|
const node = tree.get(id);
|
|
3893
4015
|
if (!node) return null;
|
|
3894
4016
|
if (node.kind === "container") {
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
const
|
|
3898
|
-
|
|
4017
|
+
const emitChildren = () => {
|
|
4018
|
+
const children = [];
|
|
4019
|
+
for (const c of tree.childrenOf(id)) {
|
|
4020
|
+
const child = emit(c);
|
|
4021
|
+
if (child) children.push(child);
|
|
4022
|
+
}
|
|
4023
|
+
return children;
|
|
4024
|
+
};
|
|
4025
|
+
const path = node.data?.path;
|
|
4026
|
+
const clip = path ? pathThroughPoseFrame(pathInPoseFrame(path, node.pose), node.pose) : void 0;
|
|
4027
|
+
const copies = node.copiesFromPose?.(node.pose, node) ?? [];
|
|
4028
|
+
const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
|
|
4029
|
+
let g = content();
|
|
4030
|
+
if (copies.length > 0) {
|
|
4031
|
+
g = {
|
|
4032
|
+
kind: "group",
|
|
4033
|
+
children: [g, ...copies.map((transform) => ({ kind: "group", transform, children: [content()] }))]
|
|
4034
|
+
};
|
|
3899
4035
|
}
|
|
3900
|
-
const g = { kind: "group", children };
|
|
3901
|
-
const clip = node.data?.path;
|
|
3902
|
-
if (clip) g.clip = pathThroughPoseFrame(pathInPoseFrame(clip, node.pose), node.pose);
|
|
3903
4036
|
return group ? group(id, g) : g;
|
|
3904
4037
|
}
|
|
3905
4038
|
return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);
|