@weasel-js/svg 1.10.0 → 1.11.0
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 +11 -1
- package/dist/index.d.ts +74 -16
- package/dist/index.js +651 -306
- 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 { resolveCorners, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, pathThroughPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, DEFAULT_TEXT_STYLE, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
|
|
3
3
|
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
4
4
|
|
|
5
5
|
// src/types.ts
|
|
@@ -177,9 +177,11 @@ function scaleLength(v, scale) {
|
|
|
177
177
|
return typeof v === "number" ? v * scale : v;
|
|
178
178
|
}
|
|
179
179
|
function scaleStroke(stroke, scale) {
|
|
180
|
-
const { width: width2 } = stroke;
|
|
181
|
-
if (scale === 1
|
|
182
|
-
const
|
|
180
|
+
const { width: width2, vertexWidths } = stroke;
|
|
181
|
+
if (scale === 1) return stroke;
|
|
182
|
+
const scaledVertices = vertexWidths ? { vertexWidths: vertexWidths.map((n) => scaleVertexWidth(n, scale)) } : {};
|
|
183
|
+
if (typeof width2 === "object") return vertexWidths ? { ...stroke, ...scaledVertices } : stroke;
|
|
184
|
+
const out = { ...stroke, ...scaledVertices };
|
|
183
185
|
if (width2 !== void 0) out.width = width2 * scale;
|
|
184
186
|
if (out.dash) out.dash = out.dash.map((n) => n * scale);
|
|
185
187
|
return out;
|
|
@@ -1381,7 +1383,8 @@ function readPattern(el, onWarn) {
|
|
|
1381
1383
|
if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
|
|
1382
1384
|
return paint;
|
|
1383
1385
|
}
|
|
1384
|
-
function serializePathD(
|
|
1386
|
+
function serializePathD(given) {
|
|
1387
|
+
const path = resolveCorners(given);
|
|
1385
1388
|
if (path.kind === "rect") {
|
|
1386
1389
|
const { x, y, width: width2, height: height2 } = path;
|
|
1387
1390
|
return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
|
|
@@ -1548,9 +1551,9 @@ function warnUnmodeled(el, elements, onWarn) {
|
|
|
1548
1551
|
break;
|
|
1549
1552
|
}
|
|
1550
1553
|
}
|
|
1551
|
-
const
|
|
1552
|
-
if (
|
|
1553
|
-
onWarn?.(`spreadMethod="${
|
|
1554
|
+
const spread2 = inheritedAttr(el, elements, "spreadMethod");
|
|
1555
|
+
if (spread2 === "reflect" || spread2 === "repeat") {
|
|
1556
|
+
onWarn?.(`spreadMethod="${spread2}" is not modeled; the gradient pads`);
|
|
1554
1557
|
}
|
|
1555
1558
|
}
|
|
1556
1559
|
function coord(el, elements, name, axis, fraction, units, onWarn) {
|
|
@@ -2096,6 +2099,97 @@ function coreStroke(stroke) {
|
|
|
2096
2099
|
if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
|
|
2097
2100
|
return out;
|
|
2098
2101
|
}
|
|
2102
|
+
var VERTEX_WIDTHS = "vertex-widths";
|
|
2103
|
+
var NIB = "nib";
|
|
2104
|
+
var WIDTH = "stroke-width";
|
|
2105
|
+
var ROLE = "role";
|
|
2106
|
+
var OUTLINE = "stroke-outline";
|
|
2107
|
+
var GROUP = "stroke-group";
|
|
2108
|
+
var MARKER_ATTR = /^(marker-(?:start|mid|end))=/;
|
|
2109
|
+
var UNSET = "-";
|
|
2110
|
+
var SIDES = "/";
|
|
2111
|
+
function attrOf(el, name) {
|
|
2112
|
+
return el.getAttributeNS(WEASEL_NS, name) ?? el.getAttribute(`${WEASEL_NS_PREFIX}:${name}`);
|
|
2113
|
+
}
|
|
2114
|
+
function hasVariableWidth(stroke) {
|
|
2115
|
+
return (stroke?.vertexWidths?.length ?? 0) > 0 || stroke?.nib !== void 0;
|
|
2116
|
+
}
|
|
2117
|
+
function writesVariableWidth(nodes) {
|
|
2118
|
+
return nodes.some((n) => n.kind === "group" ? writesVariableWidth(n.children) : n.kind === "path" && hasVariableWidth(n.stroke));
|
|
2119
|
+
}
|
|
2120
|
+
function widthToken(entry) {
|
|
2121
|
+
const { left, right } = vertexHalfWidths(entry, NaN);
|
|
2122
|
+
const side = (v) => Number.isNaN(v) ? UNSET : trimNumber(v);
|
|
2123
|
+
if (typeof entry === "number") return Number.isNaN(left) ? UNSET : trimNumber(left + right);
|
|
2124
|
+
return Number.isNaN(left) && Number.isNaN(right) ? UNSET : `${side(left)}${SIDES}${side(right)}`;
|
|
2125
|
+
}
|
|
2126
|
+
function widthOfToken(token) {
|
|
2127
|
+
const side = (v) => v === UNSET ? NaN : Number(v);
|
|
2128
|
+
if (!token.includes(SIDES)) return side(token);
|
|
2129
|
+
const [left, right] = token.split(SIDES);
|
|
2130
|
+
return { left: side(left), right: side(right) };
|
|
2131
|
+
}
|
|
2132
|
+
function variableWidthAttrs(stroke, strokeAttrs) {
|
|
2133
|
+
const width2 = typeof stroke.width === "number" ? stroke.width : stroke.width.px;
|
|
2134
|
+
const out = [
|
|
2135
|
+
...strokeAttrs.map((a) => a.startsWith("stroke-width=") ? 'stroke-width="0"' : MARKER_ATTR.test(a) ? `${WEASEL_NS_PREFIX}:${a}` : a),
|
|
2136
|
+
`${WEASEL_NS_PREFIX}:${WIDTH}="${trimNumber(width2)}"`
|
|
2137
|
+
];
|
|
2138
|
+
if (stroke.vertexWidths?.length) {
|
|
2139
|
+
const list = stroke.vertexWidths.map(widthToken).join(" ");
|
|
2140
|
+
out.push(`${WEASEL_NS_PREFIX}:${VERTEX_WIDTHS}="${list}"`);
|
|
2141
|
+
}
|
|
2142
|
+
if (stroke.nib) {
|
|
2143
|
+
out.push(`${WEASEL_NS_PREFIX}:${NIB}="${String(stroke.nib.angle)} ${String(stroke.nib.ratio)}"`);
|
|
2144
|
+
}
|
|
2145
|
+
return out;
|
|
2146
|
+
}
|
|
2147
|
+
function strokeOutlineXml(path, stroke, fillAttrs, transform) {
|
|
2148
|
+
const kit = {
|
|
2149
|
+
width: stroke.width,
|
|
2150
|
+
...stroke.vertexWidths ? { vertexWidths: stroke.vertexWidths } : {},
|
|
2151
|
+
...stroke.nib ? { nib: stroke.nib } : {},
|
|
2152
|
+
...stroke.cap ? { cap: stroke.cap } : {},
|
|
2153
|
+
...stroke.join ? { join: stroke.join } : {},
|
|
2154
|
+
...stroke.dash ? { dash: stroke.dash } : {},
|
|
2155
|
+
...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {},
|
|
2156
|
+
...stroke.align ? { align: stroke.align } : {},
|
|
2157
|
+
...stroke.markerStart ? { markerStart: stroke.markerStart } : {},
|
|
2158
|
+
...stroke.markerMid ? { markerMid: stroke.markerMid } : {},
|
|
2159
|
+
...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
|
|
2160
|
+
};
|
|
2161
|
+
const outline = strokeToPath(path, kit);
|
|
2162
|
+
const attrs = [`d="${serializePathD(outline)}"`, ...fillAttrs, 'stroke="none"'];
|
|
2163
|
+
if (transform) attrs.push(transform);
|
|
2164
|
+
attrs.push(`${WEASEL_NS_PREFIX}:${ROLE}="${OUTLINE}"`);
|
|
2165
|
+
return `<path ${attrs.join(" ")}/>`;
|
|
2166
|
+
}
|
|
2167
|
+
function strokeGroupXml(opacity, inner) {
|
|
2168
|
+
return `<g ${opacity} ${WEASEL_NS_PREFIX}:${ROLE}="${GROUP}">${inner}</g>`;
|
|
2169
|
+
}
|
|
2170
|
+
function unwrapStrokeGroup(el, node) {
|
|
2171
|
+
if (attrOf(el, ROLE) !== GROUP || !node || Array.isArray(node) || node.kind !== "group") return node;
|
|
2172
|
+
const [only] = node.children;
|
|
2173
|
+
if (node.children.length !== 1 || only.kind !== "path") return node;
|
|
2174
|
+
return node.opacity != null ? { ...only, opacity: node.opacity * (only.opacity ?? 1) } : only;
|
|
2175
|
+
}
|
|
2176
|
+
function isStrokeOutline(el) {
|
|
2177
|
+
return attrOf(el, ROLE) === OUTLINE;
|
|
2178
|
+
}
|
|
2179
|
+
function withVariableWidth(el, stroke) {
|
|
2180
|
+
const width2 = parseFloat(attrOf(el, WIDTH) ?? "");
|
|
2181
|
+
if (!stroke || !Number.isFinite(width2)) return stroke;
|
|
2182
|
+
const out = { ...stroke, width: typeof stroke.width === "number" ? width2 : { px: width2 } };
|
|
2183
|
+
const list = attrOf(el, VERTEX_WIDTHS);
|
|
2184
|
+
if (list) out.vertexWidths = list.trim().split(/\s+/).map(widthOfToken);
|
|
2185
|
+
const [angle, ratio] = (attrOf(el, NIB) ?? "").trim().split(/\s+/).map(Number);
|
|
2186
|
+
if (Number.isFinite(angle) && Number.isFinite(ratio)) out.nib = { angle, ratio };
|
|
2187
|
+
for (const [attr, field] of [["marker-start", "markerStart"], ["marker-mid", "markerMid"], ["marker-end", "markerEnd"]]) {
|
|
2188
|
+
const id = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attrOf(el, attr)?.trim() ?? "")?.[1];
|
|
2189
|
+
if (id) out[field] = id;
|
|
2190
|
+
}
|
|
2191
|
+
return out;
|
|
2192
|
+
}
|
|
2099
2193
|
var near = (v, target) => Math.abs(v - target) <= 1e-9;
|
|
2100
2194
|
function placeBox(box, m, stretch) {
|
|
2101
2195
|
const [a, b, c, d] = m;
|
|
@@ -2238,6 +2332,32 @@ function anchorOffset(style, width2, text) {
|
|
|
2238
2332
|
}
|
|
2239
2333
|
|
|
2240
2334
|
// src/tspanLines.ts
|
|
2335
|
+
var LINES_ATTR = "data-weasel-lines";
|
|
2336
|
+
var LINE_HEIGHT_ATTR = "data-weasel-line-height";
|
|
2337
|
+
var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
|
|
2338
|
+
function linesXml(pieces, x, pitch) {
|
|
2339
|
+
let out = "";
|
|
2340
|
+
let pending = 0;
|
|
2341
|
+
let wrote = false;
|
|
2342
|
+
const tspan = (attrs, text) => `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
|
|
2343
|
+
const lineAttrs = () => {
|
|
2344
|
+
if (pending === 0) return [];
|
|
2345
|
+
const attrs = [`x="${trimNumber(x)}"`, `dy="${trimNumber(pending * pitch)}"`];
|
|
2346
|
+
if (pending > 1 || !wrote) attrs.push(`${LINES_ATTR}="${pending}"`);
|
|
2347
|
+
pending = 0;
|
|
2348
|
+
return attrs;
|
|
2349
|
+
};
|
|
2350
|
+
for (const piece of pieces) {
|
|
2351
|
+
piece.text.split(/[\n\u000B\u000C]/).forEach((line, i) => {
|
|
2352
|
+
if (i > 0) pending++;
|
|
2353
|
+
if (line === "") return;
|
|
2354
|
+
const attrs = [...piece.attrs ?? [], ...lineAttrs()];
|
|
2355
|
+
out += piece.attrs === null && attrs.length === 0 ? escapeText(line) : tspan(attrs, line);
|
|
2356
|
+
wrote = true;
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
return pending > 0 ? out + tspan(lineAttrs(), "") : out;
|
|
2360
|
+
}
|
|
2241
2361
|
var DEFAULT_LINE_HEIGHT = 1.2;
|
|
2242
2362
|
var LineTracker = class {
|
|
2243
2363
|
constructor(text, onWarn) {
|
|
@@ -2250,9 +2370,9 @@ var LineTracker = class {
|
|
|
2250
2370
|
/** Where the current line sits, in user units. */
|
|
2251
2371
|
at;
|
|
2252
2372
|
pitches = [];
|
|
2253
|
-
/**
|
|
2254
|
-
*
|
|
2255
|
-
* placement is not a pitch. */
|
|
2373
|
+
/** How many lines down `tspan` opens a new line, or 0 when it stays on the
|
|
2374
|
+
* current one. Call once per tspan, in order. `afterText` is whether any
|
|
2375
|
+
* text precedes it: the first line's own placement is not a pitch. */
|
|
2256
2376
|
startsLine(tspan, afterText) {
|
|
2257
2377
|
const y = tspan.getAttribute("y");
|
|
2258
2378
|
const movedY = y != null && y !== this.y;
|
|
@@ -2260,10 +2380,11 @@ var LineTracker = class {
|
|
|
2260
2380
|
const from = this.at;
|
|
2261
2381
|
const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
|
|
2262
2382
|
this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
|
|
2263
|
-
if (!tspan.hasAttribute("x")) return
|
|
2264
|
-
const
|
|
2265
|
-
|
|
2266
|
-
|
|
2383
|
+
if (!tspan.hasAttribute("x") || !(movedY || dy !== 0)) return 0;
|
|
2384
|
+
const stated = Number(tspan.getAttribute(LINES_ATTR) ?? NaN);
|
|
2385
|
+
const lines = Number.isInteger(stated) && stated >= 0 ? stated : 1;
|
|
2386
|
+
if (afterText && lines > 0 && this.at > from) this.pitches.push((this.at - from) / lines);
|
|
2387
|
+
return lines;
|
|
2267
2388
|
}
|
|
2268
2389
|
/**
|
|
2269
2390
|
* The line height the lines were pitched at, as a multiple of `fontSize`:
|
|
@@ -2278,7 +2399,7 @@ var LineTracker = class {
|
|
|
2278
2399
|
if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
|
|
2279
2400
|
this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
|
|
2280
2401
|
}
|
|
2281
|
-
const lineHeight = mean / fontSize;
|
|
2402
|
+
const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
|
|
2282
2403
|
return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
|
|
2283
2404
|
}
|
|
2284
2405
|
};
|
|
@@ -2288,20 +2409,33 @@ function warnUnreadPositions(el, onWarn) {
|
|
|
2288
2409
|
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2289
2410
|
if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
|
|
2290
2411
|
}
|
|
2291
|
-
for (const name of ["dx", "rotate"]) {
|
|
2412
|
+
for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
|
|
2292
2413
|
const raw = el.getAttribute(name)?.trim() ?? "";
|
|
2293
2414
|
if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
|
|
2294
2415
|
onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
|
|
2295
2416
|
}
|
|
2296
2417
|
}
|
|
2297
2418
|
}
|
|
2298
|
-
function
|
|
2419
|
+
function sameStyle(a, b) {
|
|
2420
|
+
const style = (run) => JSON.stringify(
|
|
2421
|
+
Object.entries(run).filter(([k, v]) => k !== "text" && v !== void 0).sort(([j], [k]) => j < k ? -1 : 1)
|
|
2422
|
+
);
|
|
2423
|
+
return style(a) === style(b);
|
|
2424
|
+
}
|
|
2425
|
+
function joinLines(lines, leading = 0) {
|
|
2299
2426
|
const out = [];
|
|
2300
|
-
let breaks =
|
|
2427
|
+
let breaks = leading;
|
|
2301
2428
|
for (const line of lines) {
|
|
2302
2429
|
if (line.length > 0) {
|
|
2303
|
-
|
|
2304
|
-
|
|
2430
|
+
const [first, ...rest] = line;
|
|
2431
|
+
const head = breaks > 0 ? { ...first, text: "\n".repeat(breaks) + first.text } : first;
|
|
2432
|
+
const last = out[out.length - 1];
|
|
2433
|
+
if (breaks > 0 && last && sameStyle(last, head)) {
|
|
2434
|
+
out[out.length - 1] = { ...last, text: last.text + head.text };
|
|
2435
|
+
} else {
|
|
2436
|
+
out.push(head);
|
|
2437
|
+
}
|
|
2438
|
+
out.push(...rest);
|
|
2305
2439
|
breaks = 0;
|
|
2306
2440
|
}
|
|
2307
2441
|
if (out.length > 0) breaks++;
|
|
@@ -2309,14 +2443,124 @@ function joinLines(lines) {
|
|
|
2309
2443
|
return out;
|
|
2310
2444
|
}
|
|
2311
2445
|
|
|
2312
|
-
// src/
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2446
|
+
// src/laidLines.ts
|
|
2447
|
+
var LAID_ATTR = "data-weasel-layout";
|
|
2448
|
+
function layoutMeasured(layout, text) {
|
|
2449
|
+
return layout.laid.bounds.width > 0 || text.trim() === "";
|
|
2450
|
+
}
|
|
2451
|
+
function anchorX(cells, { anchor, rtl }) {
|
|
2452
|
+
const left = Math.min(...cells.map((c) => c.x));
|
|
2453
|
+
const right = Math.max(...cells.map((c) => c.x + c.advance));
|
|
2454
|
+
if (anchor === "middle") return (left + right) / 2;
|
|
2455
|
+
return anchor === "start" === !rtl ? left : right;
|
|
2456
|
+
}
|
|
2457
|
+
function wordStarts(text, start, end) {
|
|
2458
|
+
const out = [start];
|
|
2459
|
+
for (let i = start + 1; i < end; i++) {
|
|
2460
|
+
if (!/\s/.test(text[i]) && /\s/.test(text[i - 1])) out.push(i);
|
|
2461
|
+
}
|
|
2462
|
+
return out;
|
|
2463
|
+
}
|
|
2464
|
+
function rangeXml(pieces, offsets, start, end, lead) {
|
|
2465
|
+
let out = "";
|
|
2466
|
+
let first = true;
|
|
2467
|
+
pieces.forEach((piece, k) => {
|
|
2468
|
+
const from = Math.max(start, offsets[k]);
|
|
2469
|
+
const to = Math.min(end, offsets[k] + piece.text.length);
|
|
2470
|
+
if (from >= to) return;
|
|
2471
|
+
const text = piece.text.slice(from - offsets[k], to - offsets[k]);
|
|
2472
|
+
const attrs = [...piece.attrs ?? [], ...first ? lead : []];
|
|
2473
|
+
first = false;
|
|
2474
|
+
out += piece.attrs === null && attrs.length === 0 ? escapeText(text) : `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
|
|
2475
|
+
});
|
|
2476
|
+
return out;
|
|
2477
|
+
}
|
|
2478
|
+
var HARD_BREAKS = /\r\n|[\n\v\f\r\u0085\u2028\u2029]/g;
|
|
2479
|
+
var breaksIn = (text) => text.match(HARD_BREAKS)?.length ?? 0;
|
|
2480
|
+
function laidLinesXml(pieces, layout, anchor) {
|
|
2481
|
+
const text = pieces.map((p) => p.text).join("");
|
|
2482
|
+
const offsets = [];
|
|
2483
|
+
pieces.reduce((at, p) => {
|
|
2484
|
+
offsets.push(at);
|
|
2485
|
+
return at + p.text.length;
|
|
2486
|
+
}, 0);
|
|
2487
|
+
const px = (x) => `x="${trimNumber(layout.x + x)}"`;
|
|
2488
|
+
const py = (line) => `y="${trimNumber(layout.y + line.baselineY)}"`;
|
|
2489
|
+
let out = "";
|
|
2490
|
+
let wrote = false;
|
|
2491
|
+
let prevEnd = 0;
|
|
2492
|
+
const count = (lines) => {
|
|
2493
|
+
const stated = lines !== 1 || !wrote ? [`${LINES_ATTR}="${lines}"`] : [];
|
|
2494
|
+
wrote = true;
|
|
2495
|
+
return stated;
|
|
2496
|
+
};
|
|
2497
|
+
for (const line of layout.laid.lines) {
|
|
2498
|
+
if (line.cells.length === 0) continue;
|
|
2499
|
+
const start = Math.min(...line.cells.map((c) => c.srcIndex));
|
|
2500
|
+
const stepped = breaksIn(text.slice(prevEnd, start));
|
|
2501
|
+
prevEnd = line.srcEnd;
|
|
2502
|
+
const starts = line.justified ? wordStarts(text, start, line.srcEnd) : [start];
|
|
2503
|
+
starts.forEach((from, k) => {
|
|
2504
|
+
const to = starts[k + 1] ?? line.srcEnd;
|
|
2505
|
+
const cells = line.cells.filter((c) => c.srcIndex >= from && c.srcIndex < to);
|
|
2506
|
+
const lead = [px(anchorX(cells, anchor)), ...k === 0 ? [py(line), ...count(stepped)] : []];
|
|
2507
|
+
out += rangeXml(pieces, offsets, from, to, lead);
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
const trailing = breaksIn(text.slice(prevEnd));
|
|
2511
|
+
const last = layout.laid.lines[layout.laid.lines.length - 1];
|
|
2512
|
+
if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
|
|
2513
|
+
return out;
|
|
2514
|
+
}
|
|
2515
|
+
var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
|
|
2516
|
+
function readDx(el, onWarn) {
|
|
2517
|
+
const raw = el.getAttribute("dx")?.trim();
|
|
2518
|
+
if (!raw) return null;
|
|
2519
|
+
const values = raw.split(/[\s,]+/).map((v) => resolveLength(v, el, "dx", "x", 0, onWarn));
|
|
2520
|
+
return values.some((v) => v !== 0) ? values : null;
|
|
2521
|
+
}
|
|
2522
|
+
var spacingOf = (run, inherited2) => resolveScreenLength(run.letterSpacing ?? inherited2 ?? 0, 1);
|
|
2523
|
+
function spread(run, extra, inherited2) {
|
|
2524
|
+
const base = spacingOf(run, inherited2);
|
|
2525
|
+
const out = [];
|
|
2526
|
+
let last;
|
|
2527
|
+
Array.from(run.text).forEach((glyph, i) => {
|
|
2528
|
+
const more = extra[i] ?? 0;
|
|
2529
|
+
if (out.length > 0 && more === last) out[out.length - 1].text += glyph;
|
|
2530
|
+
else out.push(more === 0 ? { ...run, text: glyph } : { ...run, text: glyph, letterSpacing: base + more });
|
|
2531
|
+
last = more;
|
|
2532
|
+
});
|
|
2533
|
+
return out;
|
|
2534
|
+
}
|
|
2535
|
+
function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
|
|
2536
|
+
const line = lines[lines.length - 1];
|
|
2537
|
+
const dx = readDx(tspan, onWarn);
|
|
2538
|
+
if (!dx) {
|
|
2539
|
+
line.push(run);
|
|
2540
|
+
return 0;
|
|
2541
|
+
}
|
|
2542
|
+
const raw = tspan.getAttribute("dx");
|
|
2543
|
+
if (COLLAPSIBLE.test(run.text)) {
|
|
2544
|
+
onWarn(`<tspan dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
|
|
2545
|
+
line.push(run);
|
|
2546
|
+
return 0;
|
|
2547
|
+
}
|
|
2548
|
+
let lead = 0;
|
|
2549
|
+
let before = line.length - 1;
|
|
2550
|
+
while (before >= 0 && line[before].text === "") before--;
|
|
2551
|
+
if (dx[0] !== 0 && before >= 0) {
|
|
2552
|
+
const glyphs = Array.from(line[before].text);
|
|
2553
|
+
line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]], inherited2));
|
|
2554
|
+
} else if (dx[0] !== 0 && lines.length === 1) {
|
|
2555
|
+
lead = dx[0];
|
|
2556
|
+
} else if (dx[0] !== 0) {
|
|
2557
|
+
onWarn(`<tspan dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
|
|
2558
|
+
}
|
|
2559
|
+
line.push(...spread(run, dx.slice(1), inherited2));
|
|
2560
|
+
return lead;
|
|
2317
2561
|
}
|
|
2318
2562
|
|
|
2319
|
-
// src/
|
|
2563
|
+
// src/textWhitespace.ts
|
|
2320
2564
|
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
2321
2565
|
function preservesSpace(el) {
|
|
2322
2566
|
for (let e = el; e; e = e.parentElement) {
|
|
@@ -2353,6 +2597,15 @@ function collapseRunWhitespace(runs) {
|
|
|
2353
2597
|
if (runs[i].text === "") runs.splice(i, 1);
|
|
2354
2598
|
}
|
|
2355
2599
|
}
|
|
2600
|
+
|
|
2601
|
+
// src/fillPaint.ts
|
|
2602
|
+
function mapFillPaint(fill, f) {
|
|
2603
|
+
if (!fill.paint) return fill;
|
|
2604
|
+
const paint = f(fill.paint);
|
|
2605
|
+
return paint === fill.paint ? fill : { ...fill, paint };
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// src/readText.ts
|
|
2356
2609
|
var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
|
|
2357
2610
|
function laidOutWidth(runs, style, plain) {
|
|
2358
2611
|
const resolved = resolveTextStyle(style);
|
|
@@ -2367,6 +2620,10 @@ function laidOutWidth(runs, style, plain) {
|
|
|
2367
2620
|
const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
|
|
2368
2621
|
return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
|
|
2369
2622
|
}
|
|
2623
|
+
function numberAttr(el, name) {
|
|
2624
|
+
const n = parseFloat(el.getAttribute(name) ?? "");
|
|
2625
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2626
|
+
}
|
|
2370
2627
|
function placeTextNode(node, m, onWarn) {
|
|
2371
2628
|
const cx = node.x + node.width / 2;
|
|
2372
2629
|
const cy = node.y + node.height / 2;
|
|
@@ -2408,38 +2665,14 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2408
2665
|
const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
|
|
2409
2666
|
const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
|
|
2410
2667
|
const dominantBaseline = el.getAttribute("dominant-baseline");
|
|
2411
|
-
const
|
|
2668
|
+
const laid = el.hasAttribute(LAID_ATTR);
|
|
2669
|
+
const explicitTopAnchor = laid || dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
2412
2670
|
const topY = explicitTopAnchor ? ay : ay - fontSize;
|
|
2413
2671
|
const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
|
|
2414
|
-
const runLines = [[]];
|
|
2415
2672
|
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);
|
|
2673
|
+
const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
|
|
2441
2674
|
const plain = runs.map((r) => r.text).join("");
|
|
2442
|
-
const pitched = tracker.lineHeight(fontSize);
|
|
2675
|
+
const pitched = laid ? numberAttr(el, LINE_HEIGHT_ATTR) : tracker.lineHeight(fontSize);
|
|
2443
2676
|
if (pitched !== void 0) textStyle.lineHeight = pitched;
|
|
2444
2677
|
const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
|
|
2445
2678
|
const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
|
|
@@ -2450,7 +2683,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2450
2683
|
const opacity = ownValue(el, "opacity");
|
|
2451
2684
|
const node = {
|
|
2452
2685
|
kind: "text",
|
|
2453
|
-
x: ax - anchorOffset(textStyle, width2, plain),
|
|
2686
|
+
x: ax + lead - anchorOffset(textStyle, width2, plain),
|
|
2454
2687
|
y: topY,
|
|
2455
2688
|
width: width2,
|
|
2456
2689
|
height: height2,
|
|
@@ -2476,6 +2709,38 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
2476
2709
|
placeTextNode(node, multiply(ctm, localTransform), onWarn);
|
|
2477
2710
|
return node;
|
|
2478
2711
|
}
|
|
2712
|
+
function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafStyle, onWarn) {
|
|
2713
|
+
const runLines = [[]];
|
|
2714
|
+
let leading = 0;
|
|
2715
|
+
let lead = 0;
|
|
2716
|
+
const push = (run) => {
|
|
2717
|
+
runLines[runLines.length - 1].push(run);
|
|
2718
|
+
};
|
|
2719
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
2720
|
+
const child = el.childNodes[i];
|
|
2721
|
+
if (child.nodeType === 3) {
|
|
2722
|
+
const t = child.textContent ?? "";
|
|
2723
|
+
if (!t) continue;
|
|
2724
|
+
push({ text: t, ...inheritedShift });
|
|
2725
|
+
} else if (child.nodeType === 1) {
|
|
2726
|
+
const sp = child;
|
|
2727
|
+
if (sp.tagName.toLowerCase() !== "tspan") {
|
|
2728
|
+
onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
|
|
2729
|
+
const t = sp.textContent ?? "";
|
|
2730
|
+
if (t) push({ text: t, ...inheritedShift });
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
warnUnreadPositions(sp, onWarn);
|
|
2734
|
+
const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
|
|
2735
|
+
const lines = tracker.startsLine(sp, afterText);
|
|
2736
|
+
if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
|
|
2737
|
+
else for (let n = 0; n < lines; n++) runLines.push([]);
|
|
2738
|
+
lead += placeTspanRun(sp, readTspanRun(sp, gradients, leafStyle, onWarn), runLines, letterSpacing, onWarn);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
|
|
2742
|
+
return { runs: joinLines(runLines, leading), lead };
|
|
2743
|
+
}
|
|
2479
2744
|
function readTspanRun(el, gradients, style, onWarn) {
|
|
2480
2745
|
const text = el.textContent ?? "";
|
|
2481
2746
|
const run = { text };
|
|
@@ -2922,10 +3187,10 @@ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix
|
|
|
2922
3187
|
for (let i = 0; i < parent.children.length; i++) {
|
|
2923
3188
|
const el = parent.children[i];
|
|
2924
3189
|
const ns = el.namespaceURI;
|
|
2925
|
-
if (ns && ns !== "http://www.w3.org/2000/svg") continue;
|
|
3190
|
+
if (ns && ns !== "http://www.w3.org/2000/svg" || isStrokeOutline(el)) continue;
|
|
2926
3191
|
const hidden = isHiddenElement(el, style);
|
|
2927
3192
|
if (hidden && !keepHidden) continue;
|
|
2928
|
-
const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
|
|
3193
|
+
const node = unwrapStrokeGroup(el, parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden));
|
|
2929
3194
|
if (!node) continue;
|
|
2930
3195
|
const parsed = Array.isArray(node) ? node : [node];
|
|
2931
3196
|
if (hidden) for (const n of parsed) n.hidden = true;
|
|
@@ -3018,7 +3283,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, kee
|
|
|
3018
3283
|
}
|
|
3019
3284
|
const leafStyle = deriveStyle(style, el);
|
|
3020
3285
|
const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
|
|
3021
|
-
const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
|
|
3286
|
+
const ownStroke = withVariableWidth(el, readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el)));
|
|
3022
3287
|
const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
|
|
3023
3288
|
if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
|
|
3024
3289
|
onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
|
|
@@ -3132,25 +3397,45 @@ function pathBounds(path) {
|
|
|
3132
3397
|
|
|
3133
3398
|
// src/openSubpathFill.ts
|
|
3134
3399
|
var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
|
|
3400
|
+
var marked = (stroke) => capsEnds(stroke) || stroke?.markerMid !== void 0;
|
|
3135
3401
|
function withoutEndMarkers(stroke) {
|
|
3136
3402
|
const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
|
|
3137
3403
|
return rest;
|
|
3138
3404
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3405
|
+
var closedStroke = (stroke) => stroke && capsEnds(stroke) ? withoutEndMarkers(stroke) : stroke;
|
|
3406
|
+
var isClosed = (part) => splitByClosure(part).open === null;
|
|
3407
|
+
function samePath(a, b) {
|
|
3408
|
+
if (a === b) return true;
|
|
3409
|
+
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]);
|
|
3410
|
+
}
|
|
3411
|
+
function strokeParts(path, stroke, filled, closed, open) {
|
|
3412
|
+
const none = { kind: "none" };
|
|
3413
|
+
const subs = marked(stroke) ? splitSubpaths(path) : [];
|
|
3414
|
+
if (subs.length > 1) {
|
|
3415
|
+
return subs.map((sub) => ({ path: sub, fill: none, stroke: isClosed(sub) ? closedStroke(stroke) : stroke }));
|
|
3416
|
+
}
|
|
3417
|
+
if (closed && open && filled === closed) {
|
|
3418
|
+
return [{ path: closed, fill: none, stroke: closedStroke(stroke) }, { path: open, fill: none, stroke }];
|
|
3419
|
+
}
|
|
3420
|
+
return [{ path, fill: none, stroke: open ? stroke : closedStroke(stroke) }];
|
|
3421
|
+
}
|
|
3422
|
+
function pathAsSvgDraws(node) {
|
|
3423
|
+
const { path, fill, stroke } = node;
|
|
3424
|
+
if (path.kind !== "polygon") return capsEnds(stroke) ? { ...node, stroke: closedStroke(stroke) } : node;
|
|
3425
|
+
if (path.commands.length === 0) return node;
|
|
3142
3426
|
const { closed, open } = splitByClosure(path);
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3145
|
-
const
|
|
3146
|
-
if (
|
|
3147
|
-
|
|
3427
|
+
const filled = fill.kind === "none" ? null : fillsOpenSubpaths(path) ? path : closed;
|
|
3428
|
+
const parts = strokeParts(path, stroke, filled, closed, open);
|
|
3429
|
+
const pieces = filled ? withFill(parts, filled, fill) : parts;
|
|
3430
|
+
if (pieces.length === 1) {
|
|
3431
|
+
const only = pieces[0];
|
|
3432
|
+
if (only.fill === fill && only.stroke === stroke) return node;
|
|
3433
|
+
const { stroke: _stroke, ...rest } = node;
|
|
3434
|
+
return { ...rest, fill: only.fill, ...only.stroke ? { stroke: only.stroke } : {} };
|
|
3435
|
+
}
|
|
3148
3436
|
const group = {
|
|
3149
3437
|
kind: "group",
|
|
3150
|
-
children:
|
|
3151
|
-
...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
|
|
3152
|
-
...opens.map((part) => piece(part, { kind: "none" }))
|
|
3153
|
-
]
|
|
3438
|
+
children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
|
|
3154
3439
|
};
|
|
3155
3440
|
if (node.rotation != null && node.rotation !== 0) {
|
|
3156
3441
|
const b = pathBounds(path);
|
|
@@ -3161,205 +3446,28 @@ function withOpenSubpathsUnfilled(node) {
|
|
|
3161
3446
|
if (node.meta) group.meta = node.meta;
|
|
3162
3447
|
return group;
|
|
3163
3448
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
var isClose = (t) => t.startsWith("</");
|
|
3169
|
-
var isSelfClosing = (t) => t.endsWith("/>");
|
|
3170
|
-
var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
|
|
3171
|
-
function matchingClose(tokens, open) {
|
|
3172
|
-
let depth = 0;
|
|
3173
|
-
for (let i = open; i < tokens.length; i++) {
|
|
3174
|
-
const t = tokens[i];
|
|
3175
|
-
if (!isTag(t) || isSelfClosing(t)) continue;
|
|
3176
|
-
depth += isClose(t) ? -1 : 1;
|
|
3177
|
-
if (depth === 0) return i;
|
|
3178
|
-
}
|
|
3179
|
-
return tokens.length - 1;
|
|
3180
|
-
}
|
|
3181
|
-
function prettyXml(xml) {
|
|
3182
|
-
const tokens = xml.match(TOKEN2) ?? [];
|
|
3183
|
-
const lines = [];
|
|
3184
|
-
let depth = 0;
|
|
3185
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
3186
|
-
const t = tokens[i];
|
|
3187
|
-
if (!isTag(t) || isSelfClosing(t)) {
|
|
3188
|
-
lines.push(" ".repeat(depth) + t);
|
|
3189
|
-
} else if (isClose(t)) {
|
|
3190
|
-
depth--;
|
|
3191
|
-
lines.push(" ".repeat(depth) + t);
|
|
3192
|
-
} else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
|
|
3193
|
-
const close2 = matchingClose(tokens, i);
|
|
3194
|
-
lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
|
|
3195
|
-
i = close2;
|
|
3196
|
-
} else {
|
|
3197
|
-
lines.push(" ".repeat(depth) + t);
|
|
3198
|
-
depth++;
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
return lines.join("\n");
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
// src/serialize.ts
|
|
3205
|
-
function serializeSvg(nodes, opts = {}) {
|
|
3206
|
-
const registry = new PaintServerRegistry();
|
|
3207
|
-
registerPaintServers(nodes, registry);
|
|
3208
|
-
const bounds = opts.viewBox ?? computeBounds(nodes);
|
|
3209
|
-
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
3210
|
-
const namespaces = opts.namespaces ?? {};
|
|
3211
|
-
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
3212
|
-
if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
|
|
3213
|
-
rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
|
|
3214
|
-
}
|
|
3215
|
-
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
3216
|
-
rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
|
|
3217
|
-
}
|
|
3218
|
-
rootAttrs.push(`viewBox="${vb}"`);
|
|
3219
|
-
if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
|
|
3220
|
-
if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
|
|
3221
|
-
if (opts.documentMeta) {
|
|
3222
|
-
for (const prefix of Object.keys(namespaces)) {
|
|
3223
|
-
const bucket = opts.documentMeta[prefix];
|
|
3224
|
-
if (!bucket?.attrs) continue;
|
|
3225
|
-
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
3226
|
-
rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
|
|
3231
|
-
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
3232
|
-
const said = /* @__PURE__ */ new Set();
|
|
3233
|
-
const warn = (m) => {
|
|
3234
|
-
if (!opts.onWarn || said.has(m)) return;
|
|
3235
|
-
said.add(m);
|
|
3236
|
-
opts.onWarn(m);
|
|
3237
|
-
};
|
|
3238
|
-
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
|
|
3239
|
-
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
3240
|
-
const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
3241
|
-
return opts.pretty ? prettyXml(xml) : xml;
|
|
3242
|
-
}
|
|
3243
|
-
function registerPaintServers(nodes, registry) {
|
|
3244
|
-
for (const n of nodes) {
|
|
3245
|
-
if (n.kind === "group") {
|
|
3246
|
-
if (n.clip) registry.clipId(n.clip);
|
|
3247
|
-
registerPaintServers(n.children, registry);
|
|
3248
|
-
} else if (n.kind === "path") {
|
|
3249
|
-
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
3250
|
-
if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
|
|
3251
|
-
registerMarkers(n.stroke, registry);
|
|
3252
|
-
} else if (n.kind === "text") {
|
|
3253
|
-
registerTextPaint(n.fill?.paint, registry);
|
|
3254
|
-
registerTextPaint(n.stroke?.paint, registry);
|
|
3255
|
-
registerMarkers(n.stroke, registry);
|
|
3256
|
-
for (const run of n.runs ?? []) {
|
|
3257
|
-
registerTextPaint(run.fill?.paint, registry);
|
|
3258
|
-
registerTextPaint(run.stroke?.paint, registry);
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
function registerTextPaint(paint, registry) {
|
|
3264
|
-
if (paint && !isSolidPaint(paint)) registry.register(paint);
|
|
3265
|
-
}
|
|
3266
|
-
function registerMarkers(stroke, registry) {
|
|
3267
|
-
if (!stroke) return;
|
|
3268
|
-
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
3269
|
-
if (ref !== void 0) registry.markerId(ref);
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
function nodeXml(node, registry, namespaces, warn) {
|
|
3273
|
-
const xml = kindXml(node, registry, namespaces, warn);
|
|
3274
|
-
return node.hidden ? xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`) : xml;
|
|
3275
|
-
}
|
|
3276
|
-
function kindXml(node, registry, namespaces, warn) {
|
|
3277
|
-
if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
|
|
3278
|
-
if (node.kind === "text") return textXml(node, registry, namespaces, warn);
|
|
3279
|
-
if (node.kind === "image") return imageXml(node, namespaces);
|
|
3280
|
-
const drawn = withOpenSubpathsUnfilled(node);
|
|
3281
|
-
return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
|
|
3449
|
+
function withFill(parts, filled, fill) {
|
|
3450
|
+
const host = parts.findIndex((p) => samePath(p.path, filled));
|
|
3451
|
+
if (host < 0) return [{ path: filled, fill, stroke: void 0 }, ...parts];
|
|
3452
|
+
return [{ ...parts[host], fill }, ...parts.filter((_, i) => i !== host)];
|
|
3282
3453
|
}
|
|
3283
3454
|
function warnStrokeAlign(align, warn) {
|
|
3284
3455
|
if (align && align !== "center") {
|
|
3285
3456
|
warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
|
|
3286
3457
|
}
|
|
3287
3458
|
}
|
|
3288
|
-
function imageXml(node, namespaces) {
|
|
3289
|
-
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
3290
|
-
const outer = [];
|
|
3291
|
-
if (node.opacity != null && node.opacity !== 1) {
|
|
3292
|
-
outer.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3293
|
-
}
|
|
3294
|
-
const frame = boxTransformAttr(node);
|
|
3295
|
-
if (frame) outer.push(frame);
|
|
3296
|
-
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3297
|
-
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3298
|
-
const href = `href="${escapeAttr(node.href)}"`;
|
|
3299
|
-
if (!node.source && !node.flipX && !node.flipY) {
|
|
3300
|
-
const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
|
|
3301
|
-
if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
|
|
3302
|
-
return `<image ${attrs}${metaAttrs}/>`;
|
|
3303
|
-
}
|
|
3304
|
-
const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3305
|
-
const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
|
|
3306
|
-
const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
|
|
3307
|
-
const head = ['<g data-weasel-image="true"', ...outer].join(" ");
|
|
3308
|
-
return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
|
|
3309
|
-
}
|
|
3310
|
-
function groupXml(node, registry, namespaces, warn) {
|
|
3311
|
-
const attrs = [];
|
|
3312
|
-
if (node.transform) {
|
|
3313
|
-
const m = formatMatrix(node.transform);
|
|
3314
|
-
if (m) attrs.push(`transform="${m}"`);
|
|
3315
|
-
}
|
|
3316
|
-
if (node.clip) {
|
|
3317
|
-
attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
|
|
3318
|
-
}
|
|
3319
|
-
if (node.opacity != null && node.opacity !== 1) {
|
|
3320
|
-
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3321
|
-
}
|
|
3322
|
-
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3323
|
-
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
3324
|
-
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
|
|
3325
|
-
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
3326
|
-
}
|
|
3327
|
-
function pathXml(node, registry, namespaces, warn) {
|
|
3328
|
-
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
3329
|
-
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
3330
|
-
for (const a of fillAttrs) attrs.push(a);
|
|
3331
|
-
if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
|
|
3332
|
-
attrs.push(`fill-rule="evenodd"`);
|
|
3333
|
-
}
|
|
3334
|
-
if (node.stroke) {
|
|
3335
|
-
const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
|
|
3336
|
-
for (const a of strokeAttrs) attrs.push(a);
|
|
3337
|
-
} else {
|
|
3338
|
-
attrs.push('stroke="none"');
|
|
3339
|
-
}
|
|
3340
|
-
if (node.opacity != null && node.opacity !== 1) {
|
|
3341
|
-
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3342
|
-
}
|
|
3343
|
-
if (node.rotation != null && node.rotation !== 0) {
|
|
3344
|
-
const b = pathBounds(node.path);
|
|
3345
|
-
const cx = b.minX + (b.maxX - b.minX) / 2;
|
|
3346
|
-
const cy = b.minY + (b.maxY - b.minY) / 2;
|
|
3347
|
-
const deg = node.rotation * 180 / Math.PI;
|
|
3348
|
-
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
3349
|
-
}
|
|
3350
|
-
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3351
|
-
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3352
|
-
if (metaEls) {
|
|
3353
|
-
return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
|
|
3354
|
-
}
|
|
3355
|
-
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
3356
|
-
}
|
|
3357
3459
|
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
3358
3460
|
if (paint.kind === "none") return [`${name}="none"`];
|
|
3359
3461
|
const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
|
|
3360
3462
|
if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
|
|
3361
3463
|
return out;
|
|
3362
3464
|
}
|
|
3465
|
+
function strokePaintAsFillAttrs(stroke, registry) {
|
|
3466
|
+
return [
|
|
3467
|
+
...paintAttrs(stroke.paint, "fill", registry, false),
|
|
3468
|
+
...opacityAttr("fill-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint))
|
|
3469
|
+
];
|
|
3470
|
+
}
|
|
3363
3471
|
function svgPaintOpacity(paint) {
|
|
3364
3472
|
if (paint.kind === "solid") return paint.opacity;
|
|
3365
3473
|
return paint.kind === "gradient" ? paint.paint.opacity : void 0;
|
|
@@ -3429,13 +3537,15 @@ function strokeAttrsFor(stroke, registry, warn) {
|
|
|
3429
3537
|
}
|
|
3430
3538
|
return attrs;
|
|
3431
3539
|
}
|
|
3432
|
-
function textXml(node, registry, namespaces, warn) {
|
|
3540
|
+
function textXml(node, registry, namespaces, warn, given) {
|
|
3541
|
+
const layout = given && layoutMeasured(given, node.text) ? given : null;
|
|
3542
|
+
const x = node.x + anchorOffset(node.style, node.width, node.text);
|
|
3433
3543
|
const attrs = [
|
|
3434
|
-
`x="${trimNumber(
|
|
3544
|
+
`x="${trimNumber(x)}"`,
|
|
3435
3545
|
`y="${trimNumber(node.y)}"`,
|
|
3436
|
-
`dominant-baseline="text-before-edge"`,
|
|
3437
|
-
//
|
|
3438
|
-
//
|
|
3546
|
+
layout ? `${LAID_ATTR}="lines"` : `dominant-baseline="text-before-edge"`,
|
|
3547
|
+
// SVG's default whitespace handling would collapse indentation and runs
|
|
3548
|
+
// of spaces away on re-import.
|
|
3439
3549
|
`xml:space="preserve"`,
|
|
3440
3550
|
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
3441
3551
|
`data-weasel-height="${trimNumber(node.height)}"`
|
|
@@ -3448,9 +3558,10 @@ function textXml(node, registry, namespaces, warn) {
|
|
|
3448
3558
|
if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
|
|
3449
3559
|
const direction = svgDirection(style, node.text);
|
|
3450
3560
|
if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
|
|
3561
|
+
let anchor = "start";
|
|
3451
3562
|
if (style?.align != null || direction === "rtl") {
|
|
3452
3563
|
const align = style?.align ?? "left";
|
|
3453
|
-
|
|
3564
|
+
anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
3454
3565
|
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
3455
3566
|
if (align === "justify") attrs.push('data-weasel-align="justify"');
|
|
3456
3567
|
}
|
|
@@ -3465,13 +3576,14 @@ function textXml(node, registry, namespaces, warn) {
|
|
|
3465
3576
|
}
|
|
3466
3577
|
if (style?.wrap) {
|
|
3467
3578
|
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)");
|
|
3579
|
+
if (!layout) 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
3580
|
}
|
|
3470
3581
|
if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
|
|
3471
3582
|
if (node.verticalAlign && node.verticalAlign !== "top") {
|
|
3472
3583
|
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)`);
|
|
3584
|
+
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)`);
|
|
3474
3585
|
}
|
|
3586
|
+
if (layout && style?.lineHeight != null) attrs.push(`${LINE_HEIGHT_ATTR}="${trimNumber(style.lineHeight)}"`);
|
|
3475
3587
|
if (node.fill !== void 0) {
|
|
3476
3588
|
const paint = node.fill?.paint;
|
|
3477
3589
|
if (!paint) attrs.push('fill="none"');
|
|
@@ -3486,7 +3598,9 @@ function textXml(node, registry, namespaces, warn) {
|
|
|
3486
3598
|
}
|
|
3487
3599
|
const frame = boxTransformAttr(node);
|
|
3488
3600
|
if (frame) attrs.push(frame);
|
|
3489
|
-
const
|
|
3601
|
+
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 }];
|
|
3602
|
+
const resolved = resolveTextStyle(style);
|
|
3603
|
+
const body = layout ? laidLinesXml(pieces, layout, { anchor, rtl: direction === "rtl" }) : linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
|
|
3490
3604
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3491
3605
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3492
3606
|
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
@@ -3496,7 +3610,7 @@ function scriptSizeFace(run, style) {
|
|
|
3496
3610
|
const face = resolveRunFace(run, resolveTextStyle(style));
|
|
3497
3611
|
return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
|
|
3498
3612
|
}
|
|
3499
|
-
function
|
|
3613
|
+
function runAttrs(run, style, registry, warn) {
|
|
3500
3614
|
const attrs = [];
|
|
3501
3615
|
if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
|
|
3502
3616
|
else if (run.bold) attrs.push(`font-weight="700"`);
|
|
@@ -3526,8 +3640,7 @@ function runXml(run, style, registry, warn) {
|
|
|
3526
3640
|
}
|
|
3527
3641
|
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
3528
3642
|
if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
|
|
3529
|
-
|
|
3530
|
-
return `${head}${escapeText(run.text)}</tspan>`;
|
|
3643
|
+
return attrs;
|
|
3531
3644
|
}
|
|
3532
3645
|
function textDecorationValue(underline, strikethrough, overline) {
|
|
3533
3646
|
const tokens = [];
|
|
@@ -3536,17 +3649,212 @@ function textDecorationValue(underline, strikethrough, overline) {
|
|
|
3536
3649
|
if (overline) tokens.push("overline");
|
|
3537
3650
|
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
3538
3651
|
}
|
|
3539
|
-
|
|
3652
|
+
|
|
3653
|
+
// src/prettyXml.ts
|
|
3654
|
+
var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
|
|
3655
|
+
var isTag = (t) => t !== void 0 && t.startsWith("<");
|
|
3656
|
+
var isClose = (t) => t.startsWith("</");
|
|
3657
|
+
var isSelfClosing = (t) => t.endsWith("/>");
|
|
3658
|
+
var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
|
|
3659
|
+
function matchingClose(tokens, open) {
|
|
3660
|
+
let depth = 0;
|
|
3661
|
+
for (let i = open; i < tokens.length; i++) {
|
|
3662
|
+
const t = tokens[i];
|
|
3663
|
+
if (!isTag(t) || isSelfClosing(t)) continue;
|
|
3664
|
+
depth += isClose(t) ? -1 : 1;
|
|
3665
|
+
if (depth === 0) return i;
|
|
3666
|
+
}
|
|
3667
|
+
return tokens.length - 1;
|
|
3668
|
+
}
|
|
3669
|
+
function prettyXml(xml) {
|
|
3670
|
+
const tokens = xml.match(TOKEN2) ?? [];
|
|
3671
|
+
const lines = [];
|
|
3672
|
+
let depth = 0;
|
|
3673
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3674
|
+
const t = tokens[i];
|
|
3675
|
+
if (!isTag(t) || isSelfClosing(t)) {
|
|
3676
|
+
lines.push(" ".repeat(depth) + t);
|
|
3677
|
+
} else if (isClose(t)) {
|
|
3678
|
+
depth--;
|
|
3679
|
+
lines.push(" ".repeat(depth) + t);
|
|
3680
|
+
} else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
|
|
3681
|
+
const close2 = matchingClose(tokens, i);
|
|
3682
|
+
lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
|
|
3683
|
+
i = close2;
|
|
3684
|
+
} else {
|
|
3685
|
+
lines.push(" ".repeat(depth) + t);
|
|
3686
|
+
depth++;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return lines.join("\n");
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// src/serialize.ts
|
|
3693
|
+
function serializeSvg(nodes, opts = {}) {
|
|
3694
|
+
const registry = new PaintServerRegistry();
|
|
3695
|
+
registerPaintServers(nodes, registry);
|
|
3696
|
+
const bounds = opts.viewBox ?? computeBounds(nodes);
|
|
3697
|
+
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
3698
|
+
const namespaces = opts.namespaces ?? {};
|
|
3699
|
+
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
3700
|
+
if ((registry.usesPrivateNamespace() || writesVariableWidth(nodes)) && namespaces[WEASEL_NS_PREFIX] == null) {
|
|
3701
|
+
rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
|
|
3702
|
+
}
|
|
3703
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
3704
|
+
rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
|
|
3705
|
+
}
|
|
3706
|
+
rootAttrs.push(`viewBox="${vb}"`);
|
|
3707
|
+
if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
|
|
3708
|
+
if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
|
|
3709
|
+
if (opts.documentMeta) {
|
|
3710
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
3711
|
+
const bucket = opts.documentMeta[prefix];
|
|
3712
|
+
if (!bucket?.attrs) continue;
|
|
3713
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
3714
|
+
rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
|
|
3719
|
+
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
3720
|
+
const said = /* @__PURE__ */ new Set();
|
|
3721
|
+
const warn = (m) => {
|
|
3722
|
+
if (!opts.onWarn || said.has(m)) return;
|
|
3723
|
+
said.add(m);
|
|
3724
|
+
opts.onWarn(m);
|
|
3725
|
+
};
|
|
3726
|
+
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn, opts.textLayout)).join("");
|
|
3727
|
+
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
3728
|
+
const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
3729
|
+
return opts.pretty ? prettyXml(xml) : xml;
|
|
3730
|
+
}
|
|
3731
|
+
function registerPaintServers(nodes, registry) {
|
|
3732
|
+
for (const n of nodes) {
|
|
3733
|
+
if (n.kind === "group") {
|
|
3734
|
+
if (n.clip) registry.clipId(n.clip);
|
|
3735
|
+
registerPaintServers(n.children, registry);
|
|
3736
|
+
} else if (n.kind === "path") {
|
|
3737
|
+
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
3738
|
+
if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
|
|
3739
|
+
registerMarkers(n.stroke, registry);
|
|
3740
|
+
} else if (n.kind === "text") {
|
|
3741
|
+
registerTextPaint(n.fill?.paint, registry);
|
|
3742
|
+
registerTextPaint(n.stroke?.paint, registry);
|
|
3743
|
+
registerMarkers(n.stroke, registry);
|
|
3744
|
+
for (const run of n.runs ?? []) {
|
|
3745
|
+
registerTextPaint(run.fill?.paint, registry);
|
|
3746
|
+
registerTextPaint(run.stroke?.paint, registry);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
function registerTextPaint(paint, registry) {
|
|
3752
|
+
if (paint && !isSolidPaint(paint)) registry.register(paint);
|
|
3753
|
+
}
|
|
3754
|
+
function registerMarkers(stroke, registry) {
|
|
3755
|
+
if (!stroke) return;
|
|
3756
|
+
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
3757
|
+
if (ref !== void 0) registry.markerId(ref);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
function nodeXml(node, registry, namespaces, warn, layoutOf) {
|
|
3761
|
+
const xml = kindXml(node, registry, namespaces, warn, layoutOf);
|
|
3762
|
+
if (!node.hidden) return xml;
|
|
3763
|
+
return xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`).replace(/<path (?=[^>]*:role="stroke-outline")/, '<path display="none" ');
|
|
3764
|
+
}
|
|
3765
|
+
function kindXml(node, registry, namespaces, warn, layoutOf) {
|
|
3766
|
+
if (node.kind === "group") return groupXml(node, registry, namespaces, warn, layoutOf);
|
|
3767
|
+
if (node.kind === "text") return textXml(node, registry, namespaces, warn, layoutOf?.(node));
|
|
3768
|
+
if (node.kind === "image") return imageXml(node, namespaces);
|
|
3769
|
+
const drawn = pathAsSvgDraws(node);
|
|
3770
|
+
return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn, layoutOf) : pathXml(drawn, registry, namespaces, warn);
|
|
3771
|
+
}
|
|
3772
|
+
function imageXml(node, namespaces) {
|
|
3773
|
+
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
3774
|
+
const outer = [];
|
|
3775
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
3776
|
+
outer.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3777
|
+
}
|
|
3778
|
+
const frame = boxTransformAttr(node);
|
|
3779
|
+
if (frame) outer.push(frame);
|
|
3780
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3781
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3782
|
+
const href = `href="${escapeAttr(node.href)}"`;
|
|
3783
|
+
if (!node.source && !node.flipX && !node.flipY) {
|
|
3784
|
+
const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
|
|
3785
|
+
if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
|
|
3786
|
+
return `<image ${attrs}${metaAttrs}/>`;
|
|
3787
|
+
}
|
|
3788
|
+
const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
3789
|
+
const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
|
|
3790
|
+
const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
|
|
3791
|
+
const head = ['<g data-weasel-image="true"', ...outer].join(" ");
|
|
3792
|
+
return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
|
|
3793
|
+
}
|
|
3794
|
+
function groupXml(node, registry, namespaces, warn, layoutOf) {
|
|
3795
|
+
const attrs = [];
|
|
3796
|
+
if (node.transform) {
|
|
3797
|
+
const m = formatMatrix(node.transform);
|
|
3798
|
+
if (m) attrs.push(`transform="${m}"`);
|
|
3799
|
+
}
|
|
3800
|
+
if (node.clip) {
|
|
3801
|
+
attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
|
|
3802
|
+
}
|
|
3803
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
3804
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
3805
|
+
}
|
|
3806
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3807
|
+
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
3808
|
+
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn, layoutOf)).join("");
|
|
3809
|
+
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
3810
|
+
}
|
|
3811
|
+
function pathXml(node, registry, namespaces, warn) {
|
|
3812
|
+
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
3813
|
+
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
3814
|
+
for (const a of fillAttrs) attrs.push(a);
|
|
3815
|
+
if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
|
|
3816
|
+
attrs.push(`fill-rule="evenodd"`);
|
|
3817
|
+
}
|
|
3818
|
+
const varies = hasVariableWidth(node.stroke);
|
|
3819
|
+
if (node.stroke) {
|
|
3820
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry, varies ? () => {
|
|
3821
|
+
} : warn);
|
|
3822
|
+
for (const a of varies ? variableWidthAttrs(node.stroke, strokeAttrs) : strokeAttrs) attrs.push(a);
|
|
3823
|
+
} else {
|
|
3824
|
+
attrs.push('stroke="none"');
|
|
3825
|
+
}
|
|
3826
|
+
const opacity = node.opacity != null && node.opacity !== 1 ? `opacity="${trimNumber(node.opacity)}"` : null;
|
|
3827
|
+
if (opacity && !varies) attrs.push(opacity);
|
|
3828
|
+
const transform = rotationAttr(node);
|
|
3829
|
+
if (transform) attrs.push(transform);
|
|
3830
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
3831
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
3832
|
+
const xml = metaEls ? `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>` : `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
3833
|
+
if (!varies) return xml;
|
|
3834
|
+
const both = xml + strokeOutlineXml(node.path, node.stroke, strokePaintAsFillAttrs(node.stroke, registry), transform);
|
|
3835
|
+
return opacity ? strokeGroupXml(opacity, both) : both;
|
|
3836
|
+
}
|
|
3837
|
+
function rotationAttr(node) {
|
|
3838
|
+
if (node.rotation == null || node.rotation === 0) return null;
|
|
3839
|
+
const b = pathBounds(node.path);
|
|
3840
|
+
const cx = b.minX + (b.maxX - b.minX) / 2;
|
|
3841
|
+
const cy = b.minY + (b.maxY - b.minY) / 2;
|
|
3842
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
3843
|
+
return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
|
|
3844
|
+
}
|
|
3845
|
+
function svgNeeds(nodes, opts = {}) {
|
|
3540
3846
|
const registry = new PaintServerRegistry();
|
|
3541
3847
|
registerPaintServers(nodes, registry);
|
|
3542
3848
|
const fonts = /* @__PURE__ */ new Map();
|
|
3849
|
+
const add = (face) => fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
|
|
3543
3850
|
const walk = (list) => {
|
|
3544
3851
|
for (const n of list) {
|
|
3545
3852
|
if (n.kind === "group") walk(n.children);
|
|
3546
3853
|
else if (n.kind === "text") {
|
|
3854
|
+
if (opts.textLayout) layoutFaces(n).forEach(add);
|
|
3547
3855
|
for (const run of n.runs ?? []) {
|
|
3548
3856
|
const face = scriptSizeFace(run, n.style);
|
|
3549
|
-
if (face)
|
|
3857
|
+
if (face) add(face);
|
|
3550
3858
|
}
|
|
3551
3859
|
}
|
|
3552
3860
|
}
|
|
@@ -3554,8 +3862,15 @@ function svgNeeds(nodes) {
|
|
|
3554
3862
|
walk(nodes);
|
|
3555
3863
|
return { fonts: [...fonts.values()], paintKinds: registry.paintKinds() };
|
|
3556
3864
|
}
|
|
3557
|
-
function
|
|
3558
|
-
const
|
|
3865
|
+
function layoutFaces(node) {
|
|
3866
|
+
const style = resolveTextStyle(node.style);
|
|
3867
|
+
return toRuns(node.runs && node.runs.length > 0 ? node.runs : node.text).map((run) => {
|
|
3868
|
+
const face = resolveRunFace(run, style);
|
|
3869
|
+
return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
function warmSvg(nodes, opts = {}) {
|
|
3873
|
+
const needs = svgNeeds(nodes, opts);
|
|
3559
3874
|
return Promise.all([
|
|
3560
3875
|
warmFonts(needs.fonts),
|
|
3561
3876
|
warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
|
|
@@ -3608,7 +3923,9 @@ function strokeDataFromSvg(stroke, box) {
|
|
|
3608
3923
|
...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
|
|
3609
3924
|
...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
|
|
3610
3925
|
...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
|
|
3611
|
-
...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
|
|
3926
|
+
...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
|
|
3927
|
+
...stroke.vertexWidths !== void 0 ? { vertexWidths: stroke.vertexWidths } : {},
|
|
3928
|
+
...stroke.nib !== void 0 ? { nib: stroke.nib } : {}
|
|
3612
3929
|
};
|
|
3613
3930
|
}
|
|
3614
3931
|
function fillInBoxFrame(fill, box) {
|
|
@@ -3651,9 +3968,12 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
|
|
|
3651
3968
|
...n.hidden ? { hidden: true } : {}
|
|
3652
3969
|
};
|
|
3653
3970
|
drafts.push(draft);
|
|
3971
|
+
const at = drafts.length - 1;
|
|
3654
3972
|
const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
|
|
3655
3973
|
if (!acc) {
|
|
3656
|
-
drafts.
|
|
3974
|
+
const kept = drafts.length > at + 1 || (options.keepEmpty?.(n) ?? false);
|
|
3975
|
+
if (!kept) drafts.splice(at, 1);
|
|
3976
|
+
else if (n.clip) draft.pose = boxOfPath(n.clip);
|
|
3657
3977
|
return null;
|
|
3658
3978
|
}
|
|
3659
3979
|
draft.pose = n.clip ? boxOfPath(n.clip) : acc;
|
|
@@ -3737,7 +4057,7 @@ async function unpackSvgFiles(files, ctx) {
|
|
|
3737
4057
|
for (const file of files) {
|
|
3738
4058
|
try {
|
|
3739
4059
|
const text = await readFileText(file);
|
|
3740
|
-
const parsed = parseSvg(text);
|
|
4060
|
+
const parsed = parseSvg(text, { keepHidden: true });
|
|
3741
4061
|
for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
|
|
3742
4062
|
let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
|
|
3743
4063
|
if (drafts.length === 0) {
|
|
@@ -3782,7 +4102,8 @@ async function unpackSvgFiles(files, ctx) {
|
|
|
3782
4102
|
layer,
|
|
3783
4103
|
pose: place(d.pose),
|
|
3784
4104
|
data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
|
|
3785
|
-
parent: d.parentId
|
|
4105
|
+
parent: d.parentId,
|
|
4106
|
+
...d.hidden ? { hidden: true } : {}
|
|
3786
4107
|
},
|
|
3787
4108
|
label: "Insert SVG"
|
|
3788
4109
|
})
|
|
@@ -3817,7 +4138,9 @@ function svgStrokeFromKit(stroke, box) {
|
|
|
3817
4138
|
...stroke.align !== void 0 ? { align: stroke.align } : {},
|
|
3818
4139
|
...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
|
|
3819
4140
|
...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
|
|
3820
|
-
...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
|
|
4141
|
+
...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
|
|
4142
|
+
...stroke.vertexWidths?.length ? { vertexWidths: stroke.vertexWidths } : {},
|
|
4143
|
+
...stroke.nib ? { nib: stroke.nib } : {}
|
|
3821
4144
|
};
|
|
3822
4145
|
}
|
|
3823
4146
|
function svgImageFromKit(image, pose) {
|
|
@@ -3851,22 +4174,12 @@ function runInPoseFrame(run, box) {
|
|
|
3851
4174
|
function svgLeafFromKit(data, pose) {
|
|
3852
4175
|
if (data.image) return svgImageFromKit(data.image, pose);
|
|
3853
4176
|
if (data.text != null) {
|
|
3854
|
-
const
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
text: data.text
|
|
3861
|
-
};
|
|
3862
|
-
if (data.style) node2.style = data.style;
|
|
3863
|
-
if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
|
|
3864
|
-
if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
|
|
3865
|
-
if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
|
|
3866
|
-
if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
|
|
3867
|
-
if (pose.rotation) node2.rotation = pose.rotation;
|
|
3868
|
-
if (pose.transform) node2.transform = pose.transform;
|
|
3869
|
-
return node2;
|
|
4177
|
+
const text = svgTextFromKit({ ...data, text: data.text }, pose);
|
|
4178
|
+
const boxPath = { kind: "rect", x: pose.x, y: pose.y, width: pose.width, height: pose.height };
|
|
4179
|
+
const clip = data.overflow === "clip" ? { clip: pathThroughPoseFrame(boxPath, pose) } : {};
|
|
4180
|
+
if (!data.background?.paint) return clip.clip ? { kind: "group", children: [text], ...clip } : text;
|
|
4181
|
+
const box = svgLeafFromKit({ path: boxPath, fill: data.background }, pose);
|
|
4182
|
+
return { kind: "group", children: [box, text], ...clip };
|
|
3870
4183
|
}
|
|
3871
4184
|
if (!data.path) return null;
|
|
3872
4185
|
const stroke = svgStrokeFromKit(data.stroke, pose);
|
|
@@ -3885,21 +4198,53 @@ function svgLeafFromKit(data, pose) {
|
|
|
3885
4198
|
} else if (pose.rotation) node.rotation = pose.rotation;
|
|
3886
4199
|
return node;
|
|
3887
4200
|
}
|
|
4201
|
+
function svgTextFromKit(data, pose) {
|
|
4202
|
+
const pad = Math.max(0, Math.min(data.padding ?? 0, pose.width / 2, pose.height / 2));
|
|
4203
|
+
const node = {
|
|
4204
|
+
kind: "text",
|
|
4205
|
+
x: pose.x + pad,
|
|
4206
|
+
y: pose.y + pad,
|
|
4207
|
+
width: pose.width - 2 * pad,
|
|
4208
|
+
height: pose.height - 2 * pad,
|
|
4209
|
+
text: data.text
|
|
4210
|
+
};
|
|
4211
|
+
if (data.style) node.style = data.style;
|
|
4212
|
+
if (data.runs) node.runs = data.runs.map((r) => runInPoseFrame(r, pose));
|
|
4213
|
+
if (data.verticalAlign) node.verticalAlign = data.verticalAlign;
|
|
4214
|
+
if (data.fill !== void 0) node.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
|
|
4215
|
+
if (data.stroke) node.stroke = strokeInPoseFrame(data.stroke, pose);
|
|
4216
|
+
if (pose.rotation) node.rotation = pose.rotation;
|
|
4217
|
+
if (pose.transform) node.transform = pose.transform;
|
|
4218
|
+
return node;
|
|
4219
|
+
}
|
|
3888
4220
|
function svgNodesFromKit(tree, options = {}) {
|
|
3889
4221
|
const { include, leaf, group } = options;
|
|
3890
4222
|
const emit = (id) => {
|
|
3891
|
-
if (include && !include(id)) return null;
|
|
3892
4223
|
const node = tree.get(id);
|
|
3893
|
-
|
|
4224
|
+
const out2 = node && (!include || include(id)) ? lower(id, node) : null;
|
|
4225
|
+
return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
|
|
4226
|
+
};
|
|
4227
|
+
const lower = (id, node) => {
|
|
3894
4228
|
if (node.kind === "container") {
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
const
|
|
3898
|
-
|
|
4229
|
+
const emitChildren = () => {
|
|
4230
|
+
const children = [];
|
|
4231
|
+
for (const c of tree.childrenOf(id)) {
|
|
4232
|
+
const child = emit(c);
|
|
4233
|
+
if (child) children.push(child);
|
|
4234
|
+
}
|
|
4235
|
+
return children;
|
|
4236
|
+
};
|
|
4237
|
+
const path = node.data?.path;
|
|
4238
|
+
const clip = path ? pathThroughPoseFrame(pathInPoseFrame(path, node.pose), node.pose) : void 0;
|
|
4239
|
+
const copies = node.copiesFromPose?.(node.pose, node) ?? [];
|
|
4240
|
+
const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
|
|
4241
|
+
let g = content();
|
|
4242
|
+
if (copies.length > 0) {
|
|
4243
|
+
g = {
|
|
4244
|
+
kind: "group",
|
|
4245
|
+
children: [g, ...copies.map((transform) => ({ kind: "group", transform, children: [content()] }))]
|
|
4246
|
+
};
|
|
3899
4247
|
}
|
|
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
4248
|
return group ? group(id, g) : g;
|
|
3904
4249
|
}
|
|
3905
4250
|
return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);
|
|
@@ -3912,6 +4257,6 @@ function svgNodesFromKit(tree, options = {}) {
|
|
|
3912
4257
|
return out;
|
|
3913
4258
|
}
|
|
3914
4259
|
|
|
3915
|
-
export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
|
|
4260
|
+
export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializePathD, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
|
|
3916
4261
|
//# sourceMappingURL=index.js.map
|
|
3917
4262
|
//# sourceMappingURL=index.js.map
|