@react-x11/components 0.10.0 → 0.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/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +25 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/codeblock/index.d.ts +13 -0
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +37 -1
- package/dist/codeblock/index.js.map +1 -1
- package/dist/html/css/cascade.d.ts +32 -10
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +72 -27
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/style.d.ts +7 -4
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +37 -6
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +8 -5
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +127 -9
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +27 -0
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +30 -2
- package/dist/html/layout/boxes.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 +236 -14
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +118 -57
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +203 -37
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +27 -20
- package/dist/html/svg.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +98 -26
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +3 -8
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +100 -25
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/code/index.ts +34 -32
- package/src/code-language/index.ts +1 -1
- package/src/code-language/runs.ts +111 -1
- package/src/codeblock/index.ts +48 -1
- package/src/html/css/cascade.ts +117 -39
- package/src/html/css/style.ts +43 -15
- package/src/html/css/ua.ts +8 -5
- package/src/html/layout/block.ts +134 -8
- package/src/html/layout/boxes.ts +43 -2
- package/src/html/layout/inline.ts +257 -13
- package/src/html/node.ts +145 -79
- package/src/html/paint.ts +236 -45
- package/src/html/svg.ts +30 -19
- package/src/internal/codelines.ts +200 -0
- package/src/markdown/index.ts +145 -26
- package/src/rich-text-editor/index.ts +3 -11
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/terminal-output/index.ts +127 -34
- package/src/terminal-output/runs.ts +16 -2
package/src/html/paint.ts
CHANGED
|
@@ -30,13 +30,13 @@ import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
|
|
|
30
30
|
import { blend, borderShades } from './css/color.js';
|
|
31
31
|
import type { Len } from './css/values.js';
|
|
32
32
|
import type {
|
|
33
|
+
BackgroundClip,
|
|
33
34
|
BackgroundRepeat,
|
|
34
35
|
BoxShadow,
|
|
35
36
|
ComputedStyle,
|
|
36
37
|
ImageRepeat,
|
|
37
38
|
LinearGradient,
|
|
38
39
|
RepeatMode,
|
|
39
|
-
VisualBox,
|
|
40
40
|
} from './css/style.js';
|
|
41
41
|
import {
|
|
42
42
|
CONTAIN_LAYOUT,
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
FADED_BLOCKS,
|
|
57
57
|
PAINT_ORDER,
|
|
58
58
|
INLINE_OFFSETS,
|
|
59
|
+
MOVED_OFF_LINES,
|
|
59
60
|
SHADOWED_TEXT,
|
|
60
61
|
SHIFTED_LINES,
|
|
61
62
|
TEXT_RAISES,
|
|
@@ -401,7 +402,7 @@ export function paintDocument(
|
|
|
401
402
|
): void {
|
|
402
403
|
if (!canFill(ctx)) return;
|
|
403
404
|
ctx.save();
|
|
404
|
-
const canvas = canvasBackground(tree
|
|
405
|
+
const canvas = canvasBackground(tree);
|
|
405
406
|
if (canvas) paintCanvas(ctx, canvas, tree.root, options);
|
|
406
407
|
paintBox(ctx, tree.root, {
|
|
407
408
|
...options,
|
|
@@ -411,27 +412,51 @@ export function paintDocument(
|
|
|
411
412
|
ctx.restore();
|
|
412
413
|
}
|
|
413
414
|
|
|
415
|
+
/** What covers the canvas: a style's background, the box that would have
|
|
416
|
+
* painted it and now paints none, and the box its image is placed by. */
|
|
417
|
+
interface CanvasBackground {
|
|
418
|
+
style: ComputedStyle;
|
|
419
|
+
source: Box | null;
|
|
420
|
+
anchor: Box;
|
|
421
|
+
}
|
|
422
|
+
|
|
414
423
|
/**
|
|
415
424
|
* Whose background covers the canvas (CSS 2.1 14.2): the root element's,
|
|
416
425
|
* or — where `<html>` has neither a colour nor an image — the first
|
|
417
426
|
* `<body>`'s, which then paints no background of its own. A document with
|
|
418
427
|
* no `<html>` tag has the root box standing in for its root element, and a
|
|
419
428
|
* `<body>` in it is still the body: mail often starts at `<body style>`.
|
|
420
|
-
*
|
|
421
|
-
*
|
|
429
|
+
* With neither tag, the root box stands in for the body, and the `<html>`
|
|
430
|
+
* around it is implied: an `html { … }` or a `:root { … }` background, which
|
|
431
|
+
* a test's reference sets on a document with no tags at all, has no box
|
|
432
|
+
* but the canvas to be painted on. `anchor` is the box the image is
|
|
433
|
+
* positioned against: the root element's, whichever box it came from.
|
|
422
434
|
*/
|
|
423
|
-
function canvasBackground(
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
!!
|
|
428
|
-
!!
|
|
435
|
+
function canvasBackground(tree: BoxTree): CanvasBackground | null {
|
|
436
|
+
const root = tree.root;
|
|
437
|
+
const has = (s: ComputedStyle) =>
|
|
438
|
+
!isTransparent(s.backgroundColor) ||
|
|
439
|
+
!!s.backgroundImage ||
|
|
440
|
+
!!s.backgroundGradient ||
|
|
441
|
+
!!s.backgroundImages;
|
|
442
|
+
const implied = tree.impliedHtml;
|
|
443
|
+
if (implied) {
|
|
444
|
+
// an `<html>` that is not displayed has no background to give
|
|
445
|
+
if (implied.display === 'none') return null;
|
|
446
|
+
if (has(implied)) return { style: implied, source: null, anchor: root };
|
|
447
|
+
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
448
|
+
if (implied.contain || root.style.contain) return null;
|
|
449
|
+
return has(root.style)
|
|
450
|
+
? { style: root.style, source: root, anchor: root }
|
|
451
|
+
: null;
|
|
452
|
+
}
|
|
429
453
|
const top = childNamed(root, 'html') ?? root;
|
|
430
|
-
if (has(top)) return { source: top, anchor: top };
|
|
454
|
+
if (has(top.style)) return { style: top.style, source: top, anchor: top };
|
|
431
455
|
const body = childNamed(top, 'body');
|
|
432
|
-
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
433
456
|
if (!body || top.style.contain || body.style.contain) return null;
|
|
434
|
-
return has(body)
|
|
457
|
+
return has(body.style)
|
|
458
|
+
? { style: body.style, source: body, anchor: top }
|
|
459
|
+
: null;
|
|
435
460
|
}
|
|
436
461
|
|
|
437
462
|
/** A box's child element of that name, or one inside the anonymous boxes
|
|
@@ -450,7 +475,7 @@ function childNamed(box: Box, name: string): Box | null {
|
|
|
450
475
|
|
|
451
476
|
function paintCanvas(
|
|
452
477
|
ctx: PaintContext,
|
|
453
|
-
{ source, anchor }:
|
|
478
|
+
{ style: source, anchor }: CanvasBackground,
|
|
454
479
|
root: Box,
|
|
455
480
|
options: PaintOptions,
|
|
456
481
|
): void {
|
|
@@ -468,8 +493,8 @@ function paintCanvas(
|
|
|
468
493
|
Math.ceil(whole.height),
|
|
469
494
|
);
|
|
470
495
|
if (!area) return;
|
|
471
|
-
const layers = layersOf(source
|
|
472
|
-
const visible = source.
|
|
496
|
+
const layers = layersOf(source) ?? [source];
|
|
497
|
+
const visible = source.visibility === 'visible';
|
|
473
498
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
474
499
|
const style = layers[i];
|
|
475
500
|
if (!isTransparent(style.backgroundColor) && visible) {
|
|
@@ -568,20 +593,37 @@ function paintLayers(
|
|
|
568
593
|
image?: (layer: ComputedStyle) => void,
|
|
569
594
|
): void {
|
|
570
595
|
// a background painted through the text is painted with it
|
|
571
|
-
// (`paintClippedText`), and not as a box
|
|
572
|
-
|
|
573
|
-
const
|
|
596
|
+
// (`paintClippedText`), and not as a box — but for what `border-area
|
|
597
|
+
// text` paints in the border as well
|
|
598
|
+
const style = box.style;
|
|
599
|
+
if (style.backgroundClipText && style.backgroundClip !== 'border-area') {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const layers = layersOf(style);
|
|
574
603
|
if (!layers) {
|
|
575
|
-
|
|
576
|
-
if (image && box.style.backgroundImage) image(box.style);
|
|
604
|
+
paintLayer(ctx, box, options, style, image);
|
|
577
605
|
return;
|
|
578
606
|
}
|
|
579
607
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
580
|
-
|
|
581
|
-
if (image && layers[i].backgroundImage) image(layers[i]);
|
|
608
|
+
paintLayer(ctx, box, options, layers[i], image);
|
|
582
609
|
}
|
|
583
610
|
}
|
|
584
611
|
|
|
612
|
+
function paintLayer(
|
|
613
|
+
ctx: PaintContext,
|
|
614
|
+
box: Frame,
|
|
615
|
+
options: PaintOptions,
|
|
616
|
+
layer: ComputedStyle,
|
|
617
|
+
image?: (layer: ComputedStyle) => void,
|
|
618
|
+
): void {
|
|
619
|
+
// painted over the border box, inside what the border paints
|
|
620
|
+
const area = layer.backgroundClip === 'border-area';
|
|
621
|
+
if (area && !pushBorderArea(ctx, box, options)) return;
|
|
622
|
+
paintBackground(ctx, box, options, layer);
|
|
623
|
+
if (image && layer.backgroundImage) image(layer);
|
|
624
|
+
if (area) ctx.restore();
|
|
625
|
+
}
|
|
626
|
+
|
|
585
627
|
/** A box's padding box in window coordinates. */
|
|
586
628
|
function paddingBox(box: Box, options: PaintOptions): Rect {
|
|
587
629
|
return {
|
|
@@ -837,22 +879,7 @@ function paintOwnBackground(
|
|
|
837
879
|
const style = box.style;
|
|
838
880
|
if (style.boxShadow) paintShadows(ctx, box, options, false);
|
|
839
881
|
if (box !== options.canvasSource) {
|
|
840
|
-
paintLayers(ctx, box, options, (
|
|
841
|
-
const painted = clipArea(box, options, layer, true);
|
|
842
|
-
const area =
|
|
843
|
-
painted &&
|
|
844
|
-
clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
845
|
-
if (area) {
|
|
846
|
-
paintBackgroundImage(
|
|
847
|
-
ctx,
|
|
848
|
-
layer,
|
|
849
|
-
area,
|
|
850
|
-
originBox(box, options, layer),
|
|
851
|
-
options,
|
|
852
|
-
painted.corners,
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
});
|
|
882
|
+
paintLayers(ctx, box, options, frameImages(ctx, box, options));
|
|
856
883
|
}
|
|
857
884
|
if (style.boxShadow) paintShadows(ctx, box, options, true);
|
|
858
885
|
if (!box.bordersCollapsed && !paintBorderImage(ctx, box, options)) {
|
|
@@ -861,6 +888,34 @@ function paintOwnBackground(
|
|
|
861
888
|
if (box.kind === 'table') paintPartBackgrounds(ctx, box, options);
|
|
862
889
|
}
|
|
863
890
|
|
|
891
|
+
/**
|
|
892
|
+
* How `paintLayers` draws a frame's image layers: each in the area its
|
|
893
|
+
* `background-clip` names, from the box its `background-origin` names
|
|
894
|
+
* (CSS Backgrounds 3, 3.7 and 3.8) — a block's, or an inline box's
|
|
895
|
+
* fragment's, whose background is its images as much as its colour.
|
|
896
|
+
*/
|
|
897
|
+
function frameImages(
|
|
898
|
+
ctx: PaintContext,
|
|
899
|
+
frame: Frame,
|
|
900
|
+
options: PaintOptions,
|
|
901
|
+
): (layer: ComputedStyle) => void {
|
|
902
|
+
return (layer) => {
|
|
903
|
+
const painted = clipArea(frame, options, layer, true);
|
|
904
|
+
const area =
|
|
905
|
+
painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
906
|
+
if (area) {
|
|
907
|
+
paintBackgroundImage(
|
|
908
|
+
ctx,
|
|
909
|
+
layer,
|
|
910
|
+
area,
|
|
911
|
+
originBox(frame, options, layer),
|
|
912
|
+
options,
|
|
913
|
+
painted.corners,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
864
919
|
/** How far past its shape a shadow's blur shows: three standard deviations,
|
|
865
920
|
* the blur being two (CSS Backgrounds 3, 7.1.1). */
|
|
866
921
|
function shadowReach(blur: number): number {
|
|
@@ -1634,12 +1689,13 @@ function spreadCorners(
|
|
|
1634
1689
|
}
|
|
1635
1690
|
|
|
1636
1691
|
/** How far in from a box's border box the box a `<visual-box>` names is:
|
|
1637
|
-
* top, right, bottom and left
|
|
1692
|
+
* top, right, bottom and left — none for `border-area`, whose layer is
|
|
1693
|
+
* painted over the border box inside what the border paints. */
|
|
1638
1694
|
function edgeInsets(
|
|
1639
1695
|
box: Frame,
|
|
1640
|
-
which:
|
|
1696
|
+
which: BackgroundClip,
|
|
1641
1697
|
): readonly [number, number, number, number] {
|
|
1642
|
-
if (which === 'border-box') return NO_INSETS;
|
|
1698
|
+
if (which === 'border-box' || which === 'border-area') return NO_INSETS;
|
|
1643
1699
|
const t = box.borderTop;
|
|
1644
1700
|
const r = box.borderRight;
|
|
1645
1701
|
const b = box.borderBottom;
|
|
@@ -3559,6 +3615,94 @@ function roundedRing(
|
|
|
3559
3615
|
return true;
|
|
3560
3616
|
}
|
|
3561
3617
|
|
|
3618
|
+
/**
|
|
3619
|
+
* Clip to what a box's border paints, for a layer of `background-clip:
|
|
3620
|
+
* border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
|
|
3621
|
+
* not its colour — a transparent border still has an area, which is what
|
|
3622
|
+
* the value is for. The shapes are the border painter's own, so that the
|
|
3623
|
+
* background shows exactly where that border would: the ring `roundedRing`
|
|
3624
|
+
* fills where a rounded border is solid, the ring the trapezoids of a 3D
|
|
3625
|
+
* style make, and otherwise the rectangles `fillEdge` fills a side at a
|
|
3626
|
+
* time — the dots, the dashes, the two lines of a double border. All of
|
|
3627
|
+
* them run clockwise and a ring's inside the other way, so the clip is the
|
|
3628
|
+
* union by the non-zero rule, the one every context's `clip` takes. False
|
|
3629
|
+
* where the border paints nothing or the context cannot clip, and nothing
|
|
3630
|
+
* is pushed.
|
|
3631
|
+
*/
|
|
3632
|
+
function pushBorderArea(
|
|
3633
|
+
ctx: PaintContext,
|
|
3634
|
+
box: Frame,
|
|
3635
|
+
options: PaintOptions,
|
|
3636
|
+
): boolean {
|
|
3637
|
+
const t = box.borderTop;
|
|
3638
|
+
const r = box.borderRight;
|
|
3639
|
+
const b = box.borderBottom;
|
|
3640
|
+
const l = box.borderLeft;
|
|
3641
|
+
if (!(t || r || b || l) || !ctx.beginPath || !ctx.rect || !ctx.clip) {
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3644
|
+
const s = box.style;
|
|
3645
|
+
const left = box.x + options.originX;
|
|
3646
|
+
const top = frameY(box) + options.originY;
|
|
3647
|
+
const x = Math.round(left);
|
|
3648
|
+
const y = Math.round(top);
|
|
3649
|
+
const w = Math.round(left + box.width) - x;
|
|
3650
|
+
const h = Math.round(top + frameHeight(box)) - y;
|
|
3651
|
+
if (w <= 0 || h <= 0) return false;
|
|
3652
|
+
const area = clampRect(options, x, y, w, h);
|
|
3653
|
+
if (!area) return false;
|
|
3654
|
+
const corners = cornersOf(s, w, h);
|
|
3655
|
+
ctx.save();
|
|
3656
|
+
ctx.beginPath();
|
|
3657
|
+
if (corners && canCurve(ctx) && solidBorder(box)) {
|
|
3658
|
+
roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
|
|
3659
|
+
const iw = area.w - l - r;
|
|
3660
|
+
const ih = area.h - t - b;
|
|
3661
|
+
if (iw > 0 && ih > 0) {
|
|
3662
|
+
const inner = insetCorners(corners, t, r, b, l);
|
|
3663
|
+
roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
|
|
3664
|
+
}
|
|
3665
|
+
} else if (sculpted(s)) {
|
|
3666
|
+
// a band a side, which the trapezoids cover between them
|
|
3667
|
+
ctx.rect(area.x, area.y, area.w, t);
|
|
3668
|
+
ctx.rect(area.x, area.y + area.h - b, area.w, b);
|
|
3669
|
+
ctx.rect(area.x, area.y + t, l, area.h - t - b);
|
|
3670
|
+
ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
|
|
3671
|
+
} else {
|
|
3672
|
+
const path = { fillRect: ctx.rect.bind(ctx) };
|
|
3673
|
+
const edge = (
|
|
3674
|
+
ex: number,
|
|
3675
|
+
ey: number,
|
|
3676
|
+
ew: number,
|
|
3677
|
+
eh: number,
|
|
3678
|
+
style: ComputedStyle['borderTopStyle'],
|
|
3679
|
+
horizontal: boolean,
|
|
3680
|
+
): void => {
|
|
3681
|
+
const rect = clampRect(options, ex, ey, ew, eh);
|
|
3682
|
+
if (rect) fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
|
|
3683
|
+
};
|
|
3684
|
+
const between = h - t - b;
|
|
3685
|
+
if (t > 0) edge(x, y, w, t, s.borderTopStyle, true);
|
|
3686
|
+
if (b > 0) edge(x, y + h - b, w, b, s.borderBottomStyle, true);
|
|
3687
|
+
if (l > 0) edge(x, y + t, l, between, s.borderLeftStyle, false);
|
|
3688
|
+
if (r > 0) edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
|
|
3689
|
+
}
|
|
3690
|
+
ctx.clip();
|
|
3691
|
+
return true;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
/** Whether every side a box has a border on is solid, which a rounded box
|
|
3695
|
+
* draws as one ring (`roundedRing`). */
|
|
3696
|
+
function solidBorder(box: Frame): boolean {
|
|
3697
|
+
const s = box.style;
|
|
3698
|
+
return (
|
|
3699
|
+
(!box.borderTop || s.borderTopStyle === 'solid') &&
|
|
3700
|
+
(!box.borderRight || s.borderRightStyle === 'solid') &&
|
|
3701
|
+
(!box.borderBottom || s.borderBottomStyle === 'solid') &&
|
|
3702
|
+
(!box.borderLeft || s.borderLeftStyle === 'solid')
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3562
3706
|
/** The border styles drawn in two shades, as though lit from the top left. */
|
|
3563
3707
|
const SCULPTED = new Set(['groove', 'ridge', 'inset', 'outset']);
|
|
3564
3708
|
|
|
@@ -3757,7 +3901,7 @@ function paintCollapsedBorders(
|
|
|
3757
3901
|
}
|
|
3758
3902
|
|
|
3759
3903
|
function fillEdge(
|
|
3760
|
-
ctx:
|
|
3904
|
+
ctx: Pick<FillContext, 'fillRect'>,
|
|
3761
3905
|
rect: { x: number; y: number; w: number; h: number },
|
|
3762
3906
|
phaseOrigin: number,
|
|
3763
3907
|
style: ComputedStyle['borderTopStyle'],
|
|
@@ -3994,6 +4138,17 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
3994
4138
|
if (line.y >= bottom) break;
|
|
3995
4139
|
if (line.y + line.height > top) visible.push(line);
|
|
3996
4140
|
}
|
|
4141
|
+
// and a line the damage misses whose text or inline-block `position:
|
|
4142
|
+
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
4143
|
+
const moved = MOVED_OFF_LINES.get(lines);
|
|
4144
|
+
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
4145
|
+
const drawn = new Set(visible);
|
|
4146
|
+
for (const line of moved) {
|
|
4147
|
+
if (movedInto(line, top, bottom)) drawn.add(line);
|
|
4148
|
+
}
|
|
4149
|
+
visible.length = 0;
|
|
4150
|
+
for (const line of lines) if (drawn.has(line)) visible.push(line);
|
|
4151
|
+
}
|
|
3997
4152
|
} else {
|
|
3998
4153
|
visible.push(...lines);
|
|
3999
4154
|
}
|
|
@@ -4060,6 +4215,39 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4060
4215
|
}
|
|
4061
4216
|
}
|
|
4062
4217
|
|
|
4218
|
+
/** Whether any of the lines moved content was taken off, and the damage
|
|
4219
|
+
* does not already draw, has it within the damage's rows. */
|
|
4220
|
+
function reachedOff(
|
|
4221
|
+
visible: readonly LineBox[],
|
|
4222
|
+
moved: ReadonlySet<LineBox>,
|
|
4223
|
+
top: number,
|
|
4224
|
+
bottom: number,
|
|
4225
|
+
): boolean {
|
|
4226
|
+
for (const line of moved) {
|
|
4227
|
+
if (!visible.includes(line) && movedInto(line, top, bottom)) return true;
|
|
4228
|
+
}
|
|
4229
|
+
return false;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
/** Whether what was moved off a line — its inline-blocks and images, where
|
|
4233
|
+
* their ink is, and its texts, where they are drawn — reaches the rows
|
|
4234
|
+
* between `top` and `bottom`. */
|
|
4235
|
+
function movedInto(line: LineBox, top: number, bottom: number): boolean {
|
|
4236
|
+
for (const placed of line.atomics) {
|
|
4237
|
+
const box = placed.box;
|
|
4238
|
+
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
4239
|
+
return true;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
for (const text of line.texts) {
|
|
4243
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
4244
|
+
if (!natural) continue;
|
|
4245
|
+
const y = text.drawY + natural.y;
|
|
4246
|
+
if (y < bottom && y + natural.height > top) return true;
|
|
4247
|
+
}
|
|
4248
|
+
return false;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4063
4251
|
/** An inline box's fragment whose padding or border reaches up over the
|
|
4064
4252
|
* lines before its own, and where its own line starts. */
|
|
4065
4253
|
interface Bleed {
|
|
@@ -4091,7 +4279,7 @@ function paintBleeds(
|
|
|
4091
4279
|
ctx.beginPath();
|
|
4092
4280
|
ctx.rect(left, top, Math.ceil(fragment.width) + 2, bottom - top);
|
|
4093
4281
|
ctx.clip();
|
|
4094
|
-
paintLayers(ctx, fragment, options);
|
|
4282
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
4095
4283
|
paintBorders(ctx, fragment, options);
|
|
4096
4284
|
ctx.restore();
|
|
4097
4285
|
}
|
|
@@ -4615,7 +4803,10 @@ function paintInlineBoxes(
|
|
|
4615
4803
|
style,
|
|
4616
4804
|
};
|
|
4617
4805
|
if (fragment.width <= 0) continue;
|
|
4618
|
-
|
|
4806
|
+
// its images from its own padding box, as its gradients are, each
|
|
4807
|
+
// fragment alike: `slice` would lay them out as though the fragments
|
|
4808
|
+
// were one box end to end, which a box on one line is
|
|
4809
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
4619
4810
|
paintBorders(ctx, fragment, options);
|
|
4620
4811
|
if (box.style.outlineStyle !== 'none') paintOutline(ctx, fragment, options);
|
|
4621
4812
|
if (bleeds && fragment.y < line.y - 0.5) {
|
package/src/html/svg.ts
CHANGED
|
@@ -83,24 +83,32 @@ export function svgSizeHint(value: string): string | null {
|
|
|
83
83
|
return m[2] ? v : `${parseFloat(m[1])}px`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
function percentOf(value: string | undefined): number {
|
|
88
|
-
const v = value?.trim();
|
|
89
|
-
if (!v?.endsWith('%')) return 1;
|
|
90
|
-
const n = parseFloat(v);
|
|
91
|
-
return Number.isFinite(n) && n >= 0 ? n / 100 : 1;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function viewBoxOf(el: Element): [number, number, number, number] | null {
|
|
86
|
+
function viewBoxParts(el: Element): [number, number, number, number] | null {
|
|
95
87
|
const parts = (svgAttr(el, 'viewBox') ?? '')
|
|
96
88
|
.trim()
|
|
97
89
|
.split(/[\s,]+/)
|
|
98
90
|
.map(Number);
|
|
99
91
|
if (parts.length !== 4 || !parts.every(Number.isFinite)) return null;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
return [parts[0], parts[1], parts[2], parts[3]];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function viewBoxOf(el: Element): [number, number, number, number] | null {
|
|
96
|
+
const parts = viewBoxParts(el);
|
|
97
|
+
// a zero or negative extent: as though there were none (see `drawsNothing`)
|
|
98
|
+
return parts && parts[2] > 0 && parts[3] > 0 ? parts : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether a `viewBox` with a zero width or height disables the drawing,
|
|
102
|
+
* as SVG has it: a negative one is an error and as though there were no
|
|
103
|
+
* `viewBox`, but a zero one is a drawing of nothing. */
|
|
104
|
+
function drawsNothing(el: Element): boolean {
|
|
105
|
+
const parts = viewBoxParts(el);
|
|
106
|
+
return (
|
|
107
|
+
parts !== null &&
|
|
108
|
+
parts[2] >= 0 &&
|
|
109
|
+
parts[3] >= 0 &&
|
|
110
|
+
(parts[2] === 0 || parts[3] === 0)
|
|
111
|
+
);
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/**
|
|
@@ -210,6 +218,7 @@ export class SvgDrawing {
|
|
|
210
218
|
// the mock backend has no path API, and SvgView draws paths
|
|
211
219
|
if (!ctx.beginPath || !ctx.rect || !ctx.clip || !ctx.fill) return;
|
|
212
220
|
const root = this._root;
|
|
221
|
+
if (drawsNothing(root)) return;
|
|
213
222
|
if (this._standalone) {
|
|
214
223
|
// the root's background is the canvas's, and an image's canvas is
|
|
215
224
|
// all of the rectangle, wherever its viewport and `viewBox` put the
|
|
@@ -222,12 +231,14 @@ export class SvgDrawing {
|
|
|
222
231
|
ctx.fillRect(x, y, w, h);
|
|
223
232
|
ctx.restore();
|
|
224
233
|
}
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
// An image's root fills the rectangle it is drawn into, whatever its
|
|
235
|
+
// own `width` and `height` say: they are what its intrinsic size was
|
|
236
|
+
// read from (`svgIntrinsics`), and the rectangle is the concrete size
|
|
237
|
+
// the image was then given — a background's tile, an `<img>`'s box.
|
|
238
|
+
// Blink sizes an SVG embedded as an image to its container the same
|
|
239
|
+
// way. Drawn as two fifths of it, `width="40%"` left the rest of a
|
|
240
|
+
// `background-size: contain` empty. An inline root's percentages are
|
|
241
|
+
// its box's, already (`svgSizeHint`).
|
|
231
242
|
}
|
|
232
243
|
const box = viewBoxOf(root);
|
|
233
244
|
// the viewport in user units, which a percentage is of
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Code drawn as a column of blocks of lines, under `<Code>` and
|
|
2
|
+
// `<Markdown>`'s fences.
|
|
3
|
+
//
|
|
4
|
+
// One `<richtext>` for a whole block of code is laid out whole whenever any
|
|
5
|
+
// of it changes, and a code block that streams — a model writing a fence a
|
|
6
|
+
// token at a time, a log of source — changes at its end every time: 0.4 s
|
|
7
|
+
// for a line appended to 5,000 in either component. As blocks, a change
|
|
8
|
+
// lays out the blocks it touches. The runs are tokenized afresh on every
|
|
9
|
+
// change, so a block is kept by what it holds rather than by identity: while
|
|
10
|
+
// its runs are the same text in the same styles, its element is the one
|
|
11
|
+
// already drawn.
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import type { ReactElement } from 'react';
|
|
14
|
+
import type { Style } from 'react-x11/style';
|
|
15
|
+
|
|
16
|
+
import { RICHTEXT_ELEMENT } from '../richtext/index.js';
|
|
17
|
+
import type { RichTextProps, TextRun } from '../richtext/index.js';
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
|
|
21
|
+
/** Lines a block of code holds. */
|
|
22
|
+
export const CODE_BLOCK_LINES = 256;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs split into blocks of `lines` whole lines: the text cut at every
|
|
26
|
+
* `lines`th newline, that newline left out, so the blocks stacked are the
|
|
27
|
+
* text again. A newline that ends the whole text stays in its block, where
|
|
28
|
+
* it draws the empty last line it always drew.
|
|
29
|
+
*/
|
|
30
|
+
export function codeBlocks(
|
|
31
|
+
runs: readonly TextRun[],
|
|
32
|
+
lines = CODE_BLOCK_LINES,
|
|
33
|
+
): TextRun[][] {
|
|
34
|
+
let last = runs.length - 1;
|
|
35
|
+
while (last >= 0 && runs[last]!.text === '') last--;
|
|
36
|
+
const blocks: TextRun[][] = [];
|
|
37
|
+
let block: TextRun[] = [];
|
|
38
|
+
let count = 0;
|
|
39
|
+
for (let r = 0; r < runs.length; r++) {
|
|
40
|
+
const run = runs[r]!;
|
|
41
|
+
let from = 0;
|
|
42
|
+
let search = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
const nl = run.text.indexOf('\n', search);
|
|
45
|
+
if (nl < 0) break;
|
|
46
|
+
search = nl + 1;
|
|
47
|
+
if (++count < lines) continue;
|
|
48
|
+
// the text's own last newline is a line of its own, and stays
|
|
49
|
+
if (r === last && nl === run.text.length - 1) break;
|
|
50
|
+
if (nl > from) block.push(piece(run, from, nl));
|
|
51
|
+
blocks.push(block);
|
|
52
|
+
block = [];
|
|
53
|
+
count = 0;
|
|
54
|
+
from = nl + 1;
|
|
55
|
+
}
|
|
56
|
+
if (from < run.text.length) {
|
|
57
|
+
block.push(from === 0 ? run : piece(run, from, run.text.length));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
blocks.push(block);
|
|
61
|
+
return blocks;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function piece(run: TextRun, from: number, to: number): TextRun {
|
|
65
|
+
return { ...run, text: run.text.slice(from, to) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether two runs are the same text in the same style, field for field. */
|
|
69
|
+
function sameRun(a: TextRun, b: TextRun): boolean {
|
|
70
|
+
if (a === b) return true;
|
|
71
|
+
const x = a as unknown as Record<string, unknown>;
|
|
72
|
+
const y = b as unknown as Record<string, unknown>;
|
|
73
|
+
for (const key in x) if (x[key] !== y[key]) return false;
|
|
74
|
+
for (const key in y) if (!(key in x) && y[key] !== undefined) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sameRuns(a: readonly TextRun[], b: readonly TextRun[]): boolean {
|
|
79
|
+
if (a.length !== b.length) return false;
|
|
80
|
+
for (let i = 0; i < a.length; i++) if (!sameRun(a[i]!, b[i]!)) return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sameStyle(a: Style, b: Style): boolean {
|
|
85
|
+
return sameRun(a as unknown as TextRun, b as unknown as TextRun);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CodeLinesProps {
|
|
89
|
+
runs: readonly TextRun[];
|
|
90
|
+
/** Every block's style, compared by value: a style object built afresh
|
|
91
|
+
* each render keeps the blocks. */
|
|
92
|
+
style: Style;
|
|
93
|
+
/** `false` lays each block out unwrapped, as `<richtext>` takes it. */
|
|
94
|
+
wrap?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The blocks' elements from the last render, and what they were built of. */
|
|
98
|
+
interface Kept {
|
|
99
|
+
style: Style;
|
|
100
|
+
wrap: boolean | undefined;
|
|
101
|
+
blocks: TextRun[][];
|
|
102
|
+
elements: ReactElement[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The runs of a block of code, drawn as blocks of lines (`codeBlocks`),
|
|
107
|
+
* each a `<richtext>` kept while its runs are the same. A selection runs
|
|
108
|
+
* across the blocks and copies as one text, since core joins text elements
|
|
109
|
+
* stacked one above the other with a newline.
|
|
110
|
+
*/
|
|
111
|
+
export function CodeLines(props: CodeLinesProps): ReactElement {
|
|
112
|
+
const { style, wrap } = props;
|
|
113
|
+
const kept = React.useRef<Kept | null>(null);
|
|
114
|
+
const was =
|
|
115
|
+
kept.current &&
|
|
116
|
+
kept.current.wrap === wrap &&
|
|
117
|
+
sameStyle(kept.current.style, style)
|
|
118
|
+
? kept.current
|
|
119
|
+
: null;
|
|
120
|
+
const blocks = codeBlocks(props.runs);
|
|
121
|
+
const elements = blocks.map((block, i) => {
|
|
122
|
+
const before = was?.blocks[i];
|
|
123
|
+
if (before && sameRuns(before, block)) {
|
|
124
|
+
blocks[i] = before;
|
|
125
|
+
return was.elements[i]!;
|
|
126
|
+
}
|
|
127
|
+
const blockProps: RichTextProps = { runs: block, style };
|
|
128
|
+
if (wrap === false) blockProps.wrap = false;
|
|
129
|
+
return h(RICHTEXT_ELEMENT, {
|
|
130
|
+
key: i,
|
|
131
|
+
...blockProps,
|
|
132
|
+
} as Record<string, unknown>);
|
|
133
|
+
});
|
|
134
|
+
kept.current = { style, wrap, blocks, elements };
|
|
135
|
+
return h(React.Fragment, null, ...elements);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CodeGutterProps {
|
|
139
|
+
/** Lines in the code beside it. */
|
|
140
|
+
lines: number;
|
|
141
|
+
/** How a number is set: the code's family and size, a dimmer colour. */
|
|
142
|
+
run: Omit<TextRun, 'text'>;
|
|
143
|
+
style: Style;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The line numbers beside `CodeLines`, blocked the same way so that the two
|
|
148
|
+
* stay in register a block at a time, and a number not rebuilt for a line
|
|
149
|
+
* appended far below it. Each number is followed by a newline, the last as
|
|
150
|
+
* well, as the gutter always was.
|
|
151
|
+
*/
|
|
152
|
+
export function CodeGutter(props: CodeGutterProps): ReactElement {
|
|
153
|
+
const { lines, run, style } = props;
|
|
154
|
+
const kept = React.useRef<{
|
|
155
|
+
run: Omit<TextRun, 'text'>;
|
|
156
|
+
style: Style;
|
|
157
|
+
byStart: Map<string, ReactElement>;
|
|
158
|
+
} | null>(null);
|
|
159
|
+
if (
|
|
160
|
+
!kept.current ||
|
|
161
|
+
!sameRun(kept.current.run as TextRun, run as TextRun) ||
|
|
162
|
+
!sameStyle(kept.current.style, style)
|
|
163
|
+
) {
|
|
164
|
+
kept.current = { run, style, byStart: new Map() };
|
|
165
|
+
}
|
|
166
|
+
const byStart = kept.current.byStart;
|
|
167
|
+
const elements: ReactElement[] = [];
|
|
168
|
+
const used = new Set<string>();
|
|
169
|
+
for (let start = 0; start < lines || start === 0; start += CODE_BLOCK_LINES) {
|
|
170
|
+
const end = Math.min(start + CODE_BLOCK_LINES, lines);
|
|
171
|
+
const key = `${start}:${end}`;
|
|
172
|
+
used.add(key);
|
|
173
|
+
let element = byStart.get(key);
|
|
174
|
+
if (!element) {
|
|
175
|
+
const runs: TextRun[] = [];
|
|
176
|
+
for (let i = start; i < end; i++) {
|
|
177
|
+
runs.push({ ...run, text: `${i + 1}\n` } as TextRun);
|
|
178
|
+
}
|
|
179
|
+
// the newline that ends a block is the gap to the next one
|
|
180
|
+
if (end < lines && runs.length > 0) {
|
|
181
|
+
const tail = runs[runs.length - 1]!;
|
|
182
|
+
runs[runs.length - 1] = { ...tail, text: tail.text.slice(0, -1) };
|
|
183
|
+
}
|
|
184
|
+
element = h(RICHTEXT_ELEMENT, {
|
|
185
|
+
key: start,
|
|
186
|
+
// The numbering is chrome, not text: `selectable={false}` keeps it
|
|
187
|
+
// out of a drag and out of the copied text.
|
|
188
|
+
selectable: false,
|
|
189
|
+
runs,
|
|
190
|
+
wrap: false,
|
|
191
|
+
style,
|
|
192
|
+
} as Record<string, unknown>);
|
|
193
|
+
byStart.set(key, element);
|
|
194
|
+
}
|
|
195
|
+
elements.push(element);
|
|
196
|
+
if (lines === 0) break;
|
|
197
|
+
}
|
|
198
|
+
for (const key of byStart.keys()) if (!used.has(key)) byStart.delete(key);
|
|
199
|
+
return h(React.Fragment, null, ...elements);
|
|
200
|
+
}
|