@jxsuite/import 0.40.1 → 0.40.3
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 +8 -30
- package/package.json +4 -4
- package/src/apply-accordions.ts +304 -0
- package/src/apply-disclosures.ts +275 -0
- package/src/apply-popovers.ts +298 -0
- package/src/apply-styles.ts +60 -11
- package/src/asset-rewrite.ts +30 -6
- package/src/componentize.ts +35 -1
- package/src/crawl.ts +8 -2
- package/src/derived-geometry.ts +115 -0
- package/src/emit.ts +25 -6
- package/src/image-family.ts +227 -0
- package/src/media-extract.ts +72 -3
- package/src/pipeline.ts +70 -2
- package/src/style-capture.ts +195 -159
- package/src/style-diff.ts +17 -4
- package/src/to-jx.ts +27 -1
- package/src/toggle-algebra.ts +167 -0
package/src/emit.ts
CHANGED
|
@@ -166,10 +166,16 @@ export async function emitMultiPageProject({
|
|
|
166
166
|
tagName: comp.tagName,
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
/* Derived placeholders fill in for props the template interpolates but nobody declared;
|
|
170
|
+
a value the template DECLARED wins over them. This was an assignment over the spread, so
|
|
171
|
+
a component that carried its own `state` — the `active` index a tab group switches on —
|
|
172
|
+
lost it on the way out, and every interpolation referencing it went dead. */
|
|
173
|
+
const derived: Record<string, string> = {};
|
|
174
|
+
extractStateDefaults(comp.template, derived);
|
|
175
|
+
const declared = (comp.template.state ?? {}) as Record<string, unknown>;
|
|
176
|
+
const state = { ...derived, ...declared };
|
|
171
177
|
if (Object.keys(state).length > 0) {
|
|
172
|
-
compDoc.state = state
|
|
178
|
+
compDoc.state = state as Record<string, string>;
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
componentFiles.set(fileName, compDoc);
|
|
@@ -298,16 +304,29 @@ export async function emitMultiPageProject({
|
|
|
298
304
|
return { classesStripped, files, projectJson: projectText };
|
|
299
305
|
}
|
|
300
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The state key a whole-string `${state.x}` names, or null.
|
|
309
|
+
*
|
|
310
|
+
* The identifier check is the point. The capture used to be `([^}]+)`, so `${state.active !== 0}` —
|
|
311
|
+
* an expression, not a reference — minted a state entry literally called `active !== 0` beside the
|
|
312
|
+
* real one. Only a bare identifier is a key; anything else is an expression over state and has no
|
|
313
|
+
* default of its own to declare.
|
|
314
|
+
*/
|
|
315
|
+
function stateKey(text: string): string | undefined {
|
|
316
|
+
const key = text.match(/^\$\{state\.([^}]+)\}$/)?.[1];
|
|
317
|
+
return key !== undefined && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
301
320
|
function extractStateDefaults(node: JxElement | string, out: Record<string, string>) {
|
|
302
321
|
if (typeof node === "string") {
|
|
303
|
-
const key = node
|
|
322
|
+
const key = stateKey(node);
|
|
304
323
|
if (key && !(key in out)) {
|
|
305
324
|
out[key] = "";
|
|
306
325
|
}
|
|
307
326
|
return;
|
|
308
327
|
}
|
|
309
328
|
if (typeof node.textContent === "string") {
|
|
310
|
-
const key = node.textContent
|
|
329
|
+
const key = stateKey(node.textContent);
|
|
311
330
|
if (key && !(key in out)) {
|
|
312
331
|
out[key] = "";
|
|
313
332
|
}
|
|
@@ -315,7 +334,7 @@ function extractStateDefaults(node: JxElement | string, out: Record<string, stri
|
|
|
315
334
|
if (node.attributes) {
|
|
316
335
|
for (const v of Object.values(node.attributes)) {
|
|
317
336
|
if (typeof v === "string") {
|
|
318
|
-
const key = v
|
|
337
|
+
const key = stateKey(v);
|
|
319
338
|
if (key && !(key in out)) {
|
|
320
339
|
out[key] = "";
|
|
321
340
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group an image's responsive derivatives back into one family, and pick the member to keep.
|
|
3
|
+
*
|
|
4
|
+
* A CMS publishes one photograph as a dozen files. WordPress alone emits `-300x200`, `-768x512`,
|
|
5
|
+
* `-1024x683`, `-1536x1025`, `-2048x1366` and `-scaled` beside the upload, and every one of them
|
|
6
|
+
* appears in the `srcset` of every `<img>` that uses it. The importer enqueued each candidate as an
|
|
7
|
+
* independent asset, so a clone of a 451-image site downloaded 2,446 files and 427 MB, and the
|
|
8
|
+
* emitted markup carried the whole derivative ladder forward.
|
|
9
|
+
*
|
|
10
|
+
* That is the wrong shape twice over. It is large, and it hands the compiler a `srcset` it did not
|
|
11
|
+
* build and cannot reason about — while `image-transform.ts` caps its own ladder at the source
|
|
12
|
+
* file's width, so an `<img src>` pointing at a `-300x200` derivative is a permanent quality
|
|
13
|
+
* ceiling rather than merely a wasted byte.
|
|
14
|
+
*
|
|
15
|
+
* So the importer keeps ONE member per family and lets the compiler regenerate sizes from it.
|
|
16
|
+
*
|
|
17
|
+
* Two rules make this safe on a real site:
|
|
18
|
+
*
|
|
19
|
+
* 1. **The key comes from the source URL, never from the written filename.** `sanitizeFilename`
|
|
20
|
+
* truncates a long name to 96 characters, which cuts the `-WxH` marker off the end — five
|
|
21
|
+
* resolutions of one image would land as five unrelated families.
|
|
22
|
+
* 2. **The winner is always a URL the page actually referenced.** 120 of the 451 families on the
|
|
23
|
+
* reference corpus have no undecorated member at all, because WordPress serves only
|
|
24
|
+
* `-scaled.jpg` once an upload passes its threshold. Synthesising the bare name and fetching it
|
|
25
|
+
* would 404 on those, and a failed download leaves the remote URL in the markup — the clone then
|
|
26
|
+
* serves images from the host it cloned. Taking the largest real candidate cannot fail that
|
|
27
|
+
* way.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { DiscoveredAsset } from "./asset-collect.ts";
|
|
31
|
+
|
|
32
|
+
/** Sources whose URLs are responsive variants of one image. A CSS background is not one. */
|
|
33
|
+
const FAMILY_SOURCES = new Set(["img-src", "img-srcset", "source-srcset", "picture-source"]);
|
|
34
|
+
|
|
35
|
+
/** `name-1024x683.jpg` → the `1024x683` marker, and where it starts. */
|
|
36
|
+
const DIMENSION_SUFFIX = /-(\d{1,5})x(\d{1,5})$/;
|
|
37
|
+
|
|
38
|
+
/** WordPress's marker for "the original was too big, this is the working copy". */
|
|
39
|
+
const SCALED_SUFFIX = /-scaled$/;
|
|
40
|
+
|
|
41
|
+
interface ParsedName {
|
|
42
|
+
/** The stem with any `-WxH` and `-scaled` markers removed. */
|
|
43
|
+
stem: string;
|
|
44
|
+
/** Pixel area of the `-WxH` marker, or null when there is none. */
|
|
45
|
+
area: number | null;
|
|
46
|
+
/** Whether the name carried `-scaled`. */
|
|
47
|
+
scaled: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function splitExtension(pathname: string): { base: string; ext: string } {
|
|
51
|
+
const slash = pathname.lastIndexOf("/");
|
|
52
|
+
const dot = pathname.lastIndexOf(".");
|
|
53
|
+
return dot > slash
|
|
54
|
+
? { base: pathname.slice(0, dot), ext: pathname.slice(dot) }
|
|
55
|
+
: { base: pathname, ext: "" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip the derivative markers from a pathname's final segment.
|
|
60
|
+
*
|
|
61
|
+
* Order matters: WordPress writes `photo-scaled-768x512.jpg`, so the dimensions come off first and
|
|
62
|
+
* `-scaled` second. Doing it the other way leaves `-768x512` welded to the stem and splits the
|
|
63
|
+
* family.
|
|
64
|
+
*/
|
|
65
|
+
function parseName(pathname: string): ParsedName {
|
|
66
|
+
const { base } = splitExtension(pathname);
|
|
67
|
+
let stem = base;
|
|
68
|
+
let area: number | null = null;
|
|
69
|
+
|
|
70
|
+
const dimensions = stem.match(DIMENSION_SUFFIX);
|
|
71
|
+
if (dimensions) {
|
|
72
|
+
area = Number(dimensions[1]) * Number(dimensions[2]);
|
|
73
|
+
stem = stem.slice(0, dimensions.index);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const scaled = SCALED_SUFFIX.test(stem);
|
|
77
|
+
if (scaled) {
|
|
78
|
+
stem = stem.replace(SCALED_SUFFIX, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { area, scaled, stem };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The identity two derivatives of one image share.
|
|
86
|
+
*
|
|
87
|
+
* Origin and directory are part of it — two sites can both serve `hero.jpg`, and WordPress's
|
|
88
|
+
* `/uploads/<year>/<month>/` layout means the same basename recurs legitimately. The query string
|
|
89
|
+
* is not: a cache-buster is not a different image. A URL that will not parse is its own family,
|
|
90
|
+
* which is the conservative answer (it is kept, not merged with something it may not match).
|
|
91
|
+
*/
|
|
92
|
+
export function familyKey(url: string): string {
|
|
93
|
+
let parsed: URL;
|
|
94
|
+
try {
|
|
95
|
+
parsed = new URL(url);
|
|
96
|
+
} catch {
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
const { ext } = splitExtension(parsed.pathname);
|
|
100
|
+
const { stem } = parseName(parsed.pathname);
|
|
101
|
+
return `${parsed.origin}${stem}${ext.toLowerCase()}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pick the member of a family to keep.
|
|
106
|
+
*
|
|
107
|
+
* Ranked: an undecorated name beats everything, because it is the upload itself; otherwise the
|
|
108
|
+
* largest `-WxH` wins, and a `-scaled` copy is preferred to a small explicit crop only when nothing
|
|
109
|
+
* else is left. Ties break on the URL string so a crawl is reproducible.
|
|
110
|
+
*
|
|
111
|
+
* @param {readonly string[]} candidates - Absolute URLs, all of one family, non-empty
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
export function chooseOriginal(candidates: readonly string[]): string {
|
|
115
|
+
const ranked = candidates.map((url) => {
|
|
116
|
+
let pathname: string;
|
|
117
|
+
try {
|
|
118
|
+
({ pathname } = new URL(url));
|
|
119
|
+
} catch {
|
|
120
|
+
pathname = url;
|
|
121
|
+
}
|
|
122
|
+
const { area, scaled } = parseName(pathname);
|
|
123
|
+
// Undecorated first, then scaled-only, then explicit crops largest-first.
|
|
124
|
+
const tier = area === null ? (scaled ? 1 : 0) : 2;
|
|
125
|
+
return { area: area ?? 0, tier, url };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
ranked.sort((a, b) => {
|
|
129
|
+
if (a.tier !== b.tier) {
|
|
130
|
+
return a.tier - b.tier;
|
|
131
|
+
}
|
|
132
|
+
if (a.area !== b.area) {
|
|
133
|
+
return b.area - a.area;
|
|
134
|
+
}
|
|
135
|
+
return a.url < b.url ? -1 : a.url > b.url ? 1 : 0;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return ranked[0]!.url;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ImageFamilyPlan {
|
|
142
|
+
/** The assets to actually download — one per image family, plus every non-family asset. */
|
|
143
|
+
keep: DiscoveredAsset[];
|
|
144
|
+
/**
|
|
145
|
+
* Dropped URL → the URL kept in its place.
|
|
146
|
+
*
|
|
147
|
+
* The caller replays this over the download's `rewriteMap` so a reference to any derivative still
|
|
148
|
+
* resolves, to the one file that was written.
|
|
149
|
+
*/
|
|
150
|
+
alias: Map<string, string>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reduce a discovered asset list to one member per image family.
|
|
155
|
+
*
|
|
156
|
+
* Only `<img>`/`<source>` URLs take part. A CSS background, a font and a favicon are each kept as
|
|
157
|
+
* found: a background is authored at one size, and merging two of them on a coincidental name match
|
|
158
|
+
* would silently swap an image the stylesheet asked for.
|
|
159
|
+
*
|
|
160
|
+
* @param {readonly DiscoveredAsset[]} assets
|
|
161
|
+
* @returns {ImageFamilyPlan}
|
|
162
|
+
*/
|
|
163
|
+
export function planImageFamilies(assets: readonly DiscoveredAsset[]): ImageFamilyPlan {
|
|
164
|
+
const families = new Map<string, string[]>();
|
|
165
|
+
for (const asset of assets) {
|
|
166
|
+
if (!FAMILY_SOURCES.has(asset.source)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const key = familyKey(asset.url);
|
|
170
|
+
families.set(key, [...(families.get(key) ?? []), asset.url]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const winners = new Map<string, string>();
|
|
174
|
+
for (const [key, urls] of families) {
|
|
175
|
+
winners.set(key, chooseOriginal(urls));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const alias = new Map<string, string>();
|
|
179
|
+
const keep: DiscoveredAsset[] = [];
|
|
180
|
+
const taken = new Set<string>();
|
|
181
|
+
|
|
182
|
+
for (const asset of assets) {
|
|
183
|
+
if (!FAMILY_SOURCES.has(asset.source)) {
|
|
184
|
+
keep.push(asset);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const winner = winners.get(familyKey(asset.url))!;
|
|
188
|
+
if (asset.url === winner) {
|
|
189
|
+
if (!taken.has(winner)) {
|
|
190
|
+
taken.add(winner);
|
|
191
|
+
keep.push(asset);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
alias.set(asset.url, winner);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { alias, keep };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Point every dropped derivative at the file its family actually wrote.
|
|
203
|
+
*
|
|
204
|
+
* The download only ever saw one member per family, so the map it returns has one entry per family.
|
|
205
|
+
* Markup still refers to whichever rung it was authored against, and an unresolved reference is not
|
|
206
|
+
* merely a broken image: `asset-rewrite.ts` leaves a URL it cannot resolve untouched, so the built
|
|
207
|
+
* clone would go on serving that image from the site it was cloned from.
|
|
208
|
+
*
|
|
209
|
+
* @param {Map<string, string>} rewriteMap - Download result, mutated in place
|
|
210
|
+
* @param {ReadonlyMap<string, string>} alias - Dropped URL to kept URL, from
|
|
211
|
+
* {@link planImageFamilies}
|
|
212
|
+
* @returns {number} How many references were reconnected
|
|
213
|
+
*/
|
|
214
|
+
export function applyFamilyAliases(
|
|
215
|
+
rewriteMap: Map<string, string>,
|
|
216
|
+
alias: ReadonlyMap<string, string>,
|
|
217
|
+
): number {
|
|
218
|
+
let reconnected = 0;
|
|
219
|
+
for (const [dropped, kept] of alias) {
|
|
220
|
+
const local = rewriteMap.get(kept);
|
|
221
|
+
if (local !== undefined) {
|
|
222
|
+
rewriteMap.set(dropped, local);
|
|
223
|
+
reconnected += 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return reconnected;
|
|
227
|
+
}
|
package/src/media-extract.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { ImportPage } from "./capture.ts";
|
|
|
16
16
|
import type { CapturedStyle } from "./style-capture.ts";
|
|
17
17
|
import { captureStylesAtWidth } from "./style-capture.ts";
|
|
18
18
|
import { computeMediaDelta } from "./style-diff.ts";
|
|
19
|
-
import { analyzeMediaQueries, planBreakpoints } from "./breakpoint-plan.ts";
|
|
19
|
+
import { analyzeMediaQueries, parseWidthQuery, planBreakpoints } from "./breakpoint-plan.ts";
|
|
20
20
|
import type { Breakpoint, BreakpointPolicy } from "./breakpoint-plan.ts";
|
|
21
21
|
import type { DiffedStyle } from "./style-diff.ts";
|
|
22
22
|
|
|
@@ -53,6 +53,66 @@ export interface ExtractMediaOptions {
|
|
|
53
53
|
plan?: readonly Breakpoint[] | undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* How far inside its own band the corroborating sample sits. Wide enough that a fluid element
|
|
58
|
+
* measures differently, narrow enough to stay clear of the neighbouring breakpoint.
|
|
59
|
+
*/
|
|
60
|
+
const SECOND_SAMPLE_OFFSET = 40;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A second viewport width INSIDE the same media band.
|
|
64
|
+
*
|
|
65
|
+
* The direction is the whole point: for `(max-width: N)` the band runs downward, so the second
|
|
66
|
+
* sample is narrower; for `(min-width: N)` it runs upward and the sample must be wider, or it
|
|
67
|
+
* leaves the band and measures the rules this breakpoint exists to distinguish itself from.
|
|
68
|
+
*/
|
|
69
|
+
export function secondSampleWidth(bp: Breakpoint): number {
|
|
70
|
+
const parsed = parseWidthQuery(bp.query);
|
|
71
|
+
const direction = parsed?.type === "min" ? 1 : -1;
|
|
72
|
+
return Math.max(1, bp.testWidth + direction * SECOND_SAMPLE_OFFSET);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Keep only the declarations two samples of the same band AGREE on.
|
|
77
|
+
*
|
|
78
|
+
* `getComputedStyle` returns used values, so `width` and `height` come back as the pixels the
|
|
79
|
+
* element happens to occupy - never the authored `100%` or `auto`. Diffing one sample against the
|
|
80
|
+
* base therefore reported a "change" for every fluid element at every breakpoint: two thirds of all
|
|
81
|
+
* responsive declarations in a real import were the viewport's own width written back as the
|
|
82
|
+
* element's, which does not merely waste space but PINS the layout and defeats the reflow the
|
|
83
|
+
* breakpoint existed for.
|
|
84
|
+
*
|
|
85
|
+
* Measuring twice inside the band separates the two cases without knowing which properties are
|
|
86
|
+
* geometry. An authored value is the same at both widths and survives; a resolved one moves with
|
|
87
|
+
* the viewport and is dropped. A property whose value legitimately differs across the band (a
|
|
88
|
+
* percentage-positioned element) is dropped too - that is the intended trade, since nothing in a
|
|
89
|
+
* computed-style capture can tell it from noise.
|
|
90
|
+
*/
|
|
91
|
+
export function agreedDeltas(
|
|
92
|
+
primary: readonly DiffedStyle[],
|
|
93
|
+
corroborating: readonly DiffedStyle[],
|
|
94
|
+
): DiffedStyle[] {
|
|
95
|
+
const second = new Map(corroborating.map((d) => [d.path.join(","), d.style]));
|
|
96
|
+
const agreed: DiffedStyle[] = [];
|
|
97
|
+
|
|
98
|
+
for (const delta of primary) {
|
|
99
|
+
const other = second.get(delta.path.join(","));
|
|
100
|
+
if (!other) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const style: Record<string, string | number> = {};
|
|
104
|
+
for (const [prop, value] of Object.entries(delta.style)) {
|
|
105
|
+
if (other[prop] === value) {
|
|
106
|
+
style[prop] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(style).length > 0) {
|
|
110
|
+
agreed.push({ path: delta.path, style });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return agreed;
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
/**
|
|
57
117
|
* Extract `$media` style deltas by re-capturing at each KEPT breakpoint width.
|
|
58
118
|
*
|
|
@@ -81,8 +141,17 @@ export async function extractMedia(
|
|
|
81
141
|
const allDeltas: Record<string, DiffedStyle[]> = {};
|
|
82
142
|
|
|
83
143
|
for (const bp of keep) {
|
|
84
|
-
const
|
|
85
|
-
|
|
144
|
+
const primary = computeMediaDelta(
|
|
145
|
+
baseElements,
|
|
146
|
+
await captureStylesAtWidth(page, bp.testWidth),
|
|
147
|
+
uaDefaults,
|
|
148
|
+
);
|
|
149
|
+
const corroborating = computeMediaDelta(
|
|
150
|
+
baseElements,
|
|
151
|
+
await captureStylesAtWidth(page, secondSampleWidth(bp)),
|
|
152
|
+
uaDefaults,
|
|
153
|
+
);
|
|
154
|
+
const deltas = agreedDeltas(primary, corroborating);
|
|
86
155
|
|
|
87
156
|
if (deltas.length > 0) {
|
|
88
157
|
projectBreakpoints[bp.name] = bp.query;
|
package/src/pipeline.ts
CHANGED
|
@@ -31,10 +31,14 @@ import { diffAllStyles, kebabToCamel } from "./style-diff.ts";
|
|
|
31
31
|
import { applyStylesToTree } from "./apply-styles.ts";
|
|
32
32
|
import { collectAssets } from "./asset-collect.ts";
|
|
33
33
|
import { downloadAssets } from "./asset-download.ts";
|
|
34
|
+
import { applyFamilyAliases, planImageFamilies } from "./image-family.ts";
|
|
34
35
|
import { rewriteAssetUrls } from "./asset-rewrite.ts";
|
|
35
36
|
import { applyTokens } from "./css-tokens.ts";
|
|
36
37
|
import { crawlSite } from "./crawl.ts";
|
|
37
38
|
import { detectLayout } from "./layout-detect.ts";
|
|
39
|
+
import { applyAccordions } from "./apply-accordions.ts";
|
|
40
|
+
import { applyPopovers } from "./apply-popovers.ts";
|
|
41
|
+
import { applyDisclosures } from "./apply-disclosures.ts";
|
|
38
42
|
import type { ImportIo } from "./io.ts";
|
|
39
43
|
import type { JxElement } from "@jxsuite/schema/types";
|
|
40
44
|
import type { ComponentizeResult } from "./componentize.ts";
|
|
@@ -99,6 +103,7 @@ export type ImportPhase =
|
|
|
99
103
|
| "crawl"
|
|
100
104
|
| "styles"
|
|
101
105
|
| "assets"
|
|
106
|
+
| "convert"
|
|
102
107
|
| "layout"
|
|
103
108
|
| "components"
|
|
104
109
|
| "ai"
|
|
@@ -322,7 +327,7 @@ export async function runImportPipeline(
|
|
|
322
327
|
if (bpCount > 0) {
|
|
323
328
|
progress("styles", `${bpCount} breakpoints with style changes`);
|
|
324
329
|
({ breakpoints } = media);
|
|
325
|
-
applyStylesToTree(jx.document, diffed, media.deltas);
|
|
330
|
+
applyStylesToTree(jx.document, diffed, media.deltas, media.breakpoints);
|
|
326
331
|
} else {
|
|
327
332
|
progress("styles", "No responsive breakpoints with style changes");
|
|
328
333
|
applyStylesToTree(jx.document, diffed);
|
|
@@ -368,7 +373,12 @@ export async function runImportPipeline(
|
|
|
368
373
|
|
|
369
374
|
if (collected.assets.length > 0) {
|
|
370
375
|
progress("assets", "Downloading assets...");
|
|
371
|
-
|
|
376
|
+
/* One member per responsive family reaches the network; every dropped derivative is
|
|
377
|
+
aliased onto the file that WAS written, so a reference to any rung of the ladder still
|
|
378
|
+
resolves. The compiler regenerates the sizes from the original it now owns. */
|
|
379
|
+
const families = planImageFamilies(collected.assets);
|
|
380
|
+
const downloaded = await downloadAssets(families.keep, io, url);
|
|
381
|
+
applyFamilyAliases(downloaded.rewriteMap, families.alias);
|
|
372
382
|
progress(
|
|
373
383
|
"assets",
|
|
374
384
|
`Downloaded ${downloaded.rewriteMap.size} assets (${formatBytes(downloaded.totalBytes)})`,
|
|
@@ -408,6 +418,31 @@ export async function runImportPipeline(
|
|
|
408
418
|
await capture.page.close();
|
|
409
419
|
|
|
410
420
|
throwIfAborted(signal);
|
|
421
|
+
/* Widgets become native markup BEFORE layout detection, which replaces the pages it extracts a
|
|
422
|
+
layout from - a pass after it would see the site's widgets in neither half. */
|
|
423
|
+
const singleConverted = applyAccordions(jx.document);
|
|
424
|
+
const singleDisclosures = applyDisclosures(jx.document);
|
|
425
|
+
if (singleDisclosures.converted > 0) {
|
|
426
|
+
progress(
|
|
427
|
+
"convert",
|
|
428
|
+
`Converted ${singleDisclosures.converted} disclosure${singleDisclosures.converted === 1 ? "" : "s"} to native <details>`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const singleOverlays = applyPopovers(jx.document);
|
|
432
|
+
if (singleOverlays.converted > 0) {
|
|
433
|
+
progress(
|
|
434
|
+
"convert",
|
|
435
|
+
`Converted ${singleOverlays.converted} dropdown${singleOverlays.converted === 1 ? "" : "s"} to popovers`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (singleConverted.converted > 0) {
|
|
439
|
+
progress(
|
|
440
|
+
"convert",
|
|
441
|
+
`Converted ${singleConverted.converted} accordion${singleConverted.converted === 1 ? "" : "s"} ` +
|
|
442
|
+
`(${singleConverted.rows} rows) to native <details>`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
411
446
|
const pageMap = new Map([["pages/index.json", jx.document]]);
|
|
412
447
|
const precomputedComponents = await maybeAiComponentize(
|
|
413
448
|
pageMap,
|
|
@@ -484,6 +519,39 @@ export async function runImportPipeline(
|
|
|
484
519
|
pageMap.set(page.route, page.jx.document);
|
|
485
520
|
}
|
|
486
521
|
|
|
522
|
+
/* Widgets become native markup BEFORE layout detection, which replaces the pages it extracts a
|
|
523
|
+
layout from - a pass after it would see the site's widgets in neither half. */
|
|
524
|
+
let convertedWidgets = 0;
|
|
525
|
+
let convertedRows = 0;
|
|
526
|
+
let convertedOverlays = 0;
|
|
527
|
+
let convertedDisclosures = 0;
|
|
528
|
+
for (const document of pageMap.values()) {
|
|
529
|
+
const converted = applyAccordions(document);
|
|
530
|
+
convertedWidgets += converted.converted;
|
|
531
|
+
convertedRows += converted.rows;
|
|
532
|
+
convertedDisclosures += applyDisclosures(document).converted;
|
|
533
|
+
convertedOverlays += applyPopovers(document).converted;
|
|
534
|
+
}
|
|
535
|
+
if (convertedDisclosures > 0) {
|
|
536
|
+
progress(
|
|
537
|
+
"convert",
|
|
538
|
+
`Converted ${convertedDisclosures} disclosure${convertedDisclosures === 1 ? "" : "s"} to native <details>`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (convertedOverlays > 0) {
|
|
542
|
+
progress(
|
|
543
|
+
"convert",
|
|
544
|
+
`Converted ${convertedOverlays} dropdown${convertedOverlays === 1 ? "" : "s"} to popovers`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (convertedWidgets > 0) {
|
|
548
|
+
progress(
|
|
549
|
+
"convert",
|
|
550
|
+
`Converted ${convertedWidgets} accordion${convertedWidgets === 1 ? "" : "s"} ` +
|
|
551
|
+
`(${convertedRows} rows) to native <details>`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
487
555
|
// Layout detection
|
|
488
556
|
let layout: JxElement | undefined;
|
|
489
557
|
if (result.pages.length >= 2) {
|