@markdy/renderer-dom 0.8.26 → 0.8.28
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/index.d.ts +17 -1
- package/dist/index.js +167 -12
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ interface DiagramOptions {
|
|
|
33
33
|
onWarning?: (warning: Diagnostic) => void;
|
|
34
34
|
onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
|
|
35
35
|
onPlayStateChange?: (playing: boolean) => void;
|
|
36
|
+
onEnded?: () => void;
|
|
36
37
|
}
|
|
37
38
|
interface Diagram {
|
|
38
39
|
play(): void;
|
|
@@ -81,6 +82,21 @@ interface SvgExportOptions {
|
|
|
81
82
|
}
|
|
82
83
|
declare function exportDiagramAsVectorSvg(containerEl: HTMLElement, options?: SvgExportOptions): string;
|
|
83
84
|
|
|
85
|
+
interface TimelineController {
|
|
86
|
+
seek(seconds: number): void;
|
|
87
|
+
currentTime(): number;
|
|
88
|
+
duration(): number;
|
|
89
|
+
isPlaying(): boolean;
|
|
90
|
+
play(): void;
|
|
91
|
+
pause(): void;
|
|
92
|
+
}
|
|
93
|
+
interface GifDiagramExportOptions extends SvgExportOptions {
|
|
94
|
+
fps?: number;
|
|
95
|
+
pixelRatio?: number;
|
|
96
|
+
loop?: boolean;
|
|
97
|
+
}
|
|
98
|
+
declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
|
|
99
|
+
|
|
84
100
|
/**
|
|
85
101
|
* packages/renderer-dom/src/export/png-exporter.ts
|
|
86
102
|
* High-DPI raster PNG export with 2x retina scaling.
|
|
@@ -119,4 +135,4 @@ declare class DiagramPresentationController {
|
|
|
119
135
|
destroy(): void;
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, createDiagram, encodeGifSequence, exportDiagramAsPng, exportDiagramAsVectorSvg };
|
|
138
|
+
export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifDiagramExportOptions, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, type TimelineController, createDiagram, encodeGifSequence, exportDiagramAsGif, exportDiagramAsPng, exportDiagramAsVectorSvg };
|
package/dist/index.js
CHANGED
|
@@ -808,13 +808,14 @@ function ensureAnnotationStyles(doc) {
|
|
|
808
808
|
doc.head.appendChild(style);
|
|
809
809
|
}
|
|
810
810
|
function positionForAnnotation(position, bounds, index) {
|
|
811
|
-
const pad =
|
|
811
|
+
const pad = 28;
|
|
812
|
+
const topPad = 68;
|
|
812
813
|
const p = (position ?? "").toLowerCase();
|
|
813
|
-
if (p.includes("top") && p.includes("right")) return { x: bounds.width - pad - 200, y:
|
|
814
|
-
if (p.includes("top") && p.includes("left")) return { x: pad, y:
|
|
814
|
+
if (p.includes("top") && p.includes("right")) return { x: bounds.width - pad - 200, y: topPad + index * 48 };
|
|
815
|
+
if (p.includes("top") && p.includes("left")) return { x: pad, y: topPad + index * 48 };
|
|
815
816
|
if (p.includes("bottom") && p.includes("right")) return { x: bounds.width - pad - 200, y: bounds.height - pad - 40 };
|
|
816
817
|
if (p.includes("bottom") && p.includes("left")) return { x: pad, y: bounds.height - pad - 40 };
|
|
817
|
-
return { x: bounds.width - pad - 200, y:
|
|
818
|
+
return { x: bounds.width - pad - 200, y: topPad + index * 48 };
|
|
818
819
|
}
|
|
819
820
|
function mountAnnotations(layer, annotations, nodes, theme, bounds) {
|
|
820
821
|
if (annotations.length === 0) return;
|
|
@@ -2090,7 +2091,8 @@ function createDiagram(opts) {
|
|
|
2090
2091
|
copyright: explicitCopyright,
|
|
2091
2092
|
onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
|
|
2092
2093
|
onTimeUpdate,
|
|
2093
|
-
onPlayStateChange
|
|
2094
|
+
onPlayStateChange,
|
|
2095
|
+
onEnded
|
|
2094
2096
|
} = opts;
|
|
2095
2097
|
const { ast, plan } = parseAndCompile(code);
|
|
2096
2098
|
for (const w of ast.diagnostics) {
|
|
@@ -2110,11 +2112,16 @@ function createDiagram(opts) {
|
|
|
2110
2112
|
const totalDurationMs = plan.duration * 1e3;
|
|
2111
2113
|
const durationSeconds = plan.duration;
|
|
2112
2114
|
const viewport = document.createElement("div");
|
|
2115
|
+
viewport.className = "markdy-viewport";
|
|
2113
2116
|
Object.assign(viewport.style, {
|
|
2114
2117
|
position: "relative",
|
|
2115
2118
|
width: "100%",
|
|
2119
|
+
height: "100%",
|
|
2120
|
+
maxWidth: "100%",
|
|
2121
|
+
maxHeight: "100%",
|
|
2116
2122
|
aspectRatio: `${plan.meta.width} / ${plan.meta.height}`,
|
|
2117
|
-
overflow: "hidden"
|
|
2123
|
+
overflow: "hidden",
|
|
2124
|
+
boxSizing: "border-box"
|
|
2118
2125
|
});
|
|
2119
2126
|
container.appendChild(viewport);
|
|
2120
2127
|
let progressEl = null;
|
|
@@ -2269,14 +2276,85 @@ function createDiagram(opts) {
|
|
|
2269
2276
|
nodeEls.set(node.id, el);
|
|
2270
2277
|
}
|
|
2271
2278
|
let fitScale = 1;
|
|
2279
|
+
let sceneOffsetX = 0;
|
|
2280
|
+
let sceneOffsetY = 0;
|
|
2281
|
+
function computeContentBounds() {
|
|
2282
|
+
if (!plan.nodes || plan.nodes.length === 0) {
|
|
2283
|
+
return { minX: 0, minY: 0, maxX: plan.meta.width, maxY: plan.meta.height, width: plan.meta.width, height: plan.meta.height };
|
|
2284
|
+
}
|
|
2285
|
+
let minX = Infinity;
|
|
2286
|
+
let minY = Infinity;
|
|
2287
|
+
let maxX = -Infinity;
|
|
2288
|
+
let maxY = -Infinity;
|
|
2289
|
+
for (const node of plan.nodes) {
|
|
2290
|
+
minX = Math.min(minX, node.x);
|
|
2291
|
+
minY = Math.min(minY, node.y);
|
|
2292
|
+
maxX = Math.max(maxX, node.x + node.width);
|
|
2293
|
+
maxY = Math.max(maxY, node.y + node.height);
|
|
2294
|
+
}
|
|
2295
|
+
for (const gb of plan.groupBoundaries ?? []) {
|
|
2296
|
+
minX = Math.min(minX, gb.x);
|
|
2297
|
+
minY = Math.min(minY, gb.y);
|
|
2298
|
+
maxX = Math.max(maxX, gb.x + gb.width);
|
|
2299
|
+
maxY = Math.max(maxY, gb.y + gb.height);
|
|
2300
|
+
}
|
|
2301
|
+
for (const bus of plan.treeBuses ?? []) {
|
|
2302
|
+
minX = Math.min(minX, bus.parentX, ...bus.childXs.length ? bus.childXs : [bus.parentX]);
|
|
2303
|
+
minY = Math.min(minY, bus.parentY, bus.branchY, bus.childY);
|
|
2304
|
+
maxX = Math.max(maxX, bus.parentX, ...bus.childXs.length ? bus.childXs : [bus.parentX]);
|
|
2305
|
+
maxY = Math.max(maxY, bus.parentY, bus.branchY, bus.childY);
|
|
2306
|
+
}
|
|
2307
|
+
if (plan.title) {
|
|
2308
|
+
minY = Math.min(minY, 20);
|
|
2309
|
+
}
|
|
2310
|
+
const pad = 36;
|
|
2311
|
+
minX = Math.max(0, minX - pad);
|
|
2312
|
+
minY = Math.max(0, minY - pad);
|
|
2313
|
+
maxX = Math.min(plan.meta.width, maxX + pad);
|
|
2314
|
+
maxY = Math.min(plan.meta.height, maxY + pad);
|
|
2315
|
+
const width = Math.max(maxX - minX, 200);
|
|
2316
|
+
const height = Math.max(maxY - minY, 140);
|
|
2317
|
+
return { minX, minY, maxX, maxY, width, height };
|
|
2318
|
+
}
|
|
2272
2319
|
function scaleScene() {
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2320
|
+
const isBrowserLayout = (viewport.clientWidth || container.clientWidth || 0) > 0;
|
|
2321
|
+
if (!isBrowserLayout) {
|
|
2322
|
+
fitScale = 1;
|
|
2323
|
+
sceneOffsetX = 0;
|
|
2324
|
+
sceneOffsetY = 0;
|
|
2325
|
+
scene.style.left = "0px";
|
|
2326
|
+
scene.style.top = "0px";
|
|
2327
|
+
scene.style.transformOrigin = "0 0";
|
|
2328
|
+
scene.style.transform = "scale(1)";
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
const vWidth = viewport.clientWidth || container.clientWidth || plan.meta.width;
|
|
2332
|
+
const vHeight = viewport.clientHeight || container.clientHeight || vWidth * plan.meta.height / plan.meta.width;
|
|
2333
|
+
const canvasScaleX = vWidth / plan.meta.width;
|
|
2334
|
+
const canvasScaleY = vHeight / plan.meta.height;
|
|
2335
|
+
const baseCanvasScale = Math.min(canvasScaleX, canvasScaleY);
|
|
2336
|
+
const bounds = computeContentBounds();
|
|
2337
|
+
const contentScaleX = vWidth * 0.94 / bounds.width;
|
|
2338
|
+
const contentScaleY = vHeight * 0.94 / bounds.height;
|
|
2339
|
+
const optimalContentScale = Math.min(contentScaleX, contentScaleY);
|
|
2340
|
+
const chosenScale = Math.max(
|
|
2341
|
+
baseCanvasScale,
|
|
2342
|
+
Math.min(optimalContentScale, baseCanvasScale * 1.45)
|
|
2343
|
+
);
|
|
2344
|
+
fitScale = Number.isFinite(chosenScale) && chosenScale > 0 ? chosenScale : 1;
|
|
2345
|
+
const scaledWidth = plan.meta.width * fitScale;
|
|
2346
|
+
const scaledHeight = plan.meta.height * fitScale;
|
|
2347
|
+
sceneOffsetX = (vWidth - scaledWidth) / 2;
|
|
2348
|
+
sceneOffsetY = (vHeight - scaledHeight) / 2;
|
|
2349
|
+
scene.style.left = `${sceneOffsetX}px`;
|
|
2350
|
+
scene.style.top = `${sceneOffsetY}px`;
|
|
2351
|
+
scene.style.transformOrigin = "0 0";
|
|
2275
2352
|
scene.style.transform = `scale(${fitScale})`;
|
|
2276
2353
|
}
|
|
2277
2354
|
scaleScene();
|
|
2278
2355
|
const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(scaleScene) : null;
|
|
2279
2356
|
resizeObserver?.observe(viewport);
|
|
2357
|
+
if (container !== viewport) resizeObserver?.observe(container);
|
|
2280
2358
|
const edgeRuntimes = /* @__PURE__ */ new Map();
|
|
2281
2359
|
const edgeSceneId = createEdgeSceneId();
|
|
2282
2360
|
const sequenceAnims = plan.diagramType === "sequence" ? mountSequenceLayer(
|
|
@@ -2341,8 +2419,8 @@ function createDiagram(opts) {
|
|
|
2341
2419
|
function handleViewportWheel(event) {
|
|
2342
2420
|
event.preventDefault();
|
|
2343
2421
|
const rect = viewport.getBoundingClientRect();
|
|
2344
|
-
const pointerX = (event.clientX - rect.left) / fitScale;
|
|
2345
|
-
const pointerY = (event.clientY - rect.top) / fitScale;
|
|
2422
|
+
const pointerX = (event.clientX - rect.left - sceneOffsetX) / fitScale;
|
|
2423
|
+
const pointerY = (event.clientY - rect.top - sceneOffsetY) / fitScale;
|
|
2346
2424
|
const nextScale = Math.min(MAX_VIEWPORT_ZOOM, Math.max(MIN_VIEWPORT_ZOOM, viewportScale * Math.exp(-event.deltaY * VIEWPORT_ZOOM_STEP)));
|
|
2347
2425
|
if (nextScale === viewportScale) return;
|
|
2348
2426
|
const sceneX = (pointerX - viewportPanX) / viewportScale;
|
|
@@ -2426,6 +2504,7 @@ function createDiagram(opts) {
|
|
|
2426
2504
|
emitPlayStateChange(false);
|
|
2427
2505
|
lastRafTs = null;
|
|
2428
2506
|
rafId = null;
|
|
2507
|
+
onEnded?.();
|
|
2429
2508
|
return;
|
|
2430
2509
|
}
|
|
2431
2510
|
}
|
|
@@ -2784,6 +2863,19 @@ function encodeGifSequence(frames, options = {}) {
|
|
|
2784
2863
|
}
|
|
2785
2864
|
|
|
2786
2865
|
// src/export/svg-exporter.ts
|
|
2866
|
+
function copyRenderedStyles(source, clone) {
|
|
2867
|
+
if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
|
|
2868
|
+
const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
2869
|
+
const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
2870
|
+
for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
|
|
2871
|
+
const computed = window.getComputedStyle(sourceElements[index]);
|
|
2872
|
+
const target = cloneElements[index].style;
|
|
2873
|
+
for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
|
|
2874
|
+
const property = computed.item(propertyIndex);
|
|
2875
|
+
target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2787
2879
|
function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
2788
2880
|
const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
|
|
2789
2881
|
if (!sceneEl) throw new Error("No Markdy scene element found in container");
|
|
@@ -2797,6 +2889,7 @@ function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
|
2797
2889
|
${serializer2.serializeToString(clonedSvg)}`;
|
|
2798
2890
|
}
|
|
2799
2891
|
const clonedScene = sceneEl.cloneNode(true);
|
|
2892
|
+
copyRenderedStyles(sceneEl, clonedScene);
|
|
2800
2893
|
const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
|
|
2801
2894
|
const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
|
|
2802
2895
|
let width = parseFloat(widthStr);
|
|
@@ -2809,6 +2902,8 @@ ${serializer2.serializeToString(clonedSvg)}`;
|
|
|
2809
2902
|
clonedScene.style.transform = `scale(${scale})`;
|
|
2810
2903
|
clonedScene.style.transformOrigin = "0 0";
|
|
2811
2904
|
clonedScene.style.position = "relative";
|
|
2905
|
+
clonedScene.style.left = "0px";
|
|
2906
|
+
clonedScene.style.top = "0px";
|
|
2812
2907
|
clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
2813
2908
|
if (options.transparentBackground) {
|
|
2814
2909
|
clonedScene.style.background = "transparent";
|
|
@@ -2844,6 +2939,60 @@ ${serializer2.serializeToString(clonedSvg)}`;
|
|
|
2844
2939
|
` + serializer.serializeToString(svg);
|
|
2845
2940
|
}
|
|
2846
2941
|
|
|
2942
|
+
// src/export/gif-exporter.ts
|
|
2943
|
+
var nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
2944
|
+
async function rasterizeFrame(container, pixelRatio, options) {
|
|
2945
|
+
const svg = exportDiagramAsVectorSvg(container, options);
|
|
2946
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
2947
|
+
try {
|
|
2948
|
+
const image = new Image();
|
|
2949
|
+
await new Promise((resolve, reject) => {
|
|
2950
|
+
image.onload = () => resolve();
|
|
2951
|
+
image.onerror = () => reject(new Error("Failed to rasterize GIF frame"));
|
|
2952
|
+
image.src = url;
|
|
2953
|
+
});
|
|
2954
|
+
const canvas = document.createElement("canvas");
|
|
2955
|
+
canvas.width = image.naturalWidth * pixelRatio;
|
|
2956
|
+
canvas.height = image.naturalHeight * pixelRatio;
|
|
2957
|
+
const context = canvas.getContext("2d");
|
|
2958
|
+
if (!context) throw new Error("Could not create GIF canvas context");
|
|
2959
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
2960
|
+
return context.getImageData(0, 0, canvas.width, canvas.height);
|
|
2961
|
+
} finally {
|
|
2962
|
+
URL.revokeObjectURL(url);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
async function exportDiagramAsGif(container, timeline, options = {}) {
|
|
2966
|
+
const fps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
|
|
2967
|
+
const duration = timeline.duration();
|
|
2968
|
+
const frameCount = Math.max(1, Math.ceil(duration * fps));
|
|
2969
|
+
const delayMs = Math.max(20, Math.round(1e3 / fps));
|
|
2970
|
+
const priorTime = timeline.currentTime();
|
|
2971
|
+
const wasPlaying = timeline.isPlaying();
|
|
2972
|
+
timeline.pause();
|
|
2973
|
+
try {
|
|
2974
|
+
const frames = [];
|
|
2975
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
2976
|
+
timeline.seek(Math.min(duration, frame / fps));
|
|
2977
|
+
await nextFrame();
|
|
2978
|
+
frames.push({
|
|
2979
|
+
imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options),
|
|
2980
|
+
delayMs
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
timeline.seek(duration);
|
|
2984
|
+
await nextFrame();
|
|
2985
|
+
frames.push({ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options), delayMs: Math.max(delayMs, 800) });
|
|
2986
|
+
const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
|
|
2987
|
+
const bytes = new Uint8Array(encoded.byteLength);
|
|
2988
|
+
bytes.set(encoded);
|
|
2989
|
+
return new Blob([bytes.buffer], { type: "image/gif" });
|
|
2990
|
+
} finally {
|
|
2991
|
+
timeline.seek(priorTime);
|
|
2992
|
+
if (wasPlaying) timeline.play();
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2847
2996
|
// src/export/png-exporter.ts
|
|
2848
2997
|
async function exportDiagramAsPng(containerEl, options = {}) {
|
|
2849
2998
|
const svgXml = exportDiagramAsVectorSvg(containerEl, options);
|
|
@@ -2853,7 +3002,10 @@ async function exportDiagramAsPng(containerEl, options = {}) {
|
|
|
2853
3002
|
const img = new Image();
|
|
2854
3003
|
await new Promise((resolve, reject) => {
|
|
2855
3004
|
img.onload = () => resolve();
|
|
2856
|
-
img.onerror = () =>
|
|
3005
|
+
img.onerror = () => {
|
|
3006
|
+
URL.revokeObjectURL(url);
|
|
3007
|
+
reject(new Error("Failed to rasterize SVG into Image for PNG export"));
|
|
3008
|
+
};
|
|
2857
3009
|
img.src = url;
|
|
2858
3010
|
});
|
|
2859
3011
|
const width = img.naturalWidth || 800;
|
|
@@ -2925,7 +3077,9 @@ var DiagramPresentationController = class {
|
|
|
2925
3077
|
this.diagram.setPlaybackRate(rate);
|
|
2926
3078
|
}
|
|
2927
3079
|
onKeyDown = (e) => {
|
|
2928
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
3080
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement || e.target?.isContentEditable) {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
2929
3083
|
switch (e.key) {
|
|
2930
3084
|
case "ArrowRight":
|
|
2931
3085
|
case "PageDown":
|
|
@@ -2960,6 +3114,7 @@ export {
|
|
|
2960
3114
|
ICON_REGISTRY,
|
|
2961
3115
|
createDiagram,
|
|
2962
3116
|
encodeGifSequence,
|
|
3117
|
+
exportDiagramAsGif,
|
|
2963
3118
|
exportDiagramAsPng,
|
|
2964
3119
|
exportDiagramAsVectorSvg
|
|
2965
3120
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.28",
|
|
4
4
|
"description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@markdy/core": "0.8.
|
|
47
|
+
"@markdy/core": "0.8.28"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"jsdom": "^29.1.1",
|
|
51
51
|
"tsup": "^8.5.1",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
53
|
"vitest": "^4.1.7",
|
|
54
|
-
"@markdy/stdlib-systems": "0.8.
|
|
54
|
+
"@markdy/stdlib-systems": "0.8.28"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup",
|