@react-x11/components 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/controls.d.ts +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +42 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +336 -16
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +44 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +194 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +547 -144
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +14 -2
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +104 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +410 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +72 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +40 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +144 -24
- 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 +585 -85
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +144 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +97 -31
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +4 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +319 -47
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +83 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1474 -217
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +39 -9
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +482 -37
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +366 -8
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/shapes.ts +239 -0
- package/src/html/css/style.ts +652 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +466 -37
- package/src/html/layout/boxes.ts +95 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +169 -32
- package/src/html/layout/flex.ts +686 -79
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +105 -35
- package/src/html/node.ts +354 -48
- package/src/html/paint.ts +1804 -315
- package/src/html/svg.ts +573 -36
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/svg.ts
CHANGED
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
// as `preserveAspectRatio` says, and clipped to it.
|
|
15
15
|
import { parseDocument } from 'htmlparser2';
|
|
16
16
|
import { Element, Text } from 'domhandler';
|
|
17
|
-
import type { ChildNode } from 'domhandler';
|
|
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 type { ShapeStyle, ShapeStyles } from './css/shapes.js';
|
|
24
|
+
import type { RootLook } from './css/style.js';
|
|
20
25
|
import { inkColor, isTransparent, parseColor } from './css/values.js';
|
|
21
26
|
|
|
22
27
|
export { isSvgRoot };
|
|
@@ -69,20 +74,6 @@ function absoluteLength(
|
|
|
69
74
|
return px >= 0 ? px : null;
|
|
70
75
|
}
|
|
71
76
|
|
|
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
77
|
function viewBoxParts(el: Element): [number, number, number, number] | null {
|
|
87
78
|
const parts = (svgAttr(el, 'viewBox') ?? '')
|
|
88
79
|
.trim()
|
|
@@ -183,15 +174,35 @@ export class SvgDrawing {
|
|
|
183
174
|
private _seen = -1;
|
|
184
175
|
private _seenLast: ChildNode | null = null;
|
|
185
176
|
private _seenViewport = '';
|
|
177
|
+
/** The `fill` and `stroke` the view's tree was last given its root's,
|
|
178
|
+
* and what the document's rules gave the elements in it. */
|
|
179
|
+
private _seenPaint = '';
|
|
186
180
|
/** Whether a length in the tree is a percentage of the viewport; null
|
|
187
181
|
* until the tree is first read. */
|
|
188
182
|
private _percent: boolean | null = null;
|
|
183
|
+
/** Whether a `<use>` in the tree is one `SvgView` cannot draw as it
|
|
184
|
+
* stands (`needsExpanding`). */
|
|
185
|
+
private _uses = false;
|
|
186
|
+
/** The document's last node when a `<use>` was last looked up and its
|
|
187
|
+
* element not found: a streamed document may bring it yet, and has
|
|
188
|
+
* when its last node is another. Undefined where none was missing. */
|
|
189
|
+
private _missingAt: ChildNode | null | undefined = undefined;
|
|
189
190
|
private _failed = false;
|
|
190
191
|
/** An SVG image's own document, rather than an element of this one. */
|
|
191
192
|
private readonly _standalone: boolean;
|
|
192
193
|
/** The background an image's root gives its canvas; undefined until it
|
|
193
194
|
* is first read. */
|
|
194
195
|
private _canvas: string | null | undefined = undefined;
|
|
196
|
+
/** An SVG image's own style sheets, its `<style>` elements': read at its
|
|
197
|
+
* first draw, and none for most images. */
|
|
198
|
+
private _sheets: Stylesheet[] | null = null;
|
|
199
|
+
/** A cascade over them for each colour scheme and scale the image is
|
|
200
|
+
* drawn at, and what each gave it: by the viewport it was asked at where
|
|
201
|
+
* a sheet reads the viewport, and under one key where none does. */
|
|
202
|
+
private _cascades = new Map<
|
|
203
|
+
string,
|
|
204
|
+
{ cascade: Cascade; own: Map<string, ImagePaint | null> }
|
|
205
|
+
>();
|
|
195
206
|
|
|
196
207
|
constructor(root: Element, intrinsics: IntrinsicSize, standalone = false) {
|
|
197
208
|
this._root = root;
|
|
@@ -203,7 +214,12 @@ export class SvgDrawing {
|
|
|
203
214
|
* Draw into a rectangle — the box's content box, in device pixels —
|
|
204
215
|
* clipped to it. The `viewBox`, where there is one, is fitted to it as
|
|
205
216
|
* `preserveAspectRatio` says; without one a user unit is a CSS pixel,
|
|
206
|
-
* which is `scale` device pixels. `color` is `currentColor
|
|
217
|
+
* which is `scale` device pixels. `color` is `currentColor`, and `fill`
|
|
218
|
+
* and `stroke` the root's own where the document's styles set them (SVG
|
|
219
|
+
* 2, 13.2): they are properties, which a style sheet's rule sets over
|
|
220
|
+
* the presentation attribute, and what is in the drawing inherits them
|
|
221
|
+
* from its root. `shapes` is what the rules give the elements inside it,
|
|
222
|
+
* the same way, where they give any.
|
|
207
223
|
*/
|
|
208
224
|
draw(
|
|
209
225
|
ctx: ClipContext,
|
|
@@ -213,6 +229,9 @@ export class SvgDrawing {
|
|
|
213
229
|
h: number,
|
|
214
230
|
scale: number,
|
|
215
231
|
color?: string,
|
|
232
|
+
fill: string | null = null,
|
|
233
|
+
stroke: string | null = null,
|
|
234
|
+
shapes: ShapeStyles | null = null,
|
|
216
235
|
): void {
|
|
217
236
|
if (this._failed || !(w > 0 && h > 0)) return;
|
|
218
237
|
// the mock backend has no path API, and SvgView draws paths
|
|
@@ -242,9 +261,12 @@ export class SvgDrawing {
|
|
|
242
261
|
}
|
|
243
262
|
const box = viewBoxOf(root);
|
|
244
263
|
// the viewport in user units, which a percentage is of
|
|
264
|
+
const paint =
|
|
265
|
+
(fill === null ? '' : `fill:${fill};`) +
|
|
266
|
+
(stroke === null ? '' : `stroke:${stroke};`);
|
|
245
267
|
const view = box
|
|
246
|
-
? this._viewFor(box[2], box[3])
|
|
247
|
-
: this._viewFor(w / scale, h / scale);
|
|
268
|
+
? this._viewFor(box[2], box[3], paint, shapes)
|
|
269
|
+
: this._viewFor(w / scale, h / scale, paint, shapes);
|
|
248
270
|
if (!view) return;
|
|
249
271
|
ctx.save();
|
|
250
272
|
try {
|
|
@@ -294,14 +316,133 @@ export class SvgDrawing {
|
|
|
294
316
|
}
|
|
295
317
|
}
|
|
296
318
|
|
|
297
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Draw an SVG image — an `<img>`'s, a background's, a list marker's — as
|
|
321
|
+
* `draw` does, painted as its own style sheets say. An image is a
|
|
322
|
+
* document of its own: no rule of the page's reaches into it, and its
|
|
323
|
+
* `color` is not the page's. What its `<style>` elements say is given
|
|
324
|
+
* its elements as the page's rules are given a drawing inline, and its
|
|
325
|
+
* root its `color`, `fill` and `stroke`, which an inline root has from
|
|
326
|
+
* its box. `scheme` is the colour scheme of the element that embeds it:
|
|
327
|
+
* what `prefers-color-scheme` answers inside it, as Chrome has it.
|
|
328
|
+
*/
|
|
329
|
+
drawImage(
|
|
330
|
+
ctx: ClipContext,
|
|
331
|
+
x: number,
|
|
332
|
+
y: number,
|
|
333
|
+
w: number,
|
|
334
|
+
h: number,
|
|
335
|
+
scale: number,
|
|
336
|
+
scheme: 'light' | 'dark' = 'light',
|
|
337
|
+
): void {
|
|
338
|
+
if (this._failed || !(w > 0 && h > 0)) return;
|
|
339
|
+
const own = this._ownPaint(w, h, scale, scheme);
|
|
340
|
+
this.draw(
|
|
341
|
+
ctx,
|
|
342
|
+
x,
|
|
343
|
+
y,
|
|
344
|
+
w,
|
|
345
|
+
h,
|
|
346
|
+
scale,
|
|
347
|
+
own?.color,
|
|
348
|
+
own?.fill ?? null,
|
|
349
|
+
own?.stroke ?? null,
|
|
350
|
+
own?.shapes ?? null,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** What an image's own style sheets give it, drawn `w` by `h` device
|
|
355
|
+
* pixels, or null where it has none. */
|
|
356
|
+
private _ownPaint(
|
|
357
|
+
w: number,
|
|
358
|
+
h: number,
|
|
359
|
+
scale: number,
|
|
360
|
+
scheme: 'light' | 'dark',
|
|
361
|
+
): ImagePaint | null {
|
|
362
|
+
const sheets = (this._sheets ??= ownSheets(this._root));
|
|
363
|
+
if (!sheets.length) return null;
|
|
364
|
+
const made = `${scheme}|${scale}`;
|
|
365
|
+
let entry = this._cascades.get(made);
|
|
366
|
+
if (!entry) {
|
|
367
|
+
const cascade = new Cascade(
|
|
368
|
+
sheets,
|
|
369
|
+
imageLook(scheme, scale),
|
|
370
|
+
w,
|
|
371
|
+
h,
|
|
372
|
+
scale,
|
|
373
|
+
null,
|
|
374
|
+
null,
|
|
375
|
+
null,
|
|
376
|
+
null,
|
|
377
|
+
this._root,
|
|
378
|
+
);
|
|
379
|
+
entry = { cascade, own: new Map() };
|
|
380
|
+
this._cascades.set(made, entry);
|
|
381
|
+
}
|
|
382
|
+
const { cascade, own } = entry;
|
|
383
|
+
// An image's viewport is the rectangle it is drawn in, which is what a
|
|
384
|
+
// `@media (min-width)` in it, or a `vw`, is of: the answer is kept by
|
|
385
|
+
// that size only where a sheet asks.
|
|
386
|
+
const sized =
|
|
387
|
+
cascade.breakpoints.length > 0 ||
|
|
388
|
+
cascade.readsViewportWidth ||
|
|
389
|
+
cascade.readsViewportHeight;
|
|
390
|
+
const key = sized ? `${w}x${h}` : '';
|
|
391
|
+
const kept = own.get(key);
|
|
392
|
+
if (kept !== undefined) return kept;
|
|
393
|
+
cascade.viewportWidth = w;
|
|
394
|
+
cascade.viewportHeight = h;
|
|
395
|
+
let paint: ImagePaint | null = null;
|
|
396
|
+
try {
|
|
397
|
+
const root = this._root;
|
|
398
|
+
const style = cascade.styleFor(root, cascade.initial, false);
|
|
399
|
+
const ink = (value: string | null): string | null =>
|
|
400
|
+
value === null || value === 'currentColor' || value === 'none'
|
|
401
|
+
? value
|
|
402
|
+
: inkColor(value, style.color);
|
|
403
|
+
paint = {
|
|
404
|
+
color: style.color,
|
|
405
|
+
fill: ink(style.fill),
|
|
406
|
+
stroke: ink(style.stroke),
|
|
407
|
+
shapes: cascade.shapeStyles(root, style),
|
|
408
|
+
};
|
|
409
|
+
} catch {
|
|
410
|
+
// a sheet this cannot read leaves the image as its attributes draw it
|
|
411
|
+
}
|
|
412
|
+
// a background drawn at many sizes keeps a few of them
|
|
413
|
+
if (own.size >= 8) own.clear();
|
|
414
|
+
own.set(key, paint);
|
|
415
|
+
return paint;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private _viewFor(
|
|
419
|
+
width: number,
|
|
420
|
+
height: number,
|
|
421
|
+
/** The root's `fill` and `stroke` as declarations, or none. */
|
|
422
|
+
paint = '',
|
|
423
|
+
shapes: ShapeStyles | null = null,
|
|
424
|
+
): SvgViewLike | null {
|
|
298
425
|
const root = this._root;
|
|
299
426
|
const count = root.children.length;
|
|
300
427
|
const last = root.lastChild;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
428
|
+
// more of the document has arrived since a `<use>` found nothing
|
|
429
|
+
const arrived =
|
|
430
|
+
this._missingAt !== undefined &&
|
|
431
|
+
lastNode(documentOf(root)) !== this._missingAt;
|
|
432
|
+
const grown = count !== this._seen || last !== this._seenLast || arrived;
|
|
433
|
+
if (grown || this._percent === null) {
|
|
434
|
+
this._percent = hasPercent(root);
|
|
435
|
+
this._uses = needsExpanding(root);
|
|
436
|
+
}
|
|
437
|
+
const sized = this._percent || this._uses;
|
|
438
|
+
const viewport = sized ? `${width}x${height}` : '';
|
|
439
|
+
const painted = shapes ? `${paint}|${shapes.key}` : paint;
|
|
440
|
+
if (
|
|
441
|
+
this._view &&
|
|
442
|
+
!grown &&
|
|
443
|
+
viewport === this._seenViewport &&
|
|
444
|
+
painted === this._seenPaint
|
|
445
|
+
) {
|
|
305
446
|
return this._view;
|
|
306
447
|
}
|
|
307
448
|
const View = svgViewClass();
|
|
@@ -311,15 +452,18 @@ export class SvgDrawing {
|
|
|
311
452
|
}
|
|
312
453
|
try {
|
|
313
454
|
const view = this._view ?? new View(null);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
455
|
+
const missing = { any: false };
|
|
456
|
+
const tree =
|
|
457
|
+
sized || shapes || root.name.includes(':')
|
|
458
|
+
? copyTree(root, sized ? [width, height] : null, missing, shapes)
|
|
459
|
+
: root;
|
|
460
|
+
view.setSvgDom(paint ? withPaint(tree, paint) : tree);
|
|
461
|
+
this._missingAt = missing.any ? lastNode(documentOf(root)) : undefined;
|
|
319
462
|
this._view = view;
|
|
320
463
|
this._seen = count;
|
|
321
464
|
this._seenLast = last;
|
|
322
465
|
this._seenViewport = viewport;
|
|
466
|
+
this._seenPaint = painted;
|
|
323
467
|
return view;
|
|
324
468
|
} catch {
|
|
325
469
|
this._failed = true;
|
|
@@ -328,6 +472,21 @@ export class SvgDrawing {
|
|
|
328
472
|
}
|
|
329
473
|
}
|
|
330
474
|
|
|
475
|
+
/**
|
|
476
|
+
* A drawing's root with its `fill` and `stroke` the document's: a copy of
|
|
477
|
+
* the root, its children the same ones, with the declarations at the end of
|
|
478
|
+
* its `style` — where `SvgView` reads a root's paint, over its presentation
|
|
479
|
+
* attributes, for what is under it to inherit.
|
|
480
|
+
*/
|
|
481
|
+
function withPaint(root: Element, paint: string): Element {
|
|
482
|
+
const style = root.attribs.style;
|
|
483
|
+
return new Element(
|
|
484
|
+
root.name,
|
|
485
|
+
{ ...root.attribs, style: style ? `${style};${paint}` : paint },
|
|
486
|
+
root.children,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
331
490
|
/**
|
|
332
491
|
* The colour an SVG image's root element sets as its background, in its
|
|
333
492
|
* `style` attribute — the one place an image can say it, since
|
|
@@ -349,6 +508,75 @@ function canvasBackground(root: Element): string | null {
|
|
|
349
508
|
return found;
|
|
350
509
|
}
|
|
351
510
|
|
|
511
|
+
/** What an SVG image's own style sheets give it (`SvgDrawing.drawImage`):
|
|
512
|
+
* its root's `color`, `fill` and `stroke`, and what they give the
|
|
513
|
+
* elements in it. */
|
|
514
|
+
interface ImagePaint {
|
|
515
|
+
color: string;
|
|
516
|
+
fill: string | null;
|
|
517
|
+
stroke: string | null;
|
|
518
|
+
shapes: ShapeStyles | null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The style sheets an SVG image's `<style>` elements hold, in order. One
|
|
523
|
+
* that names a type other than CSS's holds none (SVG 2, 6.3), and one with
|
|
524
|
+
* a `media` query holds its rules under it. Its text is its text and CDATA
|
|
525
|
+
* sections, as an XML parse leaves them, and not its comments.
|
|
526
|
+
*/
|
|
527
|
+
function ownSheets(root: Element): Stylesheet[] {
|
|
528
|
+
const sheets: Stylesheet[] = [];
|
|
529
|
+
const layers = new Map<string, number>();
|
|
530
|
+
let order = 0;
|
|
531
|
+
const stack: Element[] = [root];
|
|
532
|
+
for (let el = stack.pop(); el; el = stack.pop()) {
|
|
533
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
534
|
+
const child = el.children[i];
|
|
535
|
+
// an XML parse gives a `<style>` a type of its own, as HTML's does
|
|
536
|
+
if (child.type === 'tag' || child.type === 'style') {
|
|
537
|
+
stack.push(child as Element);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (el === root || localName(el.name) !== 'style') continue;
|
|
541
|
+
const type = (el.attribs.type ?? '').trim().toLowerCase();
|
|
542
|
+
if (type && type !== 'text/css') continue;
|
|
543
|
+
const media = el.attribs.media?.trim();
|
|
544
|
+
const sheet = parseStylesheet(
|
|
545
|
+
rawTextOf(el),
|
|
546
|
+
order,
|
|
547
|
+
layers,
|
|
548
|
+
null,
|
|
549
|
+
media ? [parseMediaQuery(media)] : null,
|
|
550
|
+
);
|
|
551
|
+
order += sheet.rules.length + 1;
|
|
552
|
+
sheets.push(sheet);
|
|
553
|
+
}
|
|
554
|
+
return sheets;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The look an SVG image's own document starts from: CSS's initial values,
|
|
559
|
+
* black at `medium`, since nothing of the page's is inherited into an
|
|
560
|
+
* image — only the colour scheme, which its `prefers-color-scheme` answers.
|
|
561
|
+
*/
|
|
562
|
+
function imageLook(scheme: 'light' | 'dark', scale: number): RootLook {
|
|
563
|
+
return {
|
|
564
|
+
color: '#000000',
|
|
565
|
+
fontFamily: 'sans-serif',
|
|
566
|
+
fontSize: 16 * scale,
|
|
567
|
+
monoFamily: 'monospace',
|
|
568
|
+
linkColor: '#0000ee',
|
|
569
|
+
borderColor: '#000000',
|
|
570
|
+
mutedColor: '#808080',
|
|
571
|
+
background: 'transparent',
|
|
572
|
+
colorScheme: scheme,
|
|
573
|
+
surface: '#ffffff',
|
|
574
|
+
controlPadY: 0,
|
|
575
|
+
controlBorder: 0,
|
|
576
|
+
controlRadius: 0,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
352
580
|
/** Drawings of inline `<svg>` elements, per element: the element is the
|
|
353
581
|
* document's, and outlives the box trees built over it. */
|
|
354
582
|
const INLINE = new WeakMap<Element, SvgDrawing>();
|
|
@@ -413,13 +641,163 @@ function hasPercent(root: Element): boolean {
|
|
|
413
641
|
return walk(root);
|
|
414
642
|
}
|
|
415
643
|
|
|
644
|
+
/** The element a `<use>` refers to by a fragment, its id: one in another
|
|
645
|
+
* document is none here, where nothing is fetched for a drawing. */
|
|
646
|
+
function useTarget(el: Element): string | null {
|
|
647
|
+
const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
|
|
648
|
+
return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** The elements under a root by their ids, the first of each. */
|
|
652
|
+
function idsUnder(root: Element): Map<string, Element> {
|
|
653
|
+
const ids = new Map<string, Element>();
|
|
654
|
+
const walk = (el: Element): void => {
|
|
655
|
+
for (const child of el.children) {
|
|
656
|
+
if (child.type !== 'tag') continue;
|
|
657
|
+
const tag = child as Element;
|
|
658
|
+
const id = tag.attribs.id;
|
|
659
|
+
if (id && !ids.has(id)) ids.set(id, tag);
|
|
660
|
+
walk(tag);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
walk(root);
|
|
664
|
+
return ids;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Whether a `<use>` under the root is one `SvgView` cannot draw as it
|
|
669
|
+
* stands: it looks a reference up among the root's own elements, and draws
|
|
670
|
+
* a `<symbol>` as its children where they are. So one to an element
|
|
671
|
+
* outside the root — an icon sprite's, a hidden `<svg>` of symbols at the
|
|
672
|
+
* top of the page — and one to a symbol with a `viewBox` of its own are
|
|
673
|
+
* drawn from a copy (`copyTree`).
|
|
674
|
+
*/
|
|
675
|
+
function needsExpanding(root: Element): boolean {
|
|
676
|
+
let ids: Map<string, Element> | null = null;
|
|
677
|
+
const walk = (el: Element): boolean => {
|
|
678
|
+
for (const child of el.children) {
|
|
679
|
+
if (child.type !== 'tag') continue;
|
|
680
|
+
const tag = child as Element;
|
|
681
|
+
if (localName(tag.name) === 'use') {
|
|
682
|
+
const id = useTarget(tag);
|
|
683
|
+
if (id !== null) {
|
|
684
|
+
ids ??= idsUnder(root);
|
|
685
|
+
const target = ids.get(id);
|
|
686
|
+
if (!target) return true;
|
|
687
|
+
if (localName(target.name) === 'symbol' && viewBoxOf(target)) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (walk(tag)) return true;
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
695
|
+
};
|
|
696
|
+
return walk(root);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function documentOf(el: Element): ParentNode {
|
|
700
|
+
let top: ParentNode = el;
|
|
701
|
+
while (top.parent) top = top.parent;
|
|
702
|
+
return top;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** The last node of a document, in its order: what a document that is
|
|
706
|
+
* still arriving has another of once more of it has. */
|
|
707
|
+
function lastNode(top: ParentNode): ChildNode | null {
|
|
708
|
+
let last: ChildNode | null = top.lastChild;
|
|
709
|
+
while (last && 'lastChild' in last && last.lastChild) last = last.lastChild;
|
|
710
|
+
return last;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** The first element of a document with an id, in its order. */
|
|
714
|
+
function elementById(top: ParentNode, id: string): Element | null {
|
|
715
|
+
const stack: ChildNode[] = [...top.children].reverse();
|
|
716
|
+
for (let node = stack.pop(); node; node = stack.pop()) {
|
|
717
|
+
if (node.type !== 'tag') continue;
|
|
718
|
+
const el = node as Element;
|
|
719
|
+
if (el.attribs.id === id) return el;
|
|
720
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
721
|
+
stack.push(el.children[i]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** How deep a `<use>` may be of a `<use>`: one that reaches itself stops. */
|
|
728
|
+
const USE_DEPTH = 8;
|
|
729
|
+
|
|
730
|
+
/** A `<use>`'s `width` or `height`, in user units: a number, or a
|
|
731
|
+
* percentage of the viewport's; null for none, and for `auto`. */
|
|
732
|
+
function useLength(value: string | undefined, of: number): number | null {
|
|
733
|
+
if (value === undefined) return null;
|
|
734
|
+
const n = parseFloat(value);
|
|
735
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
736
|
+
return value.trim().endsWith('%') ? (n / 100) * of : n;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** The transform that fits a `viewBox` into a viewport at `x`, `y`, as
|
|
740
|
+
* `preserveAspectRatio` says (SVG 2, 8.2). */
|
|
741
|
+
function fitTransform(
|
|
742
|
+
box: [number, number, number, number],
|
|
743
|
+
x: number,
|
|
744
|
+
y: number,
|
|
745
|
+
width: number,
|
|
746
|
+
height: number,
|
|
747
|
+
preserve: string | undefined,
|
|
748
|
+
): string {
|
|
749
|
+
const fit = (preserve ?? '').trim().split(/\s+/);
|
|
750
|
+
if (fit[0] === 'defer') fit.shift();
|
|
751
|
+
let sx = width / box[2];
|
|
752
|
+
let sy = height / box[3];
|
|
753
|
+
if (fit[0] !== 'none') {
|
|
754
|
+
const align = ALIGN.exec(fit[0] ?? '') ?? ['xMidYMid', 'Mid', 'Mid'];
|
|
755
|
+
const scale = fit[1] === 'slice' ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
756
|
+
x += (width - box[2] * scale) * AT[align[1]];
|
|
757
|
+
y += (height - box[3] * scale) * AT[align[2]];
|
|
758
|
+
sx = scale;
|
|
759
|
+
sy = scale;
|
|
760
|
+
}
|
|
761
|
+
return `translate(${x - box[0] * sx},${y - box[1] * sy}) scale(${sx},${sy})`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** What a `<use>` says of where its element goes, and not of how it looks:
|
|
765
|
+
* the rest is the group's the element is drawn in. */
|
|
766
|
+
const USE_PLACEMENT = new Set([
|
|
767
|
+
'href',
|
|
768
|
+
'xlink:href',
|
|
769
|
+
'x',
|
|
770
|
+
'y',
|
|
771
|
+
'width',
|
|
772
|
+
'height',
|
|
773
|
+
]);
|
|
774
|
+
|
|
416
775
|
/**
|
|
417
776
|
* The tree `SvgView` reads, where the document's own will not do: with
|
|
418
777
|
* local names — `SvgView` knows `rect`, not `svg:rect`, and a prefix is
|
|
419
778
|
* dropped only where it is bound to SVG — and with its percentages resolved
|
|
420
|
-
* against a viewport of `[width, height]` user units, where one is given
|
|
779
|
+
* against a viewport of `[width, height]` user units, where one is given;
|
|
780
|
+
* and with each `<use>` that `SvgView` cannot draw (`needsExpanding`) as a
|
|
781
|
+
* group of what it refers to (SVG 2, 5.5): the element, from wherever in
|
|
782
|
+
* the document it is, or a symbol's children, its `viewBox` fitted to the
|
|
783
|
+
* viewport the `<use>` gives it — its `width` and `height`, and all of the
|
|
784
|
+
* drawing's where it has none. `missing.any` is set where one refers to an
|
|
785
|
+
* element the document does not have.
|
|
786
|
+
*
|
|
787
|
+
* And with what the document's rules give each element (`shapes`), where
|
|
788
|
+
* `SvgView` reads an element's own: at the end of its `style`, over its
|
|
789
|
+
* presentation attributes and over what that attribute says, which the
|
|
790
|
+
* cascade that made them has weighed already. An element a rule gives
|
|
791
|
+
* `display: none` is left out, with what is in it, and a shape one hides
|
|
792
|
+
* — `visibility` is inherited, and a shape in a hidden group may be shown
|
|
793
|
+
* again — is left out alone.
|
|
421
794
|
*/
|
|
422
|
-
function copyTree(
|
|
795
|
+
function copyTree(
|
|
796
|
+
root: Element,
|
|
797
|
+
viewport: [number, number] | null,
|
|
798
|
+
missing?: { any: boolean },
|
|
799
|
+
shapes: ShapeStyles | null = null,
|
|
800
|
+
): Element {
|
|
423
801
|
const colon = root.name.indexOf(':');
|
|
424
802
|
const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
|
|
425
803
|
const strip = (name: string): string =>
|
|
@@ -427,9 +805,104 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
|
|
|
427
805
|
const diagonal = viewport
|
|
428
806
|
? Math.sqrt((viewport[0] ** 2 + viewport[1] ** 2) / 2)
|
|
429
807
|
: 0;
|
|
430
|
-
|
|
808
|
+
let ids: Map<string, Element> | null = null;
|
|
809
|
+
const expand = (
|
|
810
|
+
use: Element,
|
|
811
|
+
resolve: boolean,
|
|
812
|
+
depth: number,
|
|
813
|
+
hidden: boolean,
|
|
814
|
+
): Element | null => {
|
|
815
|
+
const id = useTarget(use);
|
|
816
|
+
if (id === null || depth >= USE_DEPTH) return null;
|
|
817
|
+
ids ??= idsUnder(root);
|
|
818
|
+
const inside = ids.get(id);
|
|
819
|
+
const target = inside ?? elementById(documentOf(root), id);
|
|
820
|
+
if (!target) {
|
|
821
|
+
if (missing) missing.any = true;
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const symbol = localName(target.name) === 'symbol';
|
|
825
|
+
const box = symbol ? viewBoxOf(target) : null;
|
|
826
|
+
// one of the root's own with no viewport to fit: `SvgView` draws it
|
|
827
|
+
if (inside && !box) return null;
|
|
828
|
+
const attribs: Record<string, string> = {};
|
|
829
|
+
for (const name in use.attribs) {
|
|
830
|
+
if (!USE_PLACEMENT.has(name)) attribs[name] = use.attribs[name];
|
|
831
|
+
}
|
|
832
|
+
const own = shapes?.of.get(use);
|
|
833
|
+
if (own) restyle(attribs, own);
|
|
834
|
+
const x = parseFloat(use.attribs.x ?? '') || 0;
|
|
835
|
+
const y = parseFloat(use.attribs.y ?? '') || 0;
|
|
836
|
+
let placed = '';
|
|
837
|
+
if (box && viewport) {
|
|
838
|
+
const width =
|
|
839
|
+
useLength(use.attribs.width, viewport[0]) ??
|
|
840
|
+
useLength(target.attribs.width, viewport[0]) ??
|
|
841
|
+
viewport[0];
|
|
842
|
+
const height =
|
|
843
|
+
useLength(use.attribs.height, viewport[1]) ??
|
|
844
|
+
useLength(target.attribs.height, viewport[1]) ??
|
|
845
|
+
viewport[1];
|
|
846
|
+
placed = fitTransform(
|
|
847
|
+
box,
|
|
848
|
+
x,
|
|
849
|
+
y,
|
|
850
|
+
width,
|
|
851
|
+
height,
|
|
852
|
+
svgAttr(target, 'preserveAspectRatio'),
|
|
853
|
+
);
|
|
854
|
+
} else if (x || y) placed = `translate(${x},${y})`;
|
|
855
|
+
const transform = `${attribs.transform ?? ''} ${placed}`.trim();
|
|
856
|
+
if (transform) attribs.transform = transform;
|
|
857
|
+
// a length in a symbol with a `viewBox` is of that, and left as it is
|
|
858
|
+
const within = resolve && !box;
|
|
859
|
+
const children: ChildNode[] = [];
|
|
860
|
+
if (symbol) copyInto(children, target, within, depth + 1, hidden);
|
|
861
|
+
else if (shown(target, hidden)) {
|
|
862
|
+
children.push(copy(target, within, depth + 1, hidden));
|
|
863
|
+
}
|
|
864
|
+
return new Element('g', attribs, children);
|
|
865
|
+
};
|
|
866
|
+
/** Whether an element is drawn at all, in a group that is hidden or
|
|
867
|
+
* not: one no rule gives `display: none`, and no shape a rule hides. */
|
|
868
|
+
const shown = (el: Element, hidden: boolean): boolean => {
|
|
869
|
+
if (!shapes) return true;
|
|
870
|
+
const own = shapes.of.get(el);
|
|
871
|
+
const name = localName(el.name);
|
|
872
|
+
if (own?.display === 'none' && !UNDRAWN.has(name)) return false;
|
|
873
|
+
return !(hiddenIn(own, hidden) && HIDEABLE.has(name));
|
|
874
|
+
};
|
|
875
|
+
const copyInto = (
|
|
876
|
+
children: ChildNode[],
|
|
877
|
+
el: Element,
|
|
878
|
+
resolve: boolean,
|
|
879
|
+
depth: number,
|
|
880
|
+
hidden: boolean,
|
|
881
|
+
): void => {
|
|
882
|
+
for (const child of el.children) {
|
|
883
|
+
if (child.type === 'tag') {
|
|
884
|
+
if (shown(child as Element, hidden)) {
|
|
885
|
+
children.push(copy(child as Element, resolve, depth, hidden));
|
|
886
|
+
}
|
|
887
|
+
} else if (child.type === 'text') children.push(new Text(child.data));
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
const copy = (
|
|
891
|
+
el: Element,
|
|
892
|
+
resolve: boolean,
|
|
893
|
+
depth = 0,
|
|
894
|
+
/** Whether the element inherits a `visibility` of `hidden`. */
|
|
895
|
+
hidden = false,
|
|
896
|
+
): Element => {
|
|
897
|
+
const own = shapes?.of.get(el);
|
|
898
|
+
hidden = hiddenIn(own, hidden);
|
|
899
|
+
if (localName(el.name) === 'use') {
|
|
900
|
+
const group = expand(el, resolve, depth, hidden);
|
|
901
|
+
if (group) return group;
|
|
902
|
+
}
|
|
431
903
|
const here = resolve && !OWN_UNITS.has(localName(el.name));
|
|
432
904
|
const attribs = { ...el.attribs };
|
|
905
|
+
if (own) restyle(attribs, own);
|
|
433
906
|
if (here && viewport && el !== root) {
|
|
434
907
|
for (const name in attribs) {
|
|
435
908
|
const axis = PERCENT_OF[name];
|
|
@@ -443,15 +916,79 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
|
|
|
443
916
|
}
|
|
444
917
|
}
|
|
445
918
|
const children: ChildNode[] = [];
|
|
446
|
-
|
|
447
|
-
if (child.type === 'tag') children.push(copy(child as Element, here));
|
|
448
|
-
else if (child.type === 'text') children.push(new Text(child.data));
|
|
449
|
-
}
|
|
919
|
+
copyInto(children, el, here, depth, hidden);
|
|
450
920
|
return new Element(strip(el.name), attribs, children);
|
|
451
921
|
};
|
|
452
922
|
return copy(root, true);
|
|
453
923
|
}
|
|
454
924
|
|
|
925
|
+
/** What `display: none` takes nothing from: `SvgView` draws none of them
|
|
926
|
+
* where they stand, and what refers to one still finds it (SVG 2, 5.3). */
|
|
927
|
+
const UNDRAWN = new Set([
|
|
928
|
+
'defs',
|
|
929
|
+
'symbol',
|
|
930
|
+
'lineargradient',
|
|
931
|
+
'radialgradient',
|
|
932
|
+
'stop',
|
|
933
|
+
'clippath',
|
|
934
|
+
'mask',
|
|
935
|
+
'pattern',
|
|
936
|
+
'marker',
|
|
937
|
+
'style',
|
|
938
|
+
]);
|
|
939
|
+
|
|
940
|
+
/** What `visibility: hidden` leaves undrawn: the elements with something
|
|
941
|
+
* of their own to draw, and a `<use>`, since what it draws inherits the
|
|
942
|
+
* property from it. */
|
|
943
|
+
const HIDEABLE = new Set([
|
|
944
|
+
'path',
|
|
945
|
+
'rect',
|
|
946
|
+
'circle',
|
|
947
|
+
'ellipse',
|
|
948
|
+
'line',
|
|
949
|
+
'polyline',
|
|
950
|
+
'polygon',
|
|
951
|
+
'text',
|
|
952
|
+
'image',
|
|
953
|
+
'use',
|
|
954
|
+
]);
|
|
955
|
+
|
|
956
|
+
/** Whether an element is hidden, given whether what it is in is. */
|
|
957
|
+
function hiddenIn(own: ShapeStyle | undefined, hidden: boolean): boolean {
|
|
958
|
+
const visibility = own?.visibility;
|
|
959
|
+
return visibility === undefined || visibility === 'inherit'
|
|
960
|
+
? hidden
|
|
961
|
+
: visibility === 'hidden';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* An element's attributes with what the rules give it: each property at
|
|
966
|
+
* the end of its `style`, where `SvgView` reads it over the attribute of
|
|
967
|
+
* its name — but `opacity`, which it reads from the attribute alone. One
|
|
968
|
+
* the rules put back to what the element inherits is taken out of both.
|
|
969
|
+
*/
|
|
970
|
+
function restyle(attribs: Record<string, string>, own: ShapeStyle): void {
|
|
971
|
+
let style = attribs.style ?? '';
|
|
972
|
+
let added = '';
|
|
973
|
+
for (const prop in own) {
|
|
974
|
+
const value = own[prop];
|
|
975
|
+
if (prop === 'display' || prop === 'visibility') continue;
|
|
976
|
+
if (value === 'inherit') {
|
|
977
|
+
delete attribs[prop];
|
|
978
|
+
if (style) {
|
|
979
|
+
style = style
|
|
980
|
+
.split(';')
|
|
981
|
+
.filter((d) => d.slice(0, d.indexOf(':')).trim() !== prop)
|
|
982
|
+
.join(';');
|
|
983
|
+
}
|
|
984
|
+
} else if (prop === 'opacity') attribs.opacity = value;
|
|
985
|
+
else added += `${prop}:${value};`;
|
|
986
|
+
}
|
|
987
|
+
style = style && added ? `${style};${added}` : style || added;
|
|
988
|
+
if (style) attribs.style = style;
|
|
989
|
+
else delete attribs.style;
|
|
990
|
+
}
|
|
991
|
+
|
|
455
992
|
/**
|
|
456
993
|
* An SVG image's bytes as a drawing, or null for bytes that are not SVG —
|
|
457
994
|
* which is how a PNG or a JPEG goes on to the image decoder. Nothing names
|