@texuli/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/editor/document.js +8 -1
- package/dist/editor/extensions.d.ts +64 -0
- package/dist/editor/extensions.js +116 -0
- package/dist/editor/inline.d.ts +5 -3
- package/dist/editor/inline.js +24 -12
- package/dist/editor/load.d.ts +10 -0
- package/dist/editor/load.js +16 -0
- package/dist/editor/markdown.d.ts +10 -1
- package/dist/editor/markdown.js +193 -83
- package/dist/editor/model.d.ts +3 -1
- package/dist/editor/model.js +3 -1
- package/dist/editor/transforms.d.ts +1 -1
- package/dist/editor/tree.d.ts +11 -1
- package/dist/editor/tree.js +11 -2
- package/dist/editor/types.d.ts +10 -0
- package/dist/editor/types.js +1 -0
- package/dist/inert/inert.d.ts +11 -0
- package/dist/inert/inert.js +5 -3
- package/dist/markup/index.d.ts +5 -0
- package/dist/markup/index.js +5 -0
- package/dist/markup/markup.d.ts +72 -0
- package/dist/markup/markup.js +233 -0
- package/dist/motion/animate.d.ts +109 -0
- package/dist/motion/animate.js +225 -0
- package/dist/motion/flip.d.ts +58 -0
- package/dist/motion/flip.js +167 -0
- package/dist/motion/index.d.ts +15 -0
- package/dist/motion/index.js +15 -0
- package/dist/motion/preferences.d.ts +68 -0
- package/dist/motion/preferences.js +111 -0
- package/dist/motion/presence.d.ts +61 -0
- package/dist/motion/presence.js +231 -0
- package/dist/motion/sequence.d.ts +70 -0
- package/dist/motion/sequence.js +159 -0
- package/dist/motion/shared.d.ts +45 -0
- package/dist/motion/shared.js +144 -0
- package/dist/motion/spring.d.ts +109 -0
- package/dist/motion/spring.js +166 -0
- package/dist/overlay/core.js +14 -3
- package/dist/zip/index.d.ts +5 -0
- package/dist/zip/index.js +5 -0
- package/dist/zip/zip.d.ts +35 -0
- package/dist/zip/zip.js +190 -0
- package/package.json +6 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring: a pure damped harmonic oscillator solver (ADR-013).
|
|
3
|
+
*
|
|
4
|
+
* Framework-free and DOM-free. The solver moves a normalized progress from 0
|
|
5
|
+
* to 1 with the closed form for every damping case (under, critical and over
|
|
6
|
+
* damped), finds when the spring settles, samples it into keyframe offsets or
|
|
7
|
+
* a CSS `linear()` easing, and reports position and velocity at any time so an
|
|
8
|
+
* interrupted animation can be retargeted without a velocity jump.
|
|
9
|
+
*
|
|
10
|
+
* Hostile inputs are clamped: stiffness, damping, mass, velocity and sample
|
|
11
|
+
* counts have fixed ranges, NaN falls back to the defaults, and the settle
|
|
12
|
+
* search never evaluates more than `MAX_SPRING_MS + 1` points.
|
|
13
|
+
*/
|
|
14
|
+
/** Physical spring parameters (unitless, like the CSS-in-JS spring convention). */
|
|
15
|
+
export type SpringParams = {
|
|
16
|
+
/** Spring constant `k`. Clamped to [1, 10000]. */
|
|
17
|
+
readonly stiffness: number;
|
|
18
|
+
/** Damping coefficient `c`. Clamped to [0, 1000]. */
|
|
19
|
+
readonly damping: number;
|
|
20
|
+
/** Mass `m`. Clamped to [0.01, 1000]. */
|
|
21
|
+
readonly mass: number;
|
|
22
|
+
};
|
|
23
|
+
/** Options shared by the solver helpers. */
|
|
24
|
+
export type SpringSolveOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Initial velocity in progress units per second (1 = the whole distance per
|
|
27
|
+
* second). Clamped to [-1000, 1000]. Default 0.
|
|
28
|
+
*/
|
|
29
|
+
velocity?: number;
|
|
30
|
+
/** Settled when |1 − progress| is below this. Default 0.001. */
|
|
31
|
+
restDelta?: number;
|
|
32
|
+
/** Settled when |velocity| (progress per second) is below this. Default 0.01. */
|
|
33
|
+
restSpeed?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Position and velocity of the spring at one time. */
|
|
36
|
+
export type SpringState = {
|
|
37
|
+
/** Normalized progress (0 at rest start, 1 at the target; may overshoot). */
|
|
38
|
+
readonly position: number;
|
|
39
|
+
/** Progress units per second. */
|
|
40
|
+
readonly velocity: number;
|
|
41
|
+
};
|
|
42
|
+
/** Settle result. */
|
|
43
|
+
export type SpringSettle = {
|
|
44
|
+
/** Milliseconds until the spring stays within rest thresholds, at most `MAX_SPRING_MS`. */
|
|
45
|
+
readonly durationMs: number;
|
|
46
|
+
/** False when the spring was still moving at `MAX_SPRING_MS` (the last frame snaps to 1). */
|
|
47
|
+
readonly settled: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Sampled spring curve. */
|
|
50
|
+
export type SpringSamples = SpringSettle & {
|
|
51
|
+
/** Progress values at evenly spaced times from 0 to `durationMs`; first 0, last 1. */
|
|
52
|
+
readonly points: readonly number[];
|
|
53
|
+
};
|
|
54
|
+
/** Longest spring (and any Texuli motion) duration, in milliseconds. */
|
|
55
|
+
export declare const MAX_SPRING_MS = 1000;
|
|
56
|
+
/** Default sample count for keyframes and `linear()` easings. */
|
|
57
|
+
export declare const DEFAULT_SPRING_SAMPLES = 40;
|
|
58
|
+
/** Largest sample count accepted (keeps `linear()` strings short). */
|
|
59
|
+
export declare const MAX_SPRING_SAMPLES = 120;
|
|
60
|
+
/** Spring used when parameters are missing or not finite. */
|
|
61
|
+
export declare const DEFAULT_SPRING: SpringParams;
|
|
62
|
+
/** Clamp spring parameters into the supported ranges; NaN takes the default. */
|
|
63
|
+
export declare function normalizeSpring(params: Partial<SpringParams> | null | undefined): SpringParams;
|
|
64
|
+
/** Damping ratio ζ = c / (2√(k·m)); below 1 overshoots, 1 is critical. */
|
|
65
|
+
export declare function dampingRatio(params: Partial<SpringParams>): number;
|
|
66
|
+
/** Position and velocity `ms` milliseconds after release (closed form, O(1)). */
|
|
67
|
+
export declare function springState(params: Partial<SpringParams>, ms: number, options?: SpringSolveOptions): SpringState;
|
|
68
|
+
/**
|
|
69
|
+
* When the spring comes to rest: the first millisecond after which every
|
|
70
|
+
* sampled millisecond stays inside the rest thresholds. Bounded: at most
|
|
71
|
+
* `MAX_SPRING_MS + 1` closed-form evaluations.
|
|
72
|
+
*/
|
|
73
|
+
export declare function springSettle(params: Partial<SpringParams>, options?: SpringSolveOptions): SpringSettle;
|
|
74
|
+
/**
|
|
75
|
+
* Sample the spring at evenly spaced times across its settle duration. The
|
|
76
|
+
* first point is the start position (0) and the last is pinned to 1, so a
|
|
77
|
+
* spring cut at `MAX_SPRING_MS` lands exactly on its target.
|
|
78
|
+
*/
|
|
79
|
+
export declare function springSamples(params: Partial<SpringParams>, options?: SpringSolveOptions & {
|
|
80
|
+
samples?: number;
|
|
81
|
+
}): SpringSamples;
|
|
82
|
+
/**
|
|
83
|
+
* A CSS `linear()` easing that traces the spring, with its duration. Points
|
|
84
|
+
* are evenly spaced, so no stop percentages are needed.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const { easing, durationMs } = springEasing({ stiffness: 400, damping: 30, mass: 1 });
|
|
88
|
+
* element.animate(keyframes, { duration: durationMs, easing });
|
|
89
|
+
*/
|
|
90
|
+
export declare function springEasing(params: Partial<SpringParams>, options?: SpringSolveOptions & {
|
|
91
|
+
samples?: number;
|
|
92
|
+
}): SpringSettle & {
|
|
93
|
+
readonly easing: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Keyframes for any property: `build(progress)` maps a spring position to a
|
|
97
|
+
* keyframe (for example a transform). Offsets are evenly spaced.
|
|
98
|
+
*/
|
|
99
|
+
export declare function springKeyframes(params: Partial<SpringParams>, build: (progress: number) => Keyframe, options?: SpringSolveOptions & {
|
|
100
|
+
samples?: number;
|
|
101
|
+
}): SpringSettle & {
|
|
102
|
+
readonly keyframes: Keyframe[];
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Velocity for a retargeted spring: a velocity measured in the old segment's
|
|
106
|
+
* progress units, converted to the new segment (`newSpan` / `oldSpan` are the
|
|
107
|
+
* distances in any shared unit). Zero or non-finite spans give 0.
|
|
108
|
+
*/
|
|
109
|
+
export declare function retargetVelocity(velocity: number, oldSpan: number, newSpan: number): number;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring: a pure damped harmonic oscillator solver (ADR-013).
|
|
3
|
+
*
|
|
4
|
+
* Framework-free and DOM-free. The solver moves a normalized progress from 0
|
|
5
|
+
* to 1 with the closed form for every damping case (under, critical and over
|
|
6
|
+
* damped), finds when the spring settles, samples it into keyframe offsets or
|
|
7
|
+
* a CSS `linear()` easing, and reports position and velocity at any time so an
|
|
8
|
+
* interrupted animation can be retargeted without a velocity jump.
|
|
9
|
+
*
|
|
10
|
+
* Hostile inputs are clamped: stiffness, damping, mass, velocity and sample
|
|
11
|
+
* counts have fixed ranges, NaN falls back to the defaults, and the settle
|
|
12
|
+
* search never evaluates more than `MAX_SPRING_MS + 1` points.
|
|
13
|
+
*/
|
|
14
|
+
/** Longest spring (and any Texuli motion) duration, in milliseconds. */
|
|
15
|
+
export const MAX_SPRING_MS = 1000;
|
|
16
|
+
/** Default sample count for keyframes and `linear()` easings. */
|
|
17
|
+
export const DEFAULT_SPRING_SAMPLES = 40;
|
|
18
|
+
/** Largest sample count accepted (keeps `linear()` strings short). */
|
|
19
|
+
export const MAX_SPRING_SAMPLES = 120;
|
|
20
|
+
/** Spring used when parameters are missing or not finite. */
|
|
21
|
+
export const DEFAULT_SPRING = Object.freeze({
|
|
22
|
+
stiffness: 170,
|
|
23
|
+
damping: 26,
|
|
24
|
+
mass: 1,
|
|
25
|
+
});
|
|
26
|
+
function clamp(value, min, max, fallback) {
|
|
27
|
+
if (typeof value !== 'number' || Number.isNaN(value))
|
|
28
|
+
return fallback;
|
|
29
|
+
return Math.min(max, Math.max(min, value));
|
|
30
|
+
}
|
|
31
|
+
/** Clamp spring parameters into the supported ranges; NaN takes the default. */
|
|
32
|
+
export function normalizeSpring(params) {
|
|
33
|
+
const p = params ?? {};
|
|
34
|
+
return {
|
|
35
|
+
stiffness: clamp(p.stiffness ?? DEFAULT_SPRING.stiffness, 1, 10000, DEFAULT_SPRING.stiffness),
|
|
36
|
+
damping: clamp(p.damping ?? DEFAULT_SPRING.damping, 0, 1000, DEFAULT_SPRING.damping),
|
|
37
|
+
mass: clamp(p.mass ?? DEFAULT_SPRING.mass, 0.01, 1000, DEFAULT_SPRING.mass),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Damping ratio ζ = c / (2√(k·m)); below 1 overshoots, 1 is critical. */
|
|
41
|
+
export function dampingRatio(params) {
|
|
42
|
+
const { stiffness, damping, mass } = normalizeSpring(params);
|
|
43
|
+
return damping / (2 * Math.sqrt(stiffness * mass));
|
|
44
|
+
}
|
|
45
|
+
function solver(params, velocity) {
|
|
46
|
+
const { stiffness, damping, mass } = normalizeSpring(params);
|
|
47
|
+
const v0 = clamp(velocity, -1000, 1000, 0);
|
|
48
|
+
const w0 = Math.sqrt(stiffness / mass);
|
|
49
|
+
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
|
|
50
|
+
// Displacement y = position − 1, so y(0) = −1 and y'(0) = v0.
|
|
51
|
+
const a = -1;
|
|
52
|
+
if (Math.abs(zeta - 1) < 1e-6) {
|
|
53
|
+
const b = v0 + w0 * a;
|
|
54
|
+
return (t) => {
|
|
55
|
+
const e = Math.exp(-w0 * t);
|
|
56
|
+
return { position: 1 + (a + b * t) * e, velocity: e * (b - w0 * (a + b * t)) };
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (zeta < 1) {
|
|
60
|
+
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
61
|
+
const decay = zeta * w0;
|
|
62
|
+
const b = (v0 + decay * a) / wd;
|
|
63
|
+
return (t) => {
|
|
64
|
+
const e = Math.exp(-decay * t);
|
|
65
|
+
const cos = Math.cos(wd * t);
|
|
66
|
+
const sin = Math.sin(wd * t);
|
|
67
|
+
return {
|
|
68
|
+
position: 1 + e * (a * cos + b * sin),
|
|
69
|
+
velocity: e * ((-decay * a + wd * b) * cos + (-decay * b - wd * a) * sin),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const root = Math.sqrt(zeta * zeta - 1);
|
|
74
|
+
const r1 = -w0 * (zeta - root);
|
|
75
|
+
const r2 = -w0 * (zeta + root);
|
|
76
|
+
const c2 = (v0 - r1 * a) / (r2 - r1);
|
|
77
|
+
const c1 = a - c2;
|
|
78
|
+
return (t) => {
|
|
79
|
+
const e1 = Math.exp(r1 * t);
|
|
80
|
+
const e2 = Math.exp(r2 * t);
|
|
81
|
+
return { position: 1 + c1 * e1 + c2 * e2, velocity: r1 * c1 * e1 + r2 * c2 * e2 };
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Position and velocity `ms` milliseconds after release (closed form, O(1)). */
|
|
85
|
+
export function springState(params, ms, options = {}) {
|
|
86
|
+
const t = clamp(ms, 0, Number.MAX_SAFE_INTEGER, 0) / 1000;
|
|
87
|
+
return solver(params, options.velocity ?? 0)(t);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* When the spring comes to rest: the first millisecond after which every
|
|
91
|
+
* sampled millisecond stays inside the rest thresholds. Bounded: at most
|
|
92
|
+
* `MAX_SPRING_MS + 1` closed-form evaluations.
|
|
93
|
+
*/
|
|
94
|
+
export function springSettle(params, options = {}) {
|
|
95
|
+
const solve = solver(params, options.velocity ?? 0);
|
|
96
|
+
const restDelta = clamp(options.restDelta ?? 0.001, 1e-6, 0.5, 0.001);
|
|
97
|
+
const restSpeed = clamp(options.restSpeed ?? 0.01, 1e-6, 100, 0.01);
|
|
98
|
+
let lastMoving = -1;
|
|
99
|
+
for (let ms = 0; ms <= MAX_SPRING_MS; ms++) {
|
|
100
|
+
const s = solve(ms / 1000);
|
|
101
|
+
if (Math.abs(1 - s.position) >= restDelta || Math.abs(s.velocity) >= restSpeed) {
|
|
102
|
+
lastMoving = ms;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (lastMoving >= MAX_SPRING_MS)
|
|
106
|
+
return { durationMs: MAX_SPRING_MS, settled: false };
|
|
107
|
+
return { durationMs: lastMoving + 1, settled: true };
|
|
108
|
+
}
|
|
109
|
+
function sampleCount(samples) {
|
|
110
|
+
return Math.round(clamp(samples ?? DEFAULT_SPRING_SAMPLES, 2, MAX_SPRING_SAMPLES, DEFAULT_SPRING_SAMPLES));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Sample the spring at evenly spaced times across its settle duration. The
|
|
114
|
+
* first point is the start position (0) and the last is pinned to 1, so a
|
|
115
|
+
* spring cut at `MAX_SPRING_MS` lands exactly on its target.
|
|
116
|
+
*/
|
|
117
|
+
export function springSamples(params, options = {}) {
|
|
118
|
+
const settle = springSettle(params, options);
|
|
119
|
+
const count = sampleCount(options.samples);
|
|
120
|
+
const solve = solver(params, options.velocity ?? 0);
|
|
121
|
+
const points = [];
|
|
122
|
+
for (let i = 0; i < count; i++) {
|
|
123
|
+
const t = (settle.durationMs * i) / (count - 1) / 1000;
|
|
124
|
+
points.push(i === count - 1 ? 1 : round(solve(t).position));
|
|
125
|
+
}
|
|
126
|
+
points[0] = 0;
|
|
127
|
+
return { ...settle, points };
|
|
128
|
+
}
|
|
129
|
+
function round(value) {
|
|
130
|
+
const r = Math.round(value * 10000) / 10000;
|
|
131
|
+
return Object.is(r, -0) ? 0 : r;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A CSS `linear()` easing that traces the spring, with its duration. Points
|
|
135
|
+
* are evenly spaced, so no stop percentages are needed.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* const { easing, durationMs } = springEasing({ stiffness: 400, damping: 30, mass: 1 });
|
|
139
|
+
* element.animate(keyframes, { duration: durationMs, easing });
|
|
140
|
+
*/
|
|
141
|
+
export function springEasing(params, options = {}) {
|
|
142
|
+
const { points, durationMs, settled } = springSamples(params, options);
|
|
143
|
+
return { easing: `linear(${points.join(', ')})`, durationMs, settled };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Keyframes for any property: `build(progress)` maps a spring position to a
|
|
147
|
+
* keyframe (for example a transform). Offsets are evenly spaced.
|
|
148
|
+
*/
|
|
149
|
+
export function springKeyframes(params, build, options = {}) {
|
|
150
|
+
const { points, durationMs, settled } = springSamples(params, options);
|
|
151
|
+
const last = points.length - 1;
|
|
152
|
+
const keyframes = points.map((p, i) => ({ ...build(p), offset: i / last }));
|
|
153
|
+
return { keyframes, durationMs, settled };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Velocity for a retargeted spring: a velocity measured in the old segment's
|
|
157
|
+
* progress units, converted to the new segment (`newSpan` / `oldSpan` are the
|
|
158
|
+
* distances in any shared unit). Zero or non-finite spans give 0.
|
|
159
|
+
*/
|
|
160
|
+
export function retargetVelocity(velocity, oldSpan, newSpan) {
|
|
161
|
+
if (!Number.isFinite(velocity) || !Number.isFinite(oldSpan) || !Number.isFinite(newSpan))
|
|
162
|
+
return 0;
|
|
163
|
+
if (newSpan === 0)
|
|
164
|
+
return 0;
|
|
165
|
+
return clamp((velocity * oldSpan) / newSpan, -1000, 1000, 0);
|
|
166
|
+
}
|
package/dist/overlay/core.js
CHANGED
|
@@ -161,10 +161,16 @@ export function createOverlayCore(options, features = {}) {
|
|
|
161
161
|
/**
|
|
162
162
|
* R16: an exempt region shown in the top layer (the toast region as a
|
|
163
163
|
* `popover`) must keep painting above the overlay that just joined it.
|
|
164
|
-
* The top layer orders by show time, so re-show it.
|
|
164
|
+
* The top layer orders by show time, so re-show it. Exemptions registered
|
|
165
|
+
* with `raise: false` (a dialog backdrop, a menu or listbox opened inside a
|
|
166
|
+
* modal) stay where they are: re-showing them would cover this overlay.
|
|
165
167
|
*/
|
|
166
168
|
function raiseExemptPopovers() {
|
|
167
|
-
|
|
169
|
+
const regions = (inertRegistry()?.exemptions() ?? [])
|
|
170
|
+
.filter((entry) => entry.raise !== false)
|
|
171
|
+
.map((entry) => entry.element)
|
|
172
|
+
.filter((region) => region !== element && !element.contains(region));
|
|
173
|
+
for (const region of regions) {
|
|
168
174
|
const popover = region;
|
|
169
175
|
try {
|
|
170
176
|
if (!popover.matches(':popover-open'))
|
|
@@ -239,7 +245,12 @@ export function createOverlayCore(options, features = {}) {
|
|
|
239
245
|
else if (inert && inert.size() > 0 && isLive(element)) {
|
|
240
246
|
// A menu or popover opened from inside a modal: keep it live while open,
|
|
241
247
|
// even when it sits outside the modal (portaled to the body).
|
|
242
|
-
|
|
248
|
+
// `raise: false`: an overlay opened later from this one must stay above it.
|
|
249
|
+
liveExemption = inert.exempt({
|
|
250
|
+
element,
|
|
251
|
+
label: options.label ?? 'overlay',
|
|
252
|
+
raise: false,
|
|
253
|
+
});
|
|
243
254
|
}
|
|
244
255
|
if (wantsTrap && features.focusScope) {
|
|
245
256
|
focusScope = features.focusScope(element, {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/core/zip: a deterministic ZIP writer (stored, or deflated through
|
|
3
|
+
* the platform `CompressionStream`) and CRC-32 (ADR-016).
|
|
4
|
+
*/
|
|
5
|
+
export { assertZipPath, type CompressionFormatName, canCompress, compressBytes, crc32, writeZip, writeZipAsync, type ZipEntry, type ZipOptions, } from './zip.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic ZIP writer for export packages (ADR-016). Entries keep
|
|
3
|
+
* their order, use UTF-8 names and a fixed 1980-01-01 timestamp, so the
|
|
4
|
+
* same input always produces the same bytes. `writeZip` stores entries
|
|
5
|
+
* (synchronous, no compression); `writeZipAsync` deflates them through the
|
|
6
|
+
* platform `CompressionStream` when it exists and keeps an entry stored when
|
|
7
|
+
* deflate is missing or would not make it smaller. ZIP64 is not supported.
|
|
8
|
+
*/
|
|
9
|
+
export interface ZipEntry {
|
|
10
|
+
path: string;
|
|
11
|
+
data: Uint8Array | string;
|
|
12
|
+
/** `writeZipAsync` only: `false` keeps this entry stored (an already compressed image). */
|
|
13
|
+
compress?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ZipOptions {
|
|
16
|
+
/** Deflate entries when the platform can (default true). */
|
|
17
|
+
compress?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export type CompressionFormatName = 'deflate' | 'deflate-raw';
|
|
20
|
+
/** CRC-32 (IEEE 802.3), as ZIP and PNG use. */
|
|
21
|
+
export declare function crc32(data: Uint8Array): number;
|
|
22
|
+
/** Refuse empty, absolute, backslashed, dot-segment and control-character paths (no traversal on extract). */
|
|
23
|
+
export declare function assertZipPath(path: string): void;
|
|
24
|
+
/** A stored (uncompressed) ZIP, synchronously. */
|
|
25
|
+
export declare function writeZip(entries: readonly ZipEntry[]): Uint8Array;
|
|
26
|
+
/** True when the platform has `CompressionStream` with raw deflate. */
|
|
27
|
+
export declare function canCompress(format?: CompressionFormatName): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Compress bytes with the platform `CompressionStream` (`deflate` is the
|
|
30
|
+
* zlib wrapper PDF's FlateDecode reads; `deflate-raw` is what ZIP stores),
|
|
31
|
+
* or `null` when the platform has no such stream.
|
|
32
|
+
*/
|
|
33
|
+
export declare function compressBytes(data: Uint8Array, format: CompressionFormatName): Promise<Uint8Array | null>;
|
|
34
|
+
/** A ZIP whose entries are deflated where the platform can and where it makes them smaller. */
|
|
35
|
+
export declare function writeZipAsync(entries: readonly ZipEntry[], options?: ZipOptions): Promise<Uint8Array>;
|
package/dist/zip/zip.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const MAX_ENTRIES = 0xffff;
|
|
2
|
+
const MAX_SIZE = 0xffffffff;
|
|
3
|
+
const DOS_TIME = 0;
|
|
4
|
+
const DOS_DATE = (0 << 9) | (1 << 5) | 1;
|
|
5
|
+
const UTF8_FLAG = 0x0800;
|
|
6
|
+
const STORED = 0;
|
|
7
|
+
const DEFLATED = 8;
|
|
8
|
+
let crcTable;
|
|
9
|
+
function table() {
|
|
10
|
+
if (crcTable)
|
|
11
|
+
return crcTable;
|
|
12
|
+
crcTable = new Uint32Array(256);
|
|
13
|
+
for (let n = 0; n < 256; n += 1) {
|
|
14
|
+
let c = n;
|
|
15
|
+
for (let k = 0; k < 8; k += 1)
|
|
16
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
17
|
+
crcTable[n] = c >>> 0;
|
|
18
|
+
}
|
|
19
|
+
return crcTable;
|
|
20
|
+
}
|
|
21
|
+
/** CRC-32 (IEEE 802.3), as ZIP and PNG use. */
|
|
22
|
+
export function crc32(data) {
|
|
23
|
+
const lookup = table();
|
|
24
|
+
let crc = 0xffffffff;
|
|
25
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
26
|
+
crc = lookup[(crc ^ data[index]) & 0xff] ^ (crc >>> 8);
|
|
27
|
+
}
|
|
28
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
/** Refuse empty, absolute, backslashed, dot-segment and control-character paths (no traversal on extract). */
|
|
31
|
+
export function assertZipPath(path) {
|
|
32
|
+
if (typeof path !== 'string' ||
|
|
33
|
+
path.length === 0 ||
|
|
34
|
+
path.startsWith('/') ||
|
|
35
|
+
path.includes('\\') ||
|
|
36
|
+
path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..') ||
|
|
37
|
+
[...path].some((character) => (character.codePointAt(0) ?? 0) < 0x20)) {
|
|
38
|
+
throw new TypeError(`Unsafe ZIP entry path: ${JSON.stringify(path)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function prepare(entries) {
|
|
42
|
+
if (entries.length > MAX_ENTRIES)
|
|
43
|
+
throw new RangeError('Too many ZIP entries.');
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const raw = [];
|
|
47
|
+
const prepared = entries.map((entry) => {
|
|
48
|
+
assertZipPath(entry.path);
|
|
49
|
+
if (seen.has(entry.path))
|
|
50
|
+
throw new TypeError(`Duplicate ZIP entry: ${entry.path}`);
|
|
51
|
+
seen.add(entry.path);
|
|
52
|
+
const name = encoder.encode(entry.path);
|
|
53
|
+
const data = typeof entry.data === 'string' ? encoder.encode(entry.data) : entry.data;
|
|
54
|
+
if (data.length > MAX_SIZE)
|
|
55
|
+
throw new RangeError(`ZIP entry is too large: ${entry.path}`);
|
|
56
|
+
raw.push(data);
|
|
57
|
+
return { name, data, size: data.length, crc: crc32(data), method: STORED };
|
|
58
|
+
});
|
|
59
|
+
return { prepared, raw };
|
|
60
|
+
}
|
|
61
|
+
function assemble(prepared) {
|
|
62
|
+
const localSize = prepared.reduce((sum, entry) => sum + 30 + entry.name.length + entry.data.length, 0);
|
|
63
|
+
const centralSize = prepared.reduce((sum, entry) => sum + 46 + entry.name.length, 0);
|
|
64
|
+
if (localSize + centralSize > MAX_SIZE)
|
|
65
|
+
throw new RangeError('ZIP archive is too large.');
|
|
66
|
+
const bytes = new Uint8Array(localSize + centralSize + 22);
|
|
67
|
+
const view = new DataView(bytes.buffer);
|
|
68
|
+
let offset = 0;
|
|
69
|
+
const offsets = [];
|
|
70
|
+
for (const entry of prepared) {
|
|
71
|
+
offsets.push(offset);
|
|
72
|
+
view.setUint32(offset, 0x04034b50, true);
|
|
73
|
+
view.setUint16(offset + 4, 20, true);
|
|
74
|
+
view.setUint16(offset + 6, UTF8_FLAG, true);
|
|
75
|
+
view.setUint16(offset + 8, entry.method, true);
|
|
76
|
+
view.setUint16(offset + 10, DOS_TIME, true);
|
|
77
|
+
view.setUint16(offset + 12, DOS_DATE, true);
|
|
78
|
+
view.setUint32(offset + 14, entry.crc, true);
|
|
79
|
+
view.setUint32(offset + 18, entry.data.length, true);
|
|
80
|
+
view.setUint32(offset + 22, entry.size, true);
|
|
81
|
+
view.setUint16(offset + 26, entry.name.length, true);
|
|
82
|
+
view.setUint16(offset + 28, 0, true);
|
|
83
|
+
bytes.set(entry.name, offset + 30);
|
|
84
|
+
bytes.set(entry.data, offset + 30 + entry.name.length);
|
|
85
|
+
offset += 30 + entry.name.length + entry.data.length;
|
|
86
|
+
}
|
|
87
|
+
const centralStart = offset;
|
|
88
|
+
prepared.forEach((entry, index) => {
|
|
89
|
+
view.setUint32(offset, 0x02014b50, true);
|
|
90
|
+
view.setUint16(offset + 4, 20, true);
|
|
91
|
+
view.setUint16(offset + 6, 20, true);
|
|
92
|
+
view.setUint16(offset + 8, UTF8_FLAG, true);
|
|
93
|
+
view.setUint16(offset + 10, entry.method, true);
|
|
94
|
+
view.setUint16(offset + 12, DOS_TIME, true);
|
|
95
|
+
view.setUint16(offset + 14, DOS_DATE, true);
|
|
96
|
+
view.setUint32(offset + 16, entry.crc, true);
|
|
97
|
+
view.setUint32(offset + 20, entry.data.length, true);
|
|
98
|
+
view.setUint32(offset + 24, entry.size, true);
|
|
99
|
+
view.setUint16(offset + 28, entry.name.length, true);
|
|
100
|
+
view.setUint16(offset + 30, 0, true);
|
|
101
|
+
view.setUint16(offset + 32, 0, true);
|
|
102
|
+
view.setUint16(offset + 34, 0, true);
|
|
103
|
+
view.setUint16(offset + 36, 0, true);
|
|
104
|
+
view.setUint32(offset + 38, 0, true);
|
|
105
|
+
view.setUint32(offset + 42, offsets[index], true);
|
|
106
|
+
bytes.set(entry.name, offset + 46);
|
|
107
|
+
offset += 46 + entry.name.length;
|
|
108
|
+
});
|
|
109
|
+
view.setUint32(offset, 0x06054b50, true);
|
|
110
|
+
view.setUint16(offset + 4, 0, true);
|
|
111
|
+
view.setUint16(offset + 6, 0, true);
|
|
112
|
+
view.setUint16(offset + 8, prepared.length, true);
|
|
113
|
+
view.setUint16(offset + 10, prepared.length, true);
|
|
114
|
+
view.setUint32(offset + 12, offset - centralStart, true);
|
|
115
|
+
view.setUint32(offset + 16, centralStart, true);
|
|
116
|
+
view.setUint16(offset + 20, 0, true);
|
|
117
|
+
return bytes;
|
|
118
|
+
}
|
|
119
|
+
/** A stored (uncompressed) ZIP, synchronously. */
|
|
120
|
+
export function writeZip(entries) {
|
|
121
|
+
return assemble(prepare(entries).prepared);
|
|
122
|
+
}
|
|
123
|
+
/** True when the platform has `CompressionStream` with raw deflate. */
|
|
124
|
+
export function canCompress(format = 'deflate-raw') {
|
|
125
|
+
const Ctor = globalThis.CompressionStream;
|
|
126
|
+
if (typeof Ctor !== 'function')
|
|
127
|
+
return false;
|
|
128
|
+
try {
|
|
129
|
+
new Ctor(format);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Compress bytes with the platform `CompressionStream` (`deflate` is the
|
|
138
|
+
* zlib wrapper PDF's FlateDecode reads; `deflate-raw` is what ZIP stores),
|
|
139
|
+
* or `null` when the platform has no such stream.
|
|
140
|
+
*/
|
|
141
|
+
export async function compressBytes(data, format) {
|
|
142
|
+
const Ctor = globalThis.CompressionStream;
|
|
143
|
+
if (typeof Ctor !== 'function')
|
|
144
|
+
return null;
|
|
145
|
+
let stream;
|
|
146
|
+
try {
|
|
147
|
+
stream = new Ctor(format);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const writer = stream.writable.getWriter();
|
|
153
|
+
// Write and close without awaiting: the readable side drains the stream.
|
|
154
|
+
writer.write(data).catch(() => { });
|
|
155
|
+
writer.close().catch(() => { });
|
|
156
|
+
const reader = stream.readable.getReader();
|
|
157
|
+
const chunks = [];
|
|
158
|
+
let total = 0;
|
|
159
|
+
for (;;) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done)
|
|
162
|
+
break;
|
|
163
|
+
chunks.push(value);
|
|
164
|
+
total += value.length;
|
|
165
|
+
}
|
|
166
|
+
const out = new Uint8Array(total);
|
|
167
|
+
let offset = 0;
|
|
168
|
+
for (const chunk of chunks) {
|
|
169
|
+
out.set(chunk, offset);
|
|
170
|
+
offset += chunk.length;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** A ZIP whose entries are deflated where the platform can and where it makes them smaller. */
|
|
175
|
+
export async function writeZipAsync(entries, options = {}) {
|
|
176
|
+
const { prepared, raw } = prepare(entries);
|
|
177
|
+
if (options.compress !== false && canCompress('deflate-raw')) {
|
|
178
|
+
for (let index = 0; index < prepared.length; index += 1) {
|
|
179
|
+
if (entries[index]?.compress === false)
|
|
180
|
+
continue;
|
|
181
|
+
const entry = prepared[index];
|
|
182
|
+
const packed = await compressBytes(raw[index], 'deflate-raw');
|
|
183
|
+
if (packed && packed.length < entry.data.length) {
|
|
184
|
+
entry.data = packed;
|
|
185
|
+
entry.method = DEFLATED;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return assemble(prepared);
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@texuli/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Texels: framework-free behavior and policy that every Texuli component is built from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"./keyboard": "./dist/keyboard/index.js",
|
|
42
42
|
"./layer-stack": "./dist/layer-stack/index.js",
|
|
43
43
|
"./lazy": "./dist/lazy/index.js",
|
|
44
|
+
"./markup": "./dist/markup/index.js",
|
|
45
|
+
"./motion": "./dist/motion/index.js",
|
|
46
|
+
"./motion/spring": "./dist/motion/spring.js",
|
|
44
47
|
"./overlay": "./dist/overlay/index.js",
|
|
45
48
|
"./overlay-core": "./dist/overlay/core-entry.js",
|
|
46
49
|
"./portal": "./dist/portal/index.js",
|
|
@@ -50,7 +53,8 @@
|
|
|
50
53
|
"./roving": "./dist/roving/index.js",
|
|
51
54
|
"./scroll-lock": "./dist/scroll-lock/scroll-lock.js",
|
|
52
55
|
"./tabbable": "./dist/tabbable/index.js",
|
|
53
|
-
"./url-policy": "./dist/url-policy/index.js"
|
|
56
|
+
"./url-policy": "./dist/url-policy/index.js",
|
|
57
|
+
"./zip": "./dist/zip/index.js"
|
|
54
58
|
},
|
|
55
59
|
"files": [
|
|
56
60
|
"dist"
|