@materializecms/server 0.4.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/CHANGELOG.md +27 -0
- package/package.json +4 -4
- package/src/ai.d.ts +6 -1
- package/src/ai.js +11 -3
- package/src/app.d.ts +3 -1
- package/src/app.js +1119 -74
- package/src/brief.d.ts +43 -0
- package/src/brief.js +172 -0
- package/src/forms.d.ts +47 -0
- package/src/forms.js +153 -0
- package/src/studio.d.ts +13 -0
- package/src/studio.js +75 -0
- package/src/tools.d.ts +1 -0
- package/src/tools.js +89 -2
- package/src/wordpress.d.ts +2 -0
- package/src/wordpress.js +22 -5
- package/src/wpimport.d.ts +19 -1
- package/src/wpimport.js +112 -3
package/src/wordpress.js
CHANGED
|
@@ -236,6 +236,23 @@ export function imagesIn(html) {
|
|
|
236
236
|
walk(parse(html));
|
|
237
237
|
return [...new Set(out)];
|
|
238
238
|
}
|
|
239
|
+
/** The alternative text written on WordPress, trimmed. */
|
|
240
|
+
const altOf = (img) => (img.attribs?.alt ?? '').replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
241
|
+
/** Each image address in some HTML with its alternative text, for the library. */
|
|
242
|
+
export function imageAltsIn(html) {
|
|
243
|
+
const out = new Map();
|
|
244
|
+
const walk = (n) => {
|
|
245
|
+
if (n.name === 'img') {
|
|
246
|
+
const src = imageSrc(n);
|
|
247
|
+
if (src && !out.get(src))
|
|
248
|
+
out.set(src, altOf(n));
|
|
249
|
+
}
|
|
250
|
+
for (const c of n.children ?? [])
|
|
251
|
+
walk(c);
|
|
252
|
+
};
|
|
253
|
+
walk(parse(html));
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
239
256
|
function imageSrc(img) {
|
|
240
257
|
const a = img.attribs ?? {};
|
|
241
258
|
// Lazy-loading plugins keep the real address aside.
|
|
@@ -363,7 +380,7 @@ function pieces(html, ctx) {
|
|
|
363
380
|
flush();
|
|
364
381
|
const src = imageSrc(img);
|
|
365
382
|
if (src)
|
|
366
|
-
out.push({ kind: 'image', src });
|
|
383
|
+
out.push({ kind: 'image', src, alt: altOf(img) });
|
|
367
384
|
return;
|
|
368
385
|
}
|
|
369
386
|
inlineBuffer.push(n);
|
|
@@ -373,7 +390,7 @@ function pieces(html, ctx) {
|
|
|
373
390
|
if (name === 'img') {
|
|
374
391
|
const src = imageSrc(n);
|
|
375
392
|
if (src)
|
|
376
|
-
out.push({ kind: 'image', src });
|
|
393
|
+
out.push({ kind: 'image', src, alt: altOf(n) });
|
|
377
394
|
return;
|
|
378
395
|
}
|
|
379
396
|
if (/^h[1-6]$/.test(name)) {
|
|
@@ -388,7 +405,7 @@ function pieces(html, ctx) {
|
|
|
388
405
|
for (const img of allImgs(n)) {
|
|
389
406
|
const src = imageSrc(img);
|
|
390
407
|
if (src)
|
|
391
|
-
out.push({ kind: 'image', src });
|
|
408
|
+
out.push({ kind: 'image', src, alt: altOf(img) });
|
|
392
409
|
}
|
|
393
410
|
const runs = runsOf(n.children ?? [], ctx);
|
|
394
411
|
if (runs.length)
|
|
@@ -451,7 +468,7 @@ export function htmlToRichText(html, ctx) {
|
|
|
451
468
|
if (p.kind === 'image') {
|
|
452
469
|
const img = ctx.image(p.src);
|
|
453
470
|
if (img)
|
|
454
|
-
out.push({ type: 'image', children: [], image: img });
|
|
471
|
+
out.push({ type: 'image', children: [], image: { ...img, alt: p.alt || img.alt } });
|
|
455
472
|
}
|
|
456
473
|
else if (p.kind === 'button') {
|
|
457
474
|
out.push({ type: 'paragraph', children: [{ text: p.text, bold: true, href: p.href }] });
|
|
@@ -512,7 +529,7 @@ export function htmlToPage(html, title, ctx) {
|
|
|
512
529
|
type: 'image',
|
|
513
530
|
name: 'Immagine',
|
|
514
531
|
src: img.src,
|
|
515
|
-
alt: img.alt,
|
|
532
|
+
alt: p.alt || img.alt,
|
|
516
533
|
ratio: img.width && img.height ? `${img.width}/${img.height}` : '16/9',
|
|
517
534
|
radius: 'card',
|
|
518
535
|
width: 'fill',
|
package/src/wpimport.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Collection, Field, Media } from '@materializecms/core';
|
|
2
|
+
import type { PageNode } from '@materializecms/core/page';
|
|
2
3
|
import { type WpSite } from './wordpress.ts';
|
|
3
4
|
/**
|
|
4
5
|
* A WordPress site into Materialize: which collection each kind of item goes to, which field
|
|
@@ -23,6 +24,8 @@ export interface ImportPlan {
|
|
|
23
24
|
/** For a choice field: WordPress category → option value; unmatched ones take `fallback`. */
|
|
24
25
|
categories?: Record<string, string>;
|
|
25
26
|
fallbackCategory?: string;
|
|
27
|
+
/** Pages built on the canvas are laid out again by the AI with the section library (drafts). */
|
|
28
|
+
aiLayout?: boolean;
|
|
26
29
|
}
|
|
27
30
|
/** Which sources fit a field of this kind. */
|
|
28
31
|
export declare function sourcesFor(f: Field): WpSource[];
|
|
@@ -40,7 +43,7 @@ export declare function matchOption(name: string, options: {
|
|
|
40
43
|
label: string;
|
|
41
44
|
}[]): string | null;
|
|
42
45
|
export interface ImportProgress {
|
|
43
|
-
phase: 'media' | 'content' | 'done' | 'failed';
|
|
46
|
+
phase: 'media' | 'content' | 'layout' | 'done' | 'failed';
|
|
44
47
|
done: number;
|
|
45
48
|
total: number;
|
|
46
49
|
created: {
|
|
@@ -63,6 +66,8 @@ export interface ImportProgress {
|
|
|
63
66
|
reasons: Record<string, number>;
|
|
64
67
|
};
|
|
65
68
|
redirects: number;
|
|
69
|
+
/** Pages the AI laid out again. */
|
|
70
|
+
relaid: number;
|
|
66
71
|
error?: string;
|
|
67
72
|
}
|
|
68
73
|
export interface ImportDeps {
|
|
@@ -91,7 +96,20 @@ export interface ImportDeps {
|
|
|
91
96
|
from: string;
|
|
92
97
|
to: string;
|
|
93
98
|
}[]) => Promise<number>;
|
|
99
|
+
/** The AI lays a page out again in its draft; a reason when it was left as it was. */
|
|
100
|
+
relayout?: (doc: {
|
|
101
|
+
id: string;
|
|
102
|
+
collection: string;
|
|
103
|
+
title: string;
|
|
104
|
+
}) => Promise<null | string>;
|
|
94
105
|
onProgress: (p: ImportProgress) => void;
|
|
95
106
|
}
|
|
96
107
|
export declare function runImport(site: WpSite, plan: ImportPlan, deps: ImportDeps): Promise<ImportProgress>;
|
|
97
108
|
export { fold };
|
|
109
|
+
/**
|
|
110
|
+
* Whether a page laid out again by the AI still says what it said: no text lost, nothing made up
|
|
111
|
+
* (library sample texts left behind count as made up), every image still there. A reason when not.
|
|
112
|
+
*/
|
|
113
|
+
export declare function relayoutProblem(before: PageNode[], after: PageNode[]): string | null;
|
|
114
|
+
/** Descriptions the page had for its images, put back where the new layout left them empty. */
|
|
115
|
+
export declare function restoreAlts(before: PageNode[], after: PageNode[]): PageNode[];
|
package/src/wpimport.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { slugify } from '@materializecms/core';
|
|
2
|
-
import { htmlToPage, htmlToRichText,
|
|
2
|
+
import { htmlToPage, htmlToRichText, imageAltsIn, mainContent, originalOf, plain, } from './wordpress.js';
|
|
3
3
|
/** Which sources fit a field of this kind. */
|
|
4
4
|
export function sourcesFor(f) {
|
|
5
5
|
switch (f.kind) {
|
|
@@ -113,6 +113,7 @@ export async function runImport(site, plan, deps) {
|
|
|
113
113
|
problems: [],
|
|
114
114
|
media: { downloaded: 0, failed: 0, reasons: {} },
|
|
115
115
|
redirects: 0,
|
|
116
|
+
relaid: 0,
|
|
116
117
|
};
|
|
117
118
|
const report = () => deps.onProgress({ ...progress });
|
|
118
119
|
const targetOf = (item) => (item.kind === 'post' ? plan.posts : plan.pages);
|
|
@@ -136,8 +137,11 @@ export async function runImport(site, plan, deps) {
|
|
|
136
137
|
const t = targetOf(item);
|
|
137
138
|
const fields = Object.values(t.fields);
|
|
138
139
|
if (fields.includes('content'))
|
|
139
|
-
for (const src of
|
|
140
|
-
|
|
140
|
+
for (const [src, alt] of imageAltsIn(item.html)) {
|
|
141
|
+
const url = new URL(src, site.url).href;
|
|
142
|
+
if (!wanted.get(url))
|
|
143
|
+
wanted.set(url, alt);
|
|
144
|
+
}
|
|
141
145
|
if (fields.includes('featured') && item.featured?.url)
|
|
142
146
|
wanted.set(new URL(item.featured.url, site.url).href, item.featured.alt);
|
|
143
147
|
}
|
|
@@ -217,6 +221,8 @@ export async function runImport(site, plan, deps) {
|
|
|
217
221
|
const moves = [];
|
|
218
222
|
// Addresses taken by this import: WordPress allows the same slug under different parent pages.
|
|
219
223
|
const taken = new Map();
|
|
224
|
+
// Canvas pages filled from the content: the ones the AI may lay out again.
|
|
225
|
+
const canvas = [];
|
|
220
226
|
for (const entry of plans) {
|
|
221
227
|
const { item, t, def } = entry;
|
|
222
228
|
let { slug, url } = entry;
|
|
@@ -268,6 +274,8 @@ export async function runImport(site, plan, deps) {
|
|
|
268
274
|
progress.problems.push({ title: doc.title, reason: 'importato come bozza: ha campi da completare' });
|
|
269
275
|
}
|
|
270
276
|
progress.created.push({ id: doc.id, collection: def.name, title: doc.title, published });
|
|
277
|
+
if (Object.entries(t.fields).some(([name, source]) => source === 'content' && def.fields[name]?.kind === 'page'))
|
|
278
|
+
canvas.push({ id: doc.id, collection: def.name, title: doc.title });
|
|
271
279
|
const from = pathOf(item.link, site.url);
|
|
272
280
|
if (published && from && url && from !== url)
|
|
273
281
|
moves.push({ from, to: url });
|
|
@@ -282,6 +290,31 @@ export async function runImport(site, plan, deps) {
|
|
|
282
290
|
}
|
|
283
291
|
if (plan.redirects && moves.length)
|
|
284
292
|
progress.redirects = await deps.addRedirects(moves);
|
|
293
|
+
// 4. The AI lays the pages out again, one at a time, in their drafts: what WordPress had stays
|
|
294
|
+
// published until someone publishes the new layout.
|
|
295
|
+
if (plan.aiLayout && deps.relayout && canvas.length) {
|
|
296
|
+
progress.phase = 'layout';
|
|
297
|
+
progress.done = 0;
|
|
298
|
+
progress.total = canvas.length;
|
|
299
|
+
report();
|
|
300
|
+
for (const doc of canvas) {
|
|
301
|
+
try {
|
|
302
|
+
const why = await deps.relayout(doc);
|
|
303
|
+
if (why)
|
|
304
|
+
progress.problems.push({ title: doc.title, reason: `impaginazione semplice lasciata: ${why}` });
|
|
305
|
+
else
|
|
306
|
+
progress.relaid++;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
progress.problems.push({
|
|
310
|
+
title: doc.title,
|
|
311
|
+
reason: `l’AI non ha rifatto l’impaginazione: ${error.message}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
progress.done++;
|
|
315
|
+
report();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
285
318
|
progress.phase = 'done';
|
|
286
319
|
report();
|
|
287
320
|
return progress;
|
|
@@ -350,3 +383,79 @@ async function valueFor(f, source, item, ctx, plan, site, relations, deps) {
|
|
|
350
383
|
}
|
|
351
384
|
}
|
|
352
385
|
export { fold };
|
|
386
|
+
/** The words of a page (texts and buttons), lowercase, three letters or more. */
|
|
387
|
+
function wordsOf(nodes) {
|
|
388
|
+
const out = new Set();
|
|
389
|
+
const go = (list) => {
|
|
390
|
+
for (const n of list) {
|
|
391
|
+
if (!n.hidden) {
|
|
392
|
+
const text = [n.text, ...(n.runs ?? []).map((r) => r.t)].filter(Boolean).join(' ');
|
|
393
|
+
for (const w of text.toLowerCase().match(/[\p{L}\p{N}]{3,}/gu) ?? [])
|
|
394
|
+
out.add(w);
|
|
395
|
+
}
|
|
396
|
+
if (n.children)
|
|
397
|
+
go(n.children);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
go(nodes);
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
function imagesOf(nodes) {
|
|
404
|
+
const out = new Set();
|
|
405
|
+
const go = (list) => {
|
|
406
|
+
for (const n of list) {
|
|
407
|
+
if (n.type === 'image' && n.src && !n.hidden)
|
|
408
|
+
out.add(n.src);
|
|
409
|
+
if (n.bgImage?.src)
|
|
410
|
+
out.add(n.bgImage.src);
|
|
411
|
+
if (n.children)
|
|
412
|
+
go(n.children);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
go(nodes);
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Whether a page laid out again by the AI still says what it said: no text lost, nothing made up
|
|
420
|
+
* (library sample texts left behind count as made up), every image still there. A reason when not.
|
|
421
|
+
*/
|
|
422
|
+
export function relayoutProblem(before, after) {
|
|
423
|
+
const was = wordsOf(before);
|
|
424
|
+
const now = wordsOf(after);
|
|
425
|
+
if (!now.size)
|
|
426
|
+
return 'la pagina sarebbe rimasta vuota';
|
|
427
|
+
const lost = [...was].filter((w) => !now.has(w)).length;
|
|
428
|
+
const added = [...now].filter((w) => !was.has(w)).length;
|
|
429
|
+
if (lost > Math.max(3, was.size * 0.1))
|
|
430
|
+
return 'l’AI avrebbe tolto parte del testo';
|
|
431
|
+
if (added > Math.max(8, was.size * 0.15))
|
|
432
|
+
return 'l’AI avrebbe aggiunto testi che non c’erano';
|
|
433
|
+
const pictures = imagesOf(after);
|
|
434
|
+
if ([...imagesOf(before)].some((src) => !pictures.has(src)))
|
|
435
|
+
return 'l’AI avrebbe tolto delle immagini';
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
/** Descriptions the page had for its images, put back where the new layout left them empty. */
|
|
439
|
+
export function restoreAlts(before, after) {
|
|
440
|
+
const alts = new Map();
|
|
441
|
+
const read = (list) => {
|
|
442
|
+
for (const n of list) {
|
|
443
|
+
if (n.type === 'image' && n.src && n.alt)
|
|
444
|
+
alts.set(n.src, n.alt);
|
|
445
|
+
if (n.bgImage?.src && n.bgImage.alt)
|
|
446
|
+
alts.set(n.bgImage.src, n.bgImage.alt);
|
|
447
|
+
if (n.children)
|
|
448
|
+
read(n.children);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
read(before);
|
|
452
|
+
const fix = (n) => {
|
|
453
|
+
let out = n;
|
|
454
|
+
if (n.type === 'image' && n.src && !n.alt?.trim() && alts.has(n.src))
|
|
455
|
+
out = { ...out, alt: alts.get(n.src) };
|
|
456
|
+
if (n.bgImage?.src && !n.bgImage.alt?.trim() && alts.has(n.bgImage.src))
|
|
457
|
+
out = { ...out, bgImage: { ...n.bgImage, alt: alts.get(n.bgImage.src) ?? '' } };
|
|
458
|
+
return n.children ? { ...out, children: n.children.map(fix) } : out;
|
|
459
|
+
};
|
|
460
|
+
return after.map(fix);
|
|
461
|
+
}
|