@react-x11/components 0.14.0 → 0.16.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/dist/code-editor/node.d.ts +13 -0
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +23 -5
- package/dist/code-editor/node.js.map +1 -1
- package/dist/html/css/cascade.d.ts +209 -23
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +873 -175
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +25 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +307 -19
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/content.d.ts +11 -6
- package/dist/html/css/content.d.ts.map +1 -1
- package/dist/html/css/content.js +171 -12
- package/dist/html/css/content.js.map +1 -1
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +62 -9
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +62 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +214 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +42 -5
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +75 -27
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +15 -0
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +5 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +18 -9
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +7 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +38 -7
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +160 -50
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +29 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +159 -20
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +69 -19
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1648 -225
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +15 -3
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +23 -8
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/table.d.ts +27 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +907 -323
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +36 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +174 -37
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +4 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +769 -204
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.js +14 -1
- package/dist/html/resources.js.map +1 -1
- package/dist/html/svg.d.ts +50 -17
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +501 -83
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts.map +1 -1
- package/dist/html/widgets.js +57 -4
- package/dist/html/widgets.js.map +1 -1
- package/package.json +3 -3
- package/src/code-editor/node.ts +24 -5
- package/src/html/css/cascade.ts +987 -176
- package/src/html/css/color.ts +345 -18
- package/src/html/css/content.ts +173 -15
- package/src/html/css/parse.ts +60 -8
- package/src/html/css/shapes.ts +273 -0
- package/src/html/css/style.ts +123 -30
- package/src/html/css/ua.ts +15 -0
- package/src/html/css/values.ts +23 -9
- package/src/html/index.ts +7 -0
- package/src/html/layout/block.ts +169 -56
- package/src/html/layout/boxes.ts +198 -24
- package/src/html/layout/css-grid.ts +89 -22
- package/src/html/layout/flex.ts +1863 -219
- package/src/html/layout/inline.ts +27 -7
- package/src/html/layout/table.ts +989 -333
- package/src/html/node.ts +204 -37
- package/src/html/paint.ts +999 -252
- package/src/html/resources.ts +13 -1
- package/src/html/svg.ts +600 -78
- package/src/html/widgets.ts +61 -4
package/src/html/svg.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { parseDocument } from 'htmlparser2';
|
|
|
16
16
|
import { Element, Text } from 'domhandler';
|
|
17
17
|
import type { ChildNode, ParentNode } from 'domhandler';
|
|
18
18
|
import * as ntk from 'react-x11/ntk';
|
|
19
|
-
import { isSvgRoot } from './dom.js';
|
|
19
|
+
import { isSvgRoot, rawTextOf } from './dom.js';
|
|
20
|
+
import { Cascade } from './css/cascade.js';
|
|
21
|
+
import { parseMediaQuery, parseStylesheet } from './css/parse.js';
|
|
22
|
+
import type { Stylesheet } from './css/parse.js';
|
|
23
|
+
import { useHref } from './css/shapes.js';
|
|
24
|
+
import type { ShapeStyle, ShapeStyles } from './css/shapes.js';
|
|
25
|
+
import type { RootLook } from './css/style.js';
|
|
20
26
|
import { inkColor, isTransparent, parseColor } from './css/values.js';
|
|
21
27
|
|
|
22
28
|
export { isSvgRoot };
|
|
@@ -69,20 +75,6 @@ function absoluteLength(
|
|
|
69
75
|
return px >= 0 ? px : null;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
/**
|
|
73
|
-
* The `width`/`height` of an inline `<svg>` as CSS: they are presentation
|
|
74
|
-
* attributes for the properties of the same names (SVG 2, 5.1.1), so
|
|
75
|
-
* `height="50%"` is a percentage of the containing block like a style's.
|
|
76
|
-
* A bare number is pixels; null for what CSS would not parse.
|
|
77
|
-
*/
|
|
78
|
-
export function svgSizeHint(value: string): string | null {
|
|
79
|
-
const v = value.trim();
|
|
80
|
-
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v)) return v;
|
|
81
|
-
const m = LENGTH.exec(v);
|
|
82
|
-
if (!m) return null;
|
|
83
|
-
return m[2] ? v : `${parseFloat(m[1])}px`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
78
|
function viewBoxParts(el: Element): [number, number, number, number] | null {
|
|
87
79
|
const parts = (svgAttr(el, 'viewBox') ?? '')
|
|
88
80
|
.trim()
|
|
@@ -130,6 +122,36 @@ export function svgIntrinsics(el: Element, fontSize = 16): IntrinsicSize {
|
|
|
130
122
|
return { width, height, ratio };
|
|
131
123
|
}
|
|
132
124
|
|
|
125
|
+
/**
|
|
126
|
+
* An image's size, in device pixels, where nothing sets it: CSS Images 3's
|
|
127
|
+
* default sizing algorithm (5.3.3), against a default object size of
|
|
128
|
+
* `width` by `height` device pixels — a background's positioning area, a
|
|
129
|
+
* list marker's square. Its own size where it has one; the dimension it
|
|
130
|
+
* lacks from its ratio, or else from the default; and one with a ratio
|
|
131
|
+
* alone as large as fits in the default. An SVG may be any of these, and a
|
|
132
|
+
* raster image is the first. `scale` is device pixels per CSS pixel, which
|
|
133
|
+
* the image's own size is in.
|
|
134
|
+
*/
|
|
135
|
+
export function concreteSize(
|
|
136
|
+
size: IntrinsicSize,
|
|
137
|
+
width: number,
|
|
138
|
+
height: number,
|
|
139
|
+
scale: number,
|
|
140
|
+
): [number, number] {
|
|
141
|
+
const { ratio } = size;
|
|
142
|
+
const w = size.width === null ? null : size.width * scale;
|
|
143
|
+
const h = size.height === null ? null : size.height * scale;
|
|
144
|
+
if (w !== null && h !== null) return [w, h];
|
|
145
|
+
if (w !== null) return [w, ratio > 0 ? w / ratio : height];
|
|
146
|
+
if (h !== null) return [ratio > 0 ? h * ratio : width, h];
|
|
147
|
+
if (ratio > 0) {
|
|
148
|
+
return width / height > ratio
|
|
149
|
+
? [height * ratio, height]
|
|
150
|
+
: [width, width / ratio];
|
|
151
|
+
}
|
|
152
|
+
return [width, height];
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
/** The slice of ntk's `SvgView` this draws with. */
|
|
134
156
|
interface SvgViewLike {
|
|
135
157
|
naturalWidth: number;
|
|
@@ -164,6 +186,66 @@ interface ClipContext {
|
|
|
164
186
|
fillRect?(x: number, y: number, w: number, h: number): void;
|
|
165
187
|
}
|
|
166
188
|
|
|
189
|
+
/** A context as a drawing it is lent to sees it (`lend`). */
|
|
190
|
+
interface Lent {
|
|
191
|
+
readonly ctx: ClipContext;
|
|
192
|
+
/** The saves the drawing has made on it and not restored. */
|
|
193
|
+
open: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const LENT = new WeakMap<object, Lent>();
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The context a drawing is lent: the same context, keeping count of the
|
|
200
|
+
* saves the drawing makes on it, so that whatever it leaves saved can be
|
|
201
|
+
* restored after it, and making none of the restores it makes past them.
|
|
202
|
+
*
|
|
203
|
+
* `SvgView` saves the context as it starts to draw, and around an element
|
|
204
|
+
* with a `transform`, and restores in no `finally`: an element it throws
|
|
205
|
+
* on — a colour it cannot read, `fill="var(--c)"` — leaves those saves
|
|
206
|
+
* open. The restore after the drawing then took the last of them for its
|
|
207
|
+
* own save, and the clip to the drawing's box was left on the window's
|
|
208
|
+
* context: everything a later paint drew, in that frame and every frame
|
|
209
|
+
* after, was cut to a sixteen-pixel icon. nextjs.org's blog drew its
|
|
210
|
+
* heading, the icon beside it, and nothing more.
|
|
211
|
+
*/
|
|
212
|
+
function lend(ctx: ClipContext): Lent {
|
|
213
|
+
const known = LENT.get(ctx);
|
|
214
|
+
if (known) return known;
|
|
215
|
+
const bound = new WeakMap<object, unknown>();
|
|
216
|
+
const save = () => {
|
|
217
|
+
ctx.save();
|
|
218
|
+
lent.open += 1;
|
|
219
|
+
};
|
|
220
|
+
const restore = () => {
|
|
221
|
+
if (lent.open === 0) return;
|
|
222
|
+
lent.open -= 1;
|
|
223
|
+
ctx.restore();
|
|
224
|
+
};
|
|
225
|
+
const lent: Lent = {
|
|
226
|
+
open: 0,
|
|
227
|
+
ctx: new Proxy(ctx, {
|
|
228
|
+
get(target, key) {
|
|
229
|
+
if (key === 'save') return save;
|
|
230
|
+
if (key === 'restore') return restore;
|
|
231
|
+
// on the context itself: an accessor, a method and a private field
|
|
232
|
+
// all see the context they belong to
|
|
233
|
+
const value: unknown = Reflect.get(target, key);
|
|
234
|
+
if (typeof value !== 'function') return value;
|
|
235
|
+
let method = bound.get(value);
|
|
236
|
+
if (!method) {
|
|
237
|
+
method = value.bind(target);
|
|
238
|
+
bound.set(value, method);
|
|
239
|
+
}
|
|
240
|
+
return method;
|
|
241
|
+
},
|
|
242
|
+
set: (target, key, value) => Reflect.set(target, key, value),
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
LENT.set(ctx, lent);
|
|
246
|
+
return lent;
|
|
247
|
+
}
|
|
248
|
+
|
|
167
249
|
const ALIGN = /^x(Min|Mid|Max)Y(Min|Mid|Max)$/;
|
|
168
250
|
const AT: Record<string, number> = { Min: 0, Mid: 0.5, Max: 1 };
|
|
169
251
|
|
|
@@ -176,14 +258,17 @@ export class SvgDrawing {
|
|
|
176
258
|
readonly intrinsics: IntrinsicSize;
|
|
177
259
|
private readonly _root: Element;
|
|
178
260
|
private _view: SvgViewLike | null = null;
|
|
179
|
-
/** What the view was last handed the tree for: the root's child count
|
|
180
|
-
* and
|
|
181
|
-
*
|
|
182
|
-
*
|
|
261
|
+
/** What the view was last handed the tree for: the root's child count,
|
|
262
|
+
* and where the root and each element outside it a `<use>` drew ended —
|
|
263
|
+
* a streamed document grows an inline drawing after its first paint,
|
|
264
|
+
* and a sprite's symbol after the first icon drawn from it — and, for a
|
|
265
|
+
* drawing with percentages in it, the viewport they were resolved
|
|
266
|
+
* against. */
|
|
183
267
|
private _seen = -1;
|
|
184
|
-
private
|
|
268
|
+
private _seenEnds: End[] = [];
|
|
185
269
|
private _seenViewport = '';
|
|
186
|
-
/** The `fill` and `stroke` the view's tree was last given its root's
|
|
270
|
+
/** The `fill` and `stroke` the view's tree was last given its root's,
|
|
271
|
+
* and what the document's rules gave the elements in it. */
|
|
187
272
|
private _seenPaint = '';
|
|
188
273
|
/** Whether a length in the tree is a percentage of the viewport; null
|
|
189
274
|
* until the tree is first read. */
|
|
@@ -201,11 +286,31 @@ export class SvgDrawing {
|
|
|
201
286
|
/** The background an image's root gives its canvas; undefined until it
|
|
202
287
|
* is first read. */
|
|
203
288
|
private _canvas: string | null | undefined = undefined;
|
|
289
|
+
/** An SVG image's own style sheets, its `<style>` elements': read at its
|
|
290
|
+
* first draw, and none for most images. */
|
|
291
|
+
private _sheets: Stylesheet[] | null = null;
|
|
292
|
+
/** A cascade over them for each colour scheme and scale the image is
|
|
293
|
+
* drawn at, and what each gave it: by the viewport it was asked at where
|
|
294
|
+
* a sheet reads the viewport, and under one key where none does. */
|
|
295
|
+
private _cascades = new Map<
|
|
296
|
+
string,
|
|
297
|
+
{ cascade: Cascade; own: Map<string, ImagePaint | null> }
|
|
298
|
+
>();
|
|
299
|
+
|
|
300
|
+
/** The element an SVG image's URL names by its fragment, its own
|
|
301
|
+
* `:target`: a sprite sheet shows the icon `image.svg#icon` names. */
|
|
302
|
+
private readonly _target: Element | null;
|
|
204
303
|
|
|
205
|
-
constructor(
|
|
304
|
+
constructor(
|
|
305
|
+
root: Element,
|
|
306
|
+
intrinsics: IntrinsicSize,
|
|
307
|
+
standalone = false,
|
|
308
|
+
target: Element | null = null,
|
|
309
|
+
) {
|
|
206
310
|
this._root = root;
|
|
207
311
|
this.intrinsics = intrinsics;
|
|
208
312
|
this._standalone = standalone;
|
|
313
|
+
this._target = target;
|
|
209
314
|
}
|
|
210
315
|
|
|
211
316
|
/**
|
|
@@ -216,7 +321,8 @@ export class SvgDrawing {
|
|
|
216
321
|
* and `stroke` the root's own where the document's styles set them (SVG
|
|
217
322
|
* 2, 13.2): they are properties, which a style sheet's rule sets over
|
|
218
323
|
* the presentation attribute, and what is in the drawing inherits them
|
|
219
|
-
* from its root.
|
|
324
|
+
* from its root. `shapes` is what the rules give the elements inside it,
|
|
325
|
+
* the same way, where they give any.
|
|
220
326
|
*/
|
|
221
327
|
draw(
|
|
222
328
|
ctx: ClipContext,
|
|
@@ -228,6 +334,7 @@ export class SvgDrawing {
|
|
|
228
334
|
color?: string,
|
|
229
335
|
fill: string | null = null,
|
|
230
336
|
stroke: string | null = null,
|
|
337
|
+
shapes: ShapeStyles | null = null,
|
|
231
338
|
): void {
|
|
232
339
|
if (this._failed || !(w > 0 && h > 0)) return;
|
|
233
340
|
// the mock backend has no path API, and SvgView draws paths
|
|
@@ -257,14 +364,38 @@ export class SvgDrawing {
|
|
|
257
364
|
}
|
|
258
365
|
const box = viewBoxOf(root);
|
|
259
366
|
// the viewport in user units, which a percentage is of
|
|
367
|
+
let vw = w / scale;
|
|
368
|
+
let vh = h / scale;
|
|
369
|
+
// An image with no `viewBox` is laid out at its own size, along an
|
|
370
|
+
// axis it has one on, and stretched to the rectangle along it, as a
|
|
371
|
+
// raster image is: Blink gives it a `viewBox` of its own size that
|
|
372
|
+
// `preserveAspectRatio: none` fits. Drawn at its own size, an icon of
|
|
373
|
+
// `width="24" height="24"` shown 48 wide was a quarter of its box.
|
|
374
|
+
let kx = 1;
|
|
375
|
+
let ky = 1;
|
|
376
|
+
if (!box && this._standalone) {
|
|
377
|
+
const own = this.intrinsics;
|
|
378
|
+
if (own.width !== null && own.width > 0) {
|
|
379
|
+
kx = vw / own.width;
|
|
380
|
+
vw = own.width;
|
|
381
|
+
}
|
|
382
|
+
if (own.height !== null && own.height > 0) {
|
|
383
|
+
ky = vh / own.height;
|
|
384
|
+
vh = own.height;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
260
387
|
const paint =
|
|
261
388
|
(fill === null ? '' : `fill:${fill};`) +
|
|
262
389
|
(stroke === null ? '' : `stroke:${stroke};`);
|
|
263
390
|
const view = box
|
|
264
|
-
? this._viewFor(box[2], box[3], paint)
|
|
265
|
-
: this._viewFor(
|
|
391
|
+
? this._viewFor(box[2], box[3], paint, shapes)
|
|
392
|
+
: this._viewFor(vw, vh, paint, shapes);
|
|
266
393
|
if (!view) return;
|
|
267
394
|
ctx.save();
|
|
395
|
+
// the view draws on the context lent, which counts its saves
|
|
396
|
+
const lent = lend(ctx);
|
|
397
|
+
const outer = lent.open;
|
|
398
|
+
lent.open = 0;
|
|
268
399
|
try {
|
|
269
400
|
ctx.beginPath();
|
|
270
401
|
ctx.rect(x, y, w, h);
|
|
@@ -272,11 +403,11 @@ export class SvgDrawing {
|
|
|
272
403
|
const opts = color ? { color } : undefined;
|
|
273
404
|
if (!box) {
|
|
274
405
|
view.draw(
|
|
275
|
-
ctx,
|
|
406
|
+
lent.ctx,
|
|
276
407
|
x,
|
|
277
408
|
y,
|
|
278
|
-
view.naturalWidth * scale,
|
|
279
|
-
view.naturalHeight * scale,
|
|
409
|
+
view.naturalWidth * scale * kx,
|
|
410
|
+
view.naturalHeight * scale * ky,
|
|
280
411
|
opts,
|
|
281
412
|
);
|
|
282
413
|
} else {
|
|
@@ -285,7 +416,7 @@ export class SvgDrawing {
|
|
|
285
416
|
.split(/\s+/);
|
|
286
417
|
if (fit[0] === 'defer') fit.shift();
|
|
287
418
|
if (fit[0] === 'none') {
|
|
288
|
-
view.draw(ctx, x, y, w, h, opts);
|
|
419
|
+
view.draw(lent.ctx, x, y, w, h, opts);
|
|
289
420
|
} else {
|
|
290
421
|
const align = ALIGN.exec(fit[0] ?? '') ?? ['xMidYMid', 'Mid', 'Mid'];
|
|
291
422
|
const sx = w / box[2];
|
|
@@ -294,7 +425,7 @@ export class SvgDrawing {
|
|
|
294
425
|
const dw = box[2] * s;
|
|
295
426
|
const dh = box[3] * s;
|
|
296
427
|
view.draw(
|
|
297
|
-
ctx,
|
|
428
|
+
lent.ctx,
|
|
298
429
|
x + (w - dw) * AT[align[1]],
|
|
299
430
|
y + (h - dh) * AT[align[2]],
|
|
300
431
|
dw,
|
|
@@ -308,35 +439,144 @@ export class SvgDrawing {
|
|
|
308
439
|
// out of paint on every frame, where nothing could catch it
|
|
309
440
|
this._failed = true;
|
|
310
441
|
} finally {
|
|
442
|
+
// what the drawing saved and did not restore, before this one's own
|
|
443
|
+
// save — which the restore after it would otherwise have taken, and
|
|
444
|
+
// this one's clip been left on the window's context (`lend`)
|
|
445
|
+
for (; lent.open > 0; lent.open -= 1) ctx.restore();
|
|
446
|
+
lent.open = outer;
|
|
311
447
|
ctx.restore();
|
|
312
448
|
}
|
|
313
449
|
}
|
|
314
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Draw an SVG image — an `<img>`'s, a background's, a list marker's — as
|
|
453
|
+
* `draw` does, painted as its own style sheets say. An image is a
|
|
454
|
+
* document of its own: no rule of the page's reaches into it, and its
|
|
455
|
+
* `color` is not the page's. What its `<style>` elements say is given
|
|
456
|
+
* its elements as the page's rules are given a drawing inline, and its
|
|
457
|
+
* root its `color`, `fill` and `stroke`, which an inline root has from
|
|
458
|
+
* its box. `scheme` is the colour scheme of the element that embeds it:
|
|
459
|
+
* what `prefers-color-scheme` answers inside it, as Chrome has it.
|
|
460
|
+
*/
|
|
461
|
+
drawImage(
|
|
462
|
+
ctx: ClipContext,
|
|
463
|
+
x: number,
|
|
464
|
+
y: number,
|
|
465
|
+
w: number,
|
|
466
|
+
h: number,
|
|
467
|
+
scale: number,
|
|
468
|
+
scheme: 'light' | 'dark' = 'light',
|
|
469
|
+
): void {
|
|
470
|
+
if (this._failed || !(w > 0 && h > 0)) return;
|
|
471
|
+
const own = this._ownPaint(w, h, scale, scheme);
|
|
472
|
+
this.draw(
|
|
473
|
+
ctx,
|
|
474
|
+
x,
|
|
475
|
+
y,
|
|
476
|
+
w,
|
|
477
|
+
h,
|
|
478
|
+
scale,
|
|
479
|
+
own?.color,
|
|
480
|
+
own?.fill ?? null,
|
|
481
|
+
own?.stroke ?? null,
|
|
482
|
+
own?.shapes ?? null,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** What an image's own style sheets give it, drawn `w` by `h` device
|
|
487
|
+
* pixels, or null where it has none. */
|
|
488
|
+
private _ownPaint(
|
|
489
|
+
w: number,
|
|
490
|
+
h: number,
|
|
491
|
+
scale: number,
|
|
492
|
+
scheme: 'light' | 'dark',
|
|
493
|
+
): ImagePaint | null {
|
|
494
|
+
const sheets = (this._sheets ??= ownSheets(this._root));
|
|
495
|
+
if (!sheets.length) return null;
|
|
496
|
+
const made = `${scheme}|${scale}`;
|
|
497
|
+
let entry = this._cascades.get(made);
|
|
498
|
+
if (!entry) {
|
|
499
|
+
const cascade = new Cascade(
|
|
500
|
+
sheets,
|
|
501
|
+
imageLook(scheme, scale),
|
|
502
|
+
w,
|
|
503
|
+
h,
|
|
504
|
+
scale,
|
|
505
|
+
null,
|
|
506
|
+
null,
|
|
507
|
+
null,
|
|
508
|
+
null,
|
|
509
|
+
this._root,
|
|
510
|
+
this._target,
|
|
511
|
+
);
|
|
512
|
+
entry = { cascade, own: new Map() };
|
|
513
|
+
this._cascades.set(made, entry);
|
|
514
|
+
}
|
|
515
|
+
const { cascade, own } = entry;
|
|
516
|
+
// An image's viewport is the rectangle it is drawn in, which is what a
|
|
517
|
+
// `@media (min-width)` in it, or a `vw`, is of: the answer is kept by
|
|
518
|
+
// that size only where a sheet asks.
|
|
519
|
+
const sized =
|
|
520
|
+
cascade.breakpoints.length > 0 ||
|
|
521
|
+
cascade.readsViewportWidth ||
|
|
522
|
+
cascade.readsViewportHeight;
|
|
523
|
+
const key = sized ? `${w}x${h}` : '';
|
|
524
|
+
const kept = own.get(key);
|
|
525
|
+
if (kept !== undefined) return kept;
|
|
526
|
+
cascade.viewportWidth = w;
|
|
527
|
+
cascade.viewportHeight = h;
|
|
528
|
+
let paint: ImagePaint | null = null;
|
|
529
|
+
try {
|
|
530
|
+
const root = this._root;
|
|
531
|
+
const style = cascade.styleFor(root, cascade.initial, false);
|
|
532
|
+
const ink = (value: string | null): string | null =>
|
|
533
|
+
value === null || value === 'currentColor' || value === 'none'
|
|
534
|
+
? value
|
|
535
|
+
: inkColor(value, style.color);
|
|
536
|
+
paint = {
|
|
537
|
+
color: style.color,
|
|
538
|
+
fill: ink(style.fill),
|
|
539
|
+
stroke: ink(style.stroke),
|
|
540
|
+
shapes: cascade.shapeStyles(root, style),
|
|
541
|
+
};
|
|
542
|
+
} catch {
|
|
543
|
+
// a sheet this cannot read leaves the image as its attributes draw it
|
|
544
|
+
}
|
|
545
|
+
// a background drawn at many sizes keeps a few of them
|
|
546
|
+
if (own.size >= 8) own.clear();
|
|
547
|
+
own.set(key, paint);
|
|
548
|
+
return paint;
|
|
549
|
+
}
|
|
550
|
+
|
|
315
551
|
private _viewFor(
|
|
316
552
|
width: number,
|
|
317
553
|
height: number,
|
|
318
554
|
/** The root's `fill` and `stroke` as declarations, or none. */
|
|
319
555
|
paint = '',
|
|
556
|
+
shapes: ShapeStyles | null = null,
|
|
320
557
|
): SvgViewLike | null {
|
|
321
558
|
const root = this._root;
|
|
322
559
|
const count = root.children.length;
|
|
323
|
-
|
|
324
|
-
// more of the
|
|
560
|
+
// more of the document has arrived since a `<use>` found nothing, or
|
|
561
|
+
// more of what the tree was read from: the drawing, a group in it, or
|
|
562
|
+
// a symbol outside it that a chunk ended halfway through
|
|
325
563
|
const arrived =
|
|
326
|
-
this._missingAt !== undefined &&
|
|
327
|
-
|
|
328
|
-
|
|
564
|
+
(this._missingAt !== undefined &&
|
|
565
|
+
lastNode(documentOf(root)) !== this._missingAt) ||
|
|
566
|
+
this._seenEnds.some(moved);
|
|
567
|
+
const grown = count !== this._seen || arrived;
|
|
329
568
|
if (grown || this._percent === null) {
|
|
330
569
|
this._percent = hasPercent(root);
|
|
331
570
|
this._uses = needsExpanding(root);
|
|
332
571
|
}
|
|
333
572
|
const sized = this._percent || this._uses;
|
|
334
573
|
const viewport = sized ? `${width}x${height}` : '';
|
|
574
|
+
const painted = shapes ? `${paint}|${shapes.key}` : paint;
|
|
335
575
|
if (
|
|
336
576
|
this._view &&
|
|
337
577
|
!grown &&
|
|
338
578
|
viewport === this._seenViewport &&
|
|
339
|
-
|
|
579
|
+
painted === this._seenPaint
|
|
340
580
|
) {
|
|
341
581
|
return this._view;
|
|
342
582
|
}
|
|
@@ -347,18 +587,22 @@ export class SvgDrawing {
|
|
|
347
587
|
}
|
|
348
588
|
try {
|
|
349
589
|
const view = this._view ?? new View(null);
|
|
350
|
-
const
|
|
590
|
+
const reached: Reached = { missing: false, outside: new Set() };
|
|
351
591
|
const tree =
|
|
352
|
-
sized || root.name.includes(':')
|
|
353
|
-
? copyTree(root, sized ? [width, height] : null,
|
|
592
|
+
sized || shapes || root.name.includes(':')
|
|
593
|
+
? copyTree(root, sized ? [width, height] : null, reached, shapes)
|
|
354
594
|
: root;
|
|
355
|
-
view.setSvgDom(paint ?
|
|
356
|
-
this._missingAt = missing
|
|
595
|
+
view.setSvgDom(paint ? withPaint(tree, paint) : tree);
|
|
596
|
+
this._missingAt = reached.missing
|
|
597
|
+
? lastNode(documentOf(root))
|
|
598
|
+
: undefined;
|
|
599
|
+
const ends = [endOf(root)];
|
|
600
|
+
for (const el of reached.outside) ends.push(endOf(el));
|
|
357
601
|
this._view = view;
|
|
358
602
|
this._seen = count;
|
|
359
|
-
this.
|
|
603
|
+
this._seenEnds = ends;
|
|
360
604
|
this._seenViewport = viewport;
|
|
361
|
-
this._seenPaint =
|
|
605
|
+
this._seenPaint = painted;
|
|
362
606
|
return view;
|
|
363
607
|
} catch {
|
|
364
608
|
this._failed = true;
|
|
@@ -373,7 +617,7 @@ export class SvgDrawing {
|
|
|
373
617
|
* its `style` — where `SvgView` reads a root's paint, over its presentation
|
|
374
618
|
* attributes, for what is under it to inherit.
|
|
375
619
|
*/
|
|
376
|
-
function
|
|
620
|
+
function withPaint(root: Element, paint: string): Element {
|
|
377
621
|
const style = root.attribs.style;
|
|
378
622
|
return new Element(
|
|
379
623
|
root.name,
|
|
@@ -403,6 +647,75 @@ function canvasBackground(root: Element): string | null {
|
|
|
403
647
|
return found;
|
|
404
648
|
}
|
|
405
649
|
|
|
650
|
+
/** What an SVG image's own style sheets give it (`SvgDrawing.drawImage`):
|
|
651
|
+
* its root's `color`, `fill` and `stroke`, and what they give the
|
|
652
|
+
* elements in it. */
|
|
653
|
+
interface ImagePaint {
|
|
654
|
+
color: string;
|
|
655
|
+
fill: string | null;
|
|
656
|
+
stroke: string | null;
|
|
657
|
+
shapes: ShapeStyles | null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The style sheets an SVG image's `<style>` elements hold, in order. One
|
|
662
|
+
* that names a type other than CSS's holds none (SVG 2, 6.3), and one with
|
|
663
|
+
* a `media` query holds its rules under it. Its text is its text and CDATA
|
|
664
|
+
* sections, as an XML parse leaves them, and not its comments.
|
|
665
|
+
*/
|
|
666
|
+
function ownSheets(root: Element): Stylesheet[] {
|
|
667
|
+
const sheets: Stylesheet[] = [];
|
|
668
|
+
const layers = new Map<string, number>();
|
|
669
|
+
let order = 0;
|
|
670
|
+
const stack: Element[] = [root];
|
|
671
|
+
for (let el = stack.pop(); el; el = stack.pop()) {
|
|
672
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
673
|
+
const child = el.children[i];
|
|
674
|
+
// an XML parse gives a `<style>` a type of its own, as HTML's does
|
|
675
|
+
if (child.type === 'tag' || child.type === 'style') {
|
|
676
|
+
stack.push(child as Element);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (el === root || localName(el.name) !== 'style') continue;
|
|
680
|
+
const type = (el.attribs.type ?? '').trim().toLowerCase();
|
|
681
|
+
if (type && type !== 'text/css') continue;
|
|
682
|
+
const media = el.attribs.media?.trim();
|
|
683
|
+
const sheet = parseStylesheet(
|
|
684
|
+
rawTextOf(el),
|
|
685
|
+
order,
|
|
686
|
+
layers,
|
|
687
|
+
null,
|
|
688
|
+
media ? [parseMediaQuery(media)] : null,
|
|
689
|
+
);
|
|
690
|
+
order += sheet.rules.length + 1;
|
|
691
|
+
sheets.push(sheet);
|
|
692
|
+
}
|
|
693
|
+
return sheets;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* The look an SVG image's own document starts from: CSS's initial values,
|
|
698
|
+
* black at `medium`, since nothing of the page's is inherited into an
|
|
699
|
+
* image — only the colour scheme, which its `prefers-color-scheme` answers.
|
|
700
|
+
*/
|
|
701
|
+
function imageLook(scheme: 'light' | 'dark', scale: number): RootLook {
|
|
702
|
+
return {
|
|
703
|
+
color: '#000000',
|
|
704
|
+
fontFamily: 'sans-serif',
|
|
705
|
+
fontSize: 16 * scale,
|
|
706
|
+
monoFamily: 'monospace',
|
|
707
|
+
linkColor: '#0000ee',
|
|
708
|
+
borderColor: '#000000',
|
|
709
|
+
mutedColor: '#808080',
|
|
710
|
+
background: 'transparent',
|
|
711
|
+
colorScheme: scheme,
|
|
712
|
+
surface: '#ffffff',
|
|
713
|
+
controlPadY: 0,
|
|
714
|
+
controlBorder: 0,
|
|
715
|
+
controlRadius: 0,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
406
719
|
/** Drawings of inline `<svg>` elements, per element: the element is the
|
|
407
720
|
* document's, and outlives the box trees built over it. */
|
|
408
721
|
const INLINE = new WeakMap<Element, SvgDrawing>();
|
|
@@ -467,13 +780,6 @@ function hasPercent(root: Element): boolean {
|
|
|
467
780
|
return walk(root);
|
|
468
781
|
}
|
|
469
782
|
|
|
470
|
-
/** The element a `<use>` refers to by a fragment, its id: one in another
|
|
471
|
-
* document is none here, where nothing is fetched for a drawing. */
|
|
472
|
-
function useTarget(el: Element): string | null {
|
|
473
|
-
const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
|
|
474
|
-
return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
783
|
/** The elements under a root by their ids, the first of each. */
|
|
478
784
|
function idsUnder(root: Element): Map<string, Element> {
|
|
479
785
|
const ids = new Map<string, Element>();
|
|
@@ -493,10 +799,10 @@ function idsUnder(root: Element): Map<string, Element> {
|
|
|
493
799
|
/**
|
|
494
800
|
* Whether a `<use>` under the root is one `SvgView` cannot draw as it
|
|
495
801
|
* stands: it looks a reference up among the root's own elements, and draws
|
|
496
|
-
* a `<symbol>` as its children where they are
|
|
497
|
-
* outside the root — an icon sprite's, a hidden
|
|
498
|
-
* top of the page — and one to a symbol with a
|
|
499
|
-
* drawn from a copy (`copyTree`).
|
|
802
|
+
* a `<symbol>` as its children where they are, with the `<use>`'s paint.
|
|
803
|
+
* So one to an element outside the root — an icon sprite's, a hidden
|
|
804
|
+
* `<svg>` of symbols at the top of the page — and one to a symbol with a
|
|
805
|
+
* `viewBox` or a paint of its own are drawn from a copy (`copyTree`).
|
|
500
806
|
*/
|
|
501
807
|
function needsExpanding(root: Element): boolean {
|
|
502
808
|
let ids: Map<string, Element> | null = null;
|
|
@@ -505,12 +811,15 @@ function needsExpanding(root: Element): boolean {
|
|
|
505
811
|
if (child.type !== 'tag') continue;
|
|
506
812
|
const tag = child as Element;
|
|
507
813
|
if (localName(tag.name) === 'use') {
|
|
508
|
-
const id =
|
|
814
|
+
const id = useHref(tag);
|
|
509
815
|
if (id !== null) {
|
|
510
816
|
ids ??= idsUnder(root);
|
|
511
817
|
const target = ids.get(id);
|
|
512
818
|
if (!target) return true;
|
|
513
|
-
if (
|
|
819
|
+
if (
|
|
820
|
+
localName(target.name) === 'symbol' &&
|
|
821
|
+
(viewBoxOf(target) || symbolOwn(target))
|
|
822
|
+
) {
|
|
514
823
|
return true;
|
|
515
824
|
}
|
|
516
825
|
}
|
|
@@ -536,6 +845,29 @@ function lastNode(top: ParentNode): ChildNode | null {
|
|
|
536
845
|
return last;
|
|
537
846
|
}
|
|
538
847
|
|
|
848
|
+
/** Where a part of a document ends: its last node, and the length of that
|
|
849
|
+
* node's text where it is text, since a chunk's text goes on the end of
|
|
850
|
+
* the text before it (domhandler's `ontext`) and leaves the node as it
|
|
851
|
+
* was. What a document that is still arriving appends to the part moves
|
|
852
|
+
* it, and what it appends after the part does not. */
|
|
853
|
+
interface End {
|
|
854
|
+
top: ParentNode;
|
|
855
|
+
last: ChildNode | null;
|
|
856
|
+
text: number;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function endOf(top: ParentNode): End {
|
|
860
|
+
const last = lastNode(top);
|
|
861
|
+
return { top, last, text: last?.type === 'text' ? last.data.length : -1 };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** Whether more of a part has arrived since its `End` was taken. */
|
|
865
|
+
function moved(end: End): boolean {
|
|
866
|
+
const last = lastNode(end.top);
|
|
867
|
+
if (last !== end.last) return true;
|
|
868
|
+
return last?.type === 'text' && last.data.length !== end.text;
|
|
869
|
+
}
|
|
870
|
+
|
|
539
871
|
/** The first element of a document with an id, in its order. */
|
|
540
872
|
function elementById(top: ParentNode, id: string): Element | null {
|
|
541
873
|
const stack: ChildNode[] = [...top.children].reverse();
|
|
@@ -598,6 +930,51 @@ const USE_PLACEMENT = new Set([
|
|
|
598
930
|
'height',
|
|
599
931
|
]);
|
|
600
932
|
|
|
933
|
+
/** A `<symbol>`'s attributes that paint what is in it: its presentation
|
|
934
|
+
* attributes and its `style` (SVG 2, 6.3). Not where it goes, the viewport
|
|
935
|
+
* it makes or a name, which a symbol's copy is no group to have. */
|
|
936
|
+
const SYMBOL_PAINTS = new Set([
|
|
937
|
+
'style',
|
|
938
|
+
'fill',
|
|
939
|
+
'fill-opacity',
|
|
940
|
+
'fill-rule',
|
|
941
|
+
'stroke',
|
|
942
|
+
'stroke-width',
|
|
943
|
+
'stroke-linecap',
|
|
944
|
+
'stroke-linejoin',
|
|
945
|
+
'stroke-miterlimit',
|
|
946
|
+
'stroke-dasharray',
|
|
947
|
+
'stroke-dashoffset',
|
|
948
|
+
'stroke-opacity',
|
|
949
|
+
'color',
|
|
950
|
+
'opacity',
|
|
951
|
+
'visibility',
|
|
952
|
+
'display',
|
|
953
|
+
'font-family',
|
|
954
|
+
'font-size',
|
|
955
|
+
'font-style',
|
|
956
|
+
'font-weight',
|
|
957
|
+
'text-anchor',
|
|
958
|
+
]);
|
|
959
|
+
|
|
960
|
+
/** A `<symbol>`'s own attributes that paint what is in it, or null where
|
|
961
|
+
* it has none: Chrome draws a sprite's `<symbol fill="…">` in that fill,
|
|
962
|
+
* where `SvgView` draws a symbol's children alone. */
|
|
963
|
+
function symbolOwn(symbol: Element): Record<string, string> | null {
|
|
964
|
+
let kept: Record<string, string> | null = null;
|
|
965
|
+
for (const name in symbol.attribs) {
|
|
966
|
+
if (SYMBOL_PAINTS.has(name)) (kept ??= {})[name] = symbol.attribs[name];
|
|
967
|
+
}
|
|
968
|
+
return kept;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** What a copy of a drawing reached outside its root: whether a `<use>`
|
|
972
|
+
* in it found nothing, and the elements one was drawn from. */
|
|
973
|
+
interface Reached {
|
|
974
|
+
missing: boolean;
|
|
975
|
+
outside: Set<Element>;
|
|
976
|
+
}
|
|
977
|
+
|
|
601
978
|
/**
|
|
602
979
|
* The tree `SvgView` reads, where the document's own will not do: with
|
|
603
980
|
* local names — `SvgView` knows `rect`, not `svg:rect`, and a prefix is
|
|
@@ -607,13 +984,26 @@ const USE_PLACEMENT = new Set([
|
|
|
607
984
|
* group of what it refers to (SVG 2, 5.5): the element, from wherever in
|
|
608
985
|
* the document it is, or a symbol's children, its `viewBox` fitted to the
|
|
609
986
|
* viewport the `<use>` gives it — its `width` and `height`, and all of the
|
|
610
|
-
* drawing's where it has none. `
|
|
611
|
-
* element the document does not have
|
|
987
|
+
* drawing's where it has none. `reached` is told where one refers to an
|
|
988
|
+
* element the document does not have, and of each element outside the root
|
|
989
|
+
* that one was drawn from.
|
|
990
|
+
*
|
|
991
|
+
* And with what the document's rules give each element (`shapes`), where
|
|
992
|
+
* `SvgView` reads an element's own: at the end of its `style`, over its
|
|
993
|
+
* presentation attributes and over what that attribute says, which the
|
|
994
|
+
* cascade that made them has weighed already. An element a rule gives
|
|
995
|
+
* `display: none` is left out, with what is in it, and a shape one hides
|
|
996
|
+
* — `visibility` is inherited, and a shape in a hidden group may be shown
|
|
997
|
+
* again — is left out alone. What a `<use>` draws has what the rules give
|
|
998
|
+
* its copy (`ShapeStyles.used`), which may not be what they give the
|
|
999
|
+
* element where it stands, so where they give anything, every `<use>` is
|
|
1000
|
+
* drawn from a copy here.
|
|
612
1001
|
*/
|
|
613
1002
|
function copyTree(
|
|
614
1003
|
root: Element,
|
|
615
1004
|
viewport: [number, number] | null,
|
|
616
|
-
|
|
1005
|
+
reached?: Reached,
|
|
1006
|
+
shapes: ShapeStyles | null = null,
|
|
617
1007
|
): Element {
|
|
618
1008
|
const colon = root.name.indexOf(':');
|
|
619
1009
|
const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
|
|
@@ -627,24 +1017,33 @@ function copyTree(
|
|
|
627
1017
|
use: Element,
|
|
628
1018
|
resolve: boolean,
|
|
629
1019
|
depth: number,
|
|
1020
|
+
hidden: boolean,
|
|
1021
|
+
/** What the rules give the `<use>`. */
|
|
1022
|
+
own: ShapeStyle | undefined,
|
|
630
1023
|
): Element | null => {
|
|
631
|
-
const id =
|
|
1024
|
+
const id = useHref(use);
|
|
632
1025
|
if (id === null || depth >= USE_DEPTH) return null;
|
|
633
1026
|
ids ??= idsUnder(root);
|
|
634
1027
|
const inside = ids.get(id);
|
|
635
1028
|
const target = inside ?? elementById(documentOf(root), id);
|
|
636
1029
|
if (!target) {
|
|
637
|
-
if (
|
|
1030
|
+
if (reached) reached.missing = true;
|
|
638
1031
|
return null;
|
|
639
1032
|
}
|
|
1033
|
+
if (!inside) reached?.outside.add(target);
|
|
640
1034
|
const symbol = localName(target.name) === 'symbol';
|
|
641
1035
|
const box = symbol ? viewBoxOf(target) : null;
|
|
642
|
-
|
|
643
|
-
|
|
1036
|
+
const kept = symbol ? symbolOwn(target) : null;
|
|
1037
|
+
// one of the root's own that `SvgView` draws as it is: with no
|
|
1038
|
+
// viewport to fit, no paint of a symbol's own, and no rule's styles
|
|
1039
|
+
if (inside && !box && !kept && !shapes) return null;
|
|
1040
|
+
// what the rules give the copy, element by element
|
|
1041
|
+
const styles = shapes?.used?.get(use) ?? null;
|
|
644
1042
|
const attribs: Record<string, string> = {};
|
|
645
1043
|
for (const name in use.attribs) {
|
|
646
1044
|
if (!USE_PLACEMENT.has(name)) attribs[name] = use.attribs[name];
|
|
647
1045
|
}
|
|
1046
|
+
if (own) restyle(attribs, own);
|
|
648
1047
|
const x = parseFloat(use.attribs.x ?? '') || 0;
|
|
649
1048
|
const y = parseFloat(use.attribs.y ?? '') || 0;
|
|
650
1049
|
let placed = '';
|
|
@@ -672,21 +1071,76 @@ function copyTree(
|
|
|
672
1071
|
const within = resolve && !box;
|
|
673
1072
|
const children: ChildNode[] = [];
|
|
674
1073
|
if (symbol) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1074
|
+
// The symbol is the top of the copy, and paints what is in it: its
|
|
1075
|
+
// attributes, and what the rules give it there. `SvgView` draws a
|
|
1076
|
+
// symbol's children alone, so they go in a group that is it. Where
|
|
1077
|
+
// it stands a symbol is drawn by nobody, so `display: none` takes
|
|
1078
|
+
// nothing from it there (`UNDRAWN`); at the top of a copy it takes
|
|
1079
|
+
// the copy, as Chrome has it.
|
|
1080
|
+
const mine = styles?.get(target);
|
|
1081
|
+
const inner = hiddenIn(mine, hidden);
|
|
1082
|
+
if (mine?.display !== 'none') {
|
|
1083
|
+
copyInto(children, target, within, depth + 1, inner, styles);
|
|
1084
|
+
}
|
|
1085
|
+
if (kept || mine) {
|
|
1086
|
+
const paints = { ...kept };
|
|
1087
|
+
if (mine) restyle(paints, mine);
|
|
1088
|
+
const group = new Element('g', paints, children.splice(0));
|
|
1089
|
+
children.push(group);
|
|
679
1090
|
}
|
|
680
|
-
} else
|
|
1091
|
+
} else if (shown(target, hidden, styles)) {
|
|
1092
|
+
children.push(copy(target, within, depth + 1, hidden, styles));
|
|
1093
|
+
}
|
|
681
1094
|
return new Element('g', attribs, children);
|
|
682
1095
|
};
|
|
683
|
-
|
|
1096
|
+
/** Whether an element is drawn at all, in a group that is hidden or
|
|
1097
|
+
* not: one no rule gives `display: none`, and no shape a rule hides. */
|
|
1098
|
+
const shown = (
|
|
1099
|
+
el: Element,
|
|
1100
|
+
hidden: boolean,
|
|
1101
|
+
styles: ReadonlyMap<Element, ShapeStyle> | null,
|
|
1102
|
+
): boolean => {
|
|
1103
|
+
if (!styles) return true;
|
|
1104
|
+
const own = styles.get(el);
|
|
1105
|
+
const name = localName(el.name);
|
|
1106
|
+
if (own?.display === 'none' && !UNDRAWN.has(name)) return false;
|
|
1107
|
+
return !(hiddenIn(own, hidden) && HIDEABLE.has(name));
|
|
1108
|
+
};
|
|
1109
|
+
const copyInto = (
|
|
1110
|
+
children: ChildNode[],
|
|
1111
|
+
el: Element,
|
|
1112
|
+
resolve: boolean,
|
|
1113
|
+
depth: number,
|
|
1114
|
+
hidden: boolean,
|
|
1115
|
+
styles: ReadonlyMap<Element, ShapeStyle> | null,
|
|
1116
|
+
): void => {
|
|
1117
|
+
for (const child of el.children) {
|
|
1118
|
+
if (child.type === 'tag') {
|
|
1119
|
+
if (shown(child as Element, hidden, styles)) {
|
|
1120
|
+
children.push(copy(child as Element, resolve, depth, hidden, styles));
|
|
1121
|
+
}
|
|
1122
|
+
} else if (child.type === 'text') children.push(new Text(child.data));
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
const copy = (
|
|
1126
|
+
el: Element,
|
|
1127
|
+
resolve: boolean,
|
|
1128
|
+
depth = 0,
|
|
1129
|
+
/** Whether the element inherits a `visibility` of `hidden`. */
|
|
1130
|
+
hidden = false,
|
|
1131
|
+
/** What the rules give the elements of the tree it is in: the
|
|
1132
|
+
* drawing's, or a `<use>`'s copy's. */
|
|
1133
|
+
styles: ReadonlyMap<Element, ShapeStyle> | null = shapes?.of ?? null,
|
|
1134
|
+
): Element => {
|
|
1135
|
+
const own = styles?.get(el);
|
|
1136
|
+
hidden = hiddenIn(own, hidden);
|
|
684
1137
|
if (localName(el.name) === 'use') {
|
|
685
|
-
const group = expand(el, resolve, depth);
|
|
1138
|
+
const group = expand(el, resolve, depth, hidden, own);
|
|
686
1139
|
if (group) return group;
|
|
687
1140
|
}
|
|
688
1141
|
const here = resolve && !OWN_UNITS.has(localName(el.name));
|
|
689
1142
|
const attribs = { ...el.attribs };
|
|
1143
|
+
if (own) restyle(attribs, own);
|
|
690
1144
|
if (here && viewport && el !== root) {
|
|
691
1145
|
for (const name in attribs) {
|
|
692
1146
|
const axis = PERCENT_OF[name];
|
|
@@ -700,22 +1154,88 @@ function copyTree(
|
|
|
700
1154
|
}
|
|
701
1155
|
}
|
|
702
1156
|
const children: ChildNode[] = [];
|
|
703
|
-
|
|
704
|
-
if (child.type === 'tag') {
|
|
705
|
-
children.push(copy(child as Element, here, depth));
|
|
706
|
-
} else if (child.type === 'text') children.push(new Text(child.data));
|
|
707
|
-
}
|
|
1157
|
+
copyInto(children, el, here, depth, hidden, styles);
|
|
708
1158
|
return new Element(strip(el.name), attribs, children);
|
|
709
1159
|
};
|
|
710
1160
|
return copy(root, true);
|
|
711
1161
|
}
|
|
712
1162
|
|
|
1163
|
+
/** What `display: none` takes nothing from: `SvgView` draws none of them
|
|
1164
|
+
* where they stand, and what refers to one still finds it (SVG 2, 5.3). */
|
|
1165
|
+
const UNDRAWN = new Set([
|
|
1166
|
+
'defs',
|
|
1167
|
+
'symbol',
|
|
1168
|
+
'lineargradient',
|
|
1169
|
+
'radialgradient',
|
|
1170
|
+
'stop',
|
|
1171
|
+
'clippath',
|
|
1172
|
+
'mask',
|
|
1173
|
+
'pattern',
|
|
1174
|
+
'marker',
|
|
1175
|
+
'style',
|
|
1176
|
+
]);
|
|
1177
|
+
|
|
1178
|
+
/** What `visibility: hidden` leaves undrawn: the elements with something
|
|
1179
|
+
* of their own to draw, and a `<use>`, since what it draws inherits the
|
|
1180
|
+
* property from it. */
|
|
1181
|
+
const HIDEABLE = new Set([
|
|
1182
|
+
'path',
|
|
1183
|
+
'rect',
|
|
1184
|
+
'circle',
|
|
1185
|
+
'ellipse',
|
|
1186
|
+
'line',
|
|
1187
|
+
'polyline',
|
|
1188
|
+
'polygon',
|
|
1189
|
+
'text',
|
|
1190
|
+
'image',
|
|
1191
|
+
'use',
|
|
1192
|
+
]);
|
|
1193
|
+
|
|
1194
|
+
/** Whether an element is hidden, given whether what it is in is. */
|
|
1195
|
+
function hiddenIn(own: ShapeStyle | undefined, hidden: boolean): boolean {
|
|
1196
|
+
const visibility = own?.visibility;
|
|
1197
|
+
return visibility === undefined || visibility === 'inherit'
|
|
1198
|
+
? hidden
|
|
1199
|
+
: visibility === 'hidden';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* An element's attributes with what the rules give it: each property at
|
|
1204
|
+
* the end of its `style`, where `SvgView` reads it over the attribute of
|
|
1205
|
+
* its name — but `opacity`, which it reads from the attribute alone. One
|
|
1206
|
+
* the rules put back to what the element inherits is taken out of both.
|
|
1207
|
+
*/
|
|
1208
|
+
function restyle(attribs: Record<string, string>, own: ShapeStyle): void {
|
|
1209
|
+
let style = attribs.style ?? '';
|
|
1210
|
+
let added = '';
|
|
1211
|
+
for (const prop in own) {
|
|
1212
|
+
const value = own[prop];
|
|
1213
|
+
if (prop === 'display' || prop === 'visibility') continue;
|
|
1214
|
+
if (value === 'inherit') {
|
|
1215
|
+
delete attribs[prop];
|
|
1216
|
+
if (style) {
|
|
1217
|
+
style = style
|
|
1218
|
+
.split(';')
|
|
1219
|
+
.filter((d) => d.slice(0, d.indexOf(':')).trim() !== prop)
|
|
1220
|
+
.join(';');
|
|
1221
|
+
}
|
|
1222
|
+
} else if (prop === 'opacity') attribs.opacity = value;
|
|
1223
|
+
else added += `${prop}:${value};`;
|
|
1224
|
+
}
|
|
1225
|
+
style = style && added ? `${style};${added}` : style || added;
|
|
1226
|
+
if (style) attribs.style = style;
|
|
1227
|
+
else delete attribs.style;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
713
1230
|
/**
|
|
714
1231
|
* An SVG image's bytes as a drawing, or null for bytes that are not SVG —
|
|
715
1232
|
* which is how a PNG or a JPEG goes on to the image decoder. Nothing names
|
|
716
1233
|
* the type, so it is sniffed: markup, with an `<svg>` root.
|
|
717
1234
|
*/
|
|
718
|
-
export function svgFromBytes(
|
|
1235
|
+
export function svgFromBytes(
|
|
1236
|
+
bytes: Uint8Array,
|
|
1237
|
+
fragment = '',
|
|
1238
|
+
): SvgDrawing | null {
|
|
719
1239
|
let i = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;
|
|
720
1240
|
while (
|
|
721
1241
|
bytes[i] === 0x20 ||
|
|
@@ -731,7 +1251,9 @@ export function svgFromBytes(bytes: Uint8Array): SvgDrawing | null {
|
|
|
731
1251
|
try {
|
|
732
1252
|
const doc = parseDocument(text, { xmlMode: true });
|
|
733
1253
|
const root = findRoot(doc.children);
|
|
734
|
-
|
|
1254
|
+
if (!root) return null;
|
|
1255
|
+
const target = fragment ? elementById(doc, fragment) : null;
|
|
1256
|
+
return new SvgDrawing(root, svgIntrinsics(root), true, target);
|
|
735
1257
|
} catch {
|
|
736
1258
|
return null;
|
|
737
1259
|
}
|