@formfeed/devkit 0.3.10 → 0.3.11
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/index.cjs +221 -44
- package/index.js +232 -45
- package/package.json +3 -3
- package/types/lib/project.d.cts +2 -1
- package/types/lib/project.d.ts +2 -1
package/index.cjs
CHANGED
|
@@ -221,16 +221,23 @@ function readJson(path, what) {
|
|
|
221
221
|
try {
|
|
222
222
|
return JSON.parse(text2);
|
|
223
223
|
} catch (e) {
|
|
224
|
-
throw new DevkitError(
|
|
224
|
+
throw new DevkitError(
|
|
225
|
+
`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`,
|
|
226
|
+
"validation"
|
|
227
|
+
);
|
|
225
228
|
}
|
|
226
229
|
}
|
|
227
230
|
var templateFileName = (kind) => (0, import_engine2.isOfficeKind)(kind) ? `template.${kind}` : "template.html";
|
|
228
231
|
function sourceFilesIn(dir) {
|
|
229
|
-
return ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)].filter(
|
|
232
|
+
return ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)].filter(
|
|
233
|
+
(name) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name))
|
|
234
|
+
);
|
|
230
235
|
}
|
|
231
236
|
function listTemplateSlugs(project) {
|
|
232
237
|
if (!(0, import_node_fs2.existsSync)(project.templatesDir)) return [];
|
|
233
|
-
return (0, import_node_fs2.readdirSync)(project.templatesDir).filter(
|
|
238
|
+
return (0, import_node_fs2.readdirSync)(project.templatesDir).filter(
|
|
239
|
+
(name) => sourceFilesIn((0, import_node_path2.join)(project.templatesDir, name)).length > 0
|
|
240
|
+
).sort();
|
|
234
241
|
}
|
|
235
242
|
function templateDir(project, slug) {
|
|
236
243
|
return (0, import_node_path2.join)(project.templatesDir, slug);
|
|
@@ -239,22 +246,41 @@ var sha256Of = (bytes) => (0, import_node_crypto.createHash)("sha256").update(by
|
|
|
239
246
|
function readOfficeFile(dir, name) {
|
|
240
247
|
const path = (0, import_node_path2.join)(dir, name);
|
|
241
248
|
const bytes = new Uint8Array((0, import_node_fs2.readFileSync)(path));
|
|
242
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
format: (0, import_node_path2.extname)(name).slice(1),
|
|
251
|
+
path,
|
|
252
|
+
bytes,
|
|
253
|
+
sha256: sha256Of(bytes)
|
|
254
|
+
};
|
|
243
255
|
}
|
|
244
256
|
function readTemplate(project, slug) {
|
|
245
257
|
const dir = templateDir(project, slug);
|
|
246
258
|
const sources = sourceFilesIn(dir);
|
|
247
259
|
if (sources.length === 0)
|
|
248
|
-
throw new DevkitError(
|
|
260
|
+
throw new DevkitError(
|
|
261
|
+
`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`
|
|
262
|
+
);
|
|
249
263
|
if (sources.length > 1)
|
|
250
|
-
throw new DevkitError(
|
|
264
|
+
throw new DevkitError(
|
|
265
|
+
`${dir} holds ${sources.join(" and ")}; a template folder has one of them`,
|
|
266
|
+
"validation"
|
|
267
|
+
);
|
|
251
268
|
const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
|
|
252
269
|
const html = file ? "" : readText((0, import_node_path2.join)(dir, "template.html")) ?? "";
|
|
253
|
-
const metaFile = readJson(
|
|
270
|
+
const metaFile = readJson(
|
|
271
|
+
(0, import_node_path2.join)(dir, "template.json"),
|
|
272
|
+
"template.json"
|
|
273
|
+
) ?? {};
|
|
254
274
|
if (file && metaFile.kind && metaFile.kind !== file.format)
|
|
255
|
-
throw new DevkitError(
|
|
275
|
+
throw new DevkitError(
|
|
276
|
+
`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${(0, import_node_path2.basename)(file.path)}`,
|
|
277
|
+
"validation"
|
|
278
|
+
);
|
|
256
279
|
if (!file && (0, import_engine2.isOfficeKind)(metaFile.kind))
|
|
257
|
-
throw new DevkitError(
|
|
280
|
+
throw new DevkitError(
|
|
281
|
+
`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`,
|
|
282
|
+
"validation"
|
|
283
|
+
);
|
|
258
284
|
const meta = {
|
|
259
285
|
name: metaFile.name ?? titleFromSlug(slug),
|
|
260
286
|
kind: file?.format ?? metaFile.kind ?? "pdf",
|
|
@@ -262,19 +288,38 @@ function readTemplate(project, slug) {
|
|
|
262
288
|
description: metaFile.description ?? null,
|
|
263
289
|
tags: metaFile.tags ?? []
|
|
264
290
|
};
|
|
265
|
-
const settings = {
|
|
291
|
+
const settings = {
|
|
292
|
+
...readJson(
|
|
293
|
+
(0, import_node_path2.join)(dir, "settings.json"),
|
|
294
|
+
"settings.json"
|
|
295
|
+
) ?? {}
|
|
296
|
+
};
|
|
266
297
|
const header = file ? null : readText((0, import_node_path2.join)(dir, "header.html"));
|
|
267
298
|
const footer = file ? null : readText((0, import_node_path2.join)(dir, "footer.html"));
|
|
268
|
-
if (header !== null && header.trim())
|
|
269
|
-
|
|
299
|
+
if (header !== null && header.trim())
|
|
300
|
+
settings.header = { ...settings.header ?? {}, html: header };
|
|
301
|
+
if (footer !== null && footer.trim())
|
|
302
|
+
settings.footer = { ...settings.footer ?? {}, html: footer };
|
|
303
|
+
const design = readJson(
|
|
304
|
+
(0, import_node_path2.join)(dir, "design.json"),
|
|
305
|
+
"design.json"
|
|
306
|
+
);
|
|
307
|
+
if (design) settings.design = design;
|
|
270
308
|
const dataSets = {};
|
|
271
309
|
const dataDir = (0, import_node_path2.join)(dir, "data");
|
|
272
310
|
if ((0, import_node_fs2.existsSync)(dataDir)) {
|
|
273
311
|
for (const file2 of (0, import_node_fs2.readdirSync)(dataDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
274
|
-
dataSets[(0, import_node_path2.basename)(file2, ".json")] = readJson(
|
|
312
|
+
dataSets[(0, import_node_path2.basename)(file2, ".json")] = readJson(
|
|
313
|
+
(0, import_node_path2.join)(dataDir, file2),
|
|
314
|
+
`data/${file2}`
|
|
315
|
+
);
|
|
275
316
|
}
|
|
276
317
|
}
|
|
277
|
-
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
|
|
318
|
+
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
|
|
319
|
+
html,
|
|
320
|
+
settings.header?.html,
|
|
321
|
+
settings.footer?.html
|
|
322
|
+
]);
|
|
278
323
|
return {
|
|
279
324
|
slug,
|
|
280
325
|
dir,
|
|
@@ -285,8 +330,14 @@ function readTemplate(project, slug) {
|
|
|
285
330
|
head: file ? "" : readText((0, import_node_path2.join)(dir, "head.html")) ?? "",
|
|
286
331
|
settings,
|
|
287
332
|
dataSets,
|
|
288
|
-
dataSchema: readJson(
|
|
289
|
-
|
|
333
|
+
dataSchema: readJson(
|
|
334
|
+
(0, import_node_path2.join)(dir, "schema.json"),
|
|
335
|
+
"schema.json"
|
|
336
|
+
),
|
|
337
|
+
i18n: readJson(
|
|
338
|
+
(0, import_node_path2.join)(dir, "i18n.json"),
|
|
339
|
+
"i18n.json"
|
|
340
|
+
),
|
|
290
341
|
partials,
|
|
291
342
|
missingPartials: missing,
|
|
292
343
|
sharedPartials: shared
|
|
@@ -296,35 +347,68 @@ function defaultData(tpl, name) {
|
|
|
296
347
|
const names = Object.keys(tpl.dataSets);
|
|
297
348
|
const chosen = name ?? (names.includes("default") ? "default" : names[0]);
|
|
298
349
|
if (name && !(name in tpl.dataSets))
|
|
299
|
-
throw new DevkitError(
|
|
350
|
+
throw new DevkitError(
|
|
351
|
+
`Data set "${name}" not found; available: ${names.join(", ") || "none"}`
|
|
352
|
+
);
|
|
300
353
|
return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
|
|
301
354
|
}
|
|
302
355
|
function writeTemplate(project, slug, meta, version, file) {
|
|
303
356
|
const dir = templateDir(project, slug);
|
|
304
357
|
const office = (0, import_engine2.isOfficeKind)(meta.kind);
|
|
305
|
-
if (office && !file)
|
|
358
|
+
if (office && !file)
|
|
359
|
+
throw new DevkitError(
|
|
360
|
+
`${slug} is a ${meta.kind} template; its document is needed to write the folder`
|
|
361
|
+
);
|
|
306
362
|
(0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
|
|
307
363
|
const settings = { ...version.settings ?? {} };
|
|
308
364
|
const header = office ? "" : settings.header?.html ?? "";
|
|
309
365
|
const footer = office ? "" : settings.footer?.html ?? "";
|
|
310
366
|
if (settings.header) settings.header = stripHtml(settings.header);
|
|
311
367
|
if (settings.footer) settings.footer = stripHtml(settings.footer);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
368
|
+
const design = settings.design;
|
|
369
|
+
delete settings.design;
|
|
370
|
+
(0, import_node_fs2.writeFileSync)(
|
|
371
|
+
(0, import_node_path2.join)(dir, "template.json"),
|
|
372
|
+
JSON.stringify(meta, null, 2) + "\n"
|
|
373
|
+
);
|
|
374
|
+
for (const name of [
|
|
375
|
+
"template.html",
|
|
376
|
+
...import_engine2.officeKinds.map((k) => `template.${k}`)
|
|
377
|
+
])
|
|
378
|
+
if (name !== templateFileName(meta.kind) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name)))
|
|
379
|
+
(0, import_node_fs2.rmSync)((0, import_node_path2.join)(dir, name));
|
|
315
380
|
if (office) (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, templateFileName(meta.kind)), file);
|
|
316
381
|
else (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.html"), version.html ?? "");
|
|
317
382
|
writeOrRemove((0, import_node_path2.join)(dir, "style.css"), office ? "" : version.css ?? "");
|
|
318
383
|
writeOrRemove((0, import_node_path2.join)(dir, "head.html"), office ? "" : version.head ?? "");
|
|
319
384
|
writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
|
|
320
385
|
writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
|
|
321
|
-
(0, import_node_fs2.writeFileSync)(
|
|
322
|
-
|
|
386
|
+
(0, import_node_fs2.writeFileSync)(
|
|
387
|
+
(0, import_node_path2.join)(dir, "settings.json"),
|
|
388
|
+
JSON.stringify(settings, null, 2) + "\n"
|
|
389
|
+
);
|
|
390
|
+
writeOrRemove(
|
|
391
|
+
(0, import_node_path2.join)(dir, "design.json"),
|
|
392
|
+
design && typeof design === "object" ? JSON.stringify(design, null, 2) + "\n" : ""
|
|
393
|
+
);
|
|
394
|
+
(0, import_node_fs2.writeFileSync)(
|
|
395
|
+
(0, import_node_path2.join)(dir, "data", "default.json"),
|
|
396
|
+
JSON.stringify(version.sample_data ?? {}, null, 2) + "\n"
|
|
397
|
+
);
|
|
323
398
|
for (const [name, data] of Object.entries(version.data_sets ?? {}))
|
|
324
399
|
if (name !== "default" && /^[\w.-]+$/.test(name))
|
|
325
|
-
(0, import_node_fs2.writeFileSync)(
|
|
326
|
-
|
|
327
|
-
|
|
400
|
+
(0, import_node_fs2.writeFileSync)(
|
|
401
|
+
(0, import_node_path2.join)(dir, "data", `${name}.json`),
|
|
402
|
+
JSON.stringify(data ?? {}, null, 2) + "\n"
|
|
403
|
+
);
|
|
404
|
+
writeOrRemove(
|
|
405
|
+
(0, import_node_path2.join)(dir, "schema.json"),
|
|
406
|
+
version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : ""
|
|
407
|
+
);
|
|
408
|
+
writeOrRemove(
|
|
409
|
+
(0, import_node_path2.join)(dir, "i18n.json"),
|
|
410
|
+
version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : ""
|
|
411
|
+
);
|
|
328
412
|
writePartials(project, version.partials ?? null);
|
|
329
413
|
return dir;
|
|
330
414
|
}
|
|
@@ -333,7 +417,10 @@ function writePartials(project, partials) {
|
|
|
333
417
|
const changed = [];
|
|
334
418
|
for (const [name, source] of Object.entries(partials ?? {})) {
|
|
335
419
|
if (!partialName.test(name)) continue;
|
|
336
|
-
const path = (0, import_node_path2.join)(
|
|
420
|
+
const path = (0, import_node_path2.join)(
|
|
421
|
+
project.partialsDir,
|
|
422
|
+
name.includes(".") ? name : `${name}.html`
|
|
423
|
+
);
|
|
337
424
|
if (readText(path) === source) continue;
|
|
338
425
|
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
339
426
|
(0, import_node_fs2.writeFileSync)(path, source);
|
|
@@ -379,17 +466,36 @@ function versionPayload(tpl) {
|
|
|
379
466
|
function contentHash(tpl) {
|
|
380
467
|
if (tpl.file) {
|
|
381
468
|
const payload2 = officeVersionPayload(tpl);
|
|
382
|
-
const parts2 = [
|
|
469
|
+
const parts2 = [
|
|
470
|
+
tpl.file.format,
|
|
471
|
+
tpl.file.sha256,
|
|
472
|
+
payload2.settings,
|
|
473
|
+
payload2.sample_data,
|
|
474
|
+
payload2.data_sets,
|
|
475
|
+
payload2.data_schema,
|
|
476
|
+
payload2.i18n
|
|
477
|
+
];
|
|
383
478
|
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts2)).digest("hex");
|
|
384
479
|
}
|
|
385
480
|
const payload = versionPayload(tpl);
|
|
386
|
-
const parts = [
|
|
481
|
+
const parts = [
|
|
482
|
+
payload.html,
|
|
483
|
+
payload.css,
|
|
484
|
+
payload.head,
|
|
485
|
+
payload.settings,
|
|
486
|
+
payload.sample_data,
|
|
487
|
+
payload.data_sets,
|
|
488
|
+
payload.data_schema,
|
|
489
|
+
payload.i18n
|
|
490
|
+
];
|
|
387
491
|
if (payload.partials) parts.push(payload.partials);
|
|
388
492
|
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
389
493
|
}
|
|
390
494
|
var statePath = (project) => (0, import_node_path2.join)(project.root, ".formfeed", "state.json");
|
|
391
495
|
function readState(project) {
|
|
392
|
-
return readJson(statePath(project), ".formfeed/state.json") ?? {
|
|
496
|
+
return readJson(statePath(project), ".formfeed/state.json") ?? {
|
|
497
|
+
templates: {}
|
|
498
|
+
};
|
|
393
499
|
}
|
|
394
500
|
function writeState(project, state) {
|
|
395
501
|
(0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(project.root, ".formfeed"), { recursive: true });
|
|
@@ -415,7 +521,8 @@ function partialResolver(project) {
|
|
|
415
521
|
return (name) => {
|
|
416
522
|
for (const candidate of [name, `${name}.html`]) {
|
|
417
523
|
const path = (0, import_node_path2.join)(project.partialsDir, candidate);
|
|
418
|
-
if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile())
|
|
524
|
+
if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile())
|
|
525
|
+
return (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
419
526
|
}
|
|
420
527
|
shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
|
|
421
528
|
const normalised = (0, import_engine2.normalisePartialName)(name);
|
|
@@ -436,7 +543,12 @@ function recordSharedPartial(project, name, remote, source) {
|
|
|
436
543
|
const state = readState(project);
|
|
437
544
|
state.sharedPartials = {
|
|
438
545
|
...state.sharedPartials ?? {},
|
|
439
|
-
[name]: {
|
|
546
|
+
[name]: {
|
|
547
|
+
version: remote.version,
|
|
548
|
+
engine: remote.engine,
|
|
549
|
+
contentHash: partialContentHash(source),
|
|
550
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
551
|
+
}
|
|
440
552
|
};
|
|
441
553
|
writeState(project, state);
|
|
442
554
|
}
|
|
@@ -494,7 +606,10 @@ function diagnose(tpl, sampleData) {
|
|
|
494
606
|
severity: "error",
|
|
495
607
|
code: "syntax-error",
|
|
496
608
|
message: e.message,
|
|
497
|
-
range: {
|
|
609
|
+
range: {
|
|
610
|
+
start: { line: e.line ?? 1, column: e.column ?? 1 },
|
|
611
|
+
end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 }
|
|
612
|
+
}
|
|
498
613
|
});
|
|
499
614
|
} else throw e;
|
|
500
615
|
}
|
|
@@ -504,7 +619,8 @@ function diagnose(tpl, sampleData) {
|
|
|
504
619
|
["footer", tpl.settings.footer?.html]
|
|
505
620
|
]) {
|
|
506
621
|
if (!source) continue;
|
|
507
|
-
for (const d of engine.analyze(source, { sampleData }).diagnostics)
|
|
622
|
+
for (const d of engine.analyze(source, { sampleData }).diagnostics)
|
|
623
|
+
diagnostics.push({ ...d, message: `${part}: ${d.message}` });
|
|
508
624
|
}
|
|
509
625
|
for (const name of tpl.missingPartials)
|
|
510
626
|
diagnostics.push({
|
|
@@ -514,8 +630,28 @@ function diagnose(tpl, sampleData) {
|
|
|
514
630
|
message: `No partial "${name}" in the project's partials folder; if it is shared by the organisation, formfeed partials pull brings it`,
|
|
515
631
|
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
|
|
516
632
|
});
|
|
633
|
+
const design = designDiagnostic(tpl);
|
|
634
|
+
if (design) diagnostics.push(design);
|
|
517
635
|
return diagnostics;
|
|
518
636
|
}
|
|
637
|
+
function designDiagnostic(tpl) {
|
|
638
|
+
if (tpl.settings.design === void 0) return null;
|
|
639
|
+
const parsed = (0, import_engine4.parseDesign)(tpl.settings.design);
|
|
640
|
+
if (!parsed.design)
|
|
641
|
+
return {
|
|
642
|
+
severity: "warning",
|
|
643
|
+
code: "design-invalid",
|
|
644
|
+
message: `design.json is not a design the app can open (${parsed.problems.slice(0, 3).join("; ")}); the template opens as code`,
|
|
645
|
+
range: nowhere
|
|
646
|
+
};
|
|
647
|
+
if (parsed.design.output === (0, import_engine4.designOutputHash)(tpl.html, tpl.css)) return null;
|
|
648
|
+
return {
|
|
649
|
+
severity: "warning",
|
|
650
|
+
code: "design-stale",
|
|
651
|
+
message: "template.html or style.css was edited after design.json was written; the app opens the template as code until you choose to keep the code or apply the design again",
|
|
652
|
+
range: nowhere
|
|
653
|
+
};
|
|
654
|
+
}
|
|
519
655
|
var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
|
|
520
656
|
function diagnoseOffice(tpl, file, sampleData) {
|
|
521
657
|
let analysis;
|
|
@@ -523,7 +659,14 @@ function diagnoseOffice(tpl, file, sampleData) {
|
|
|
523
659
|
analysis = (0, import_engine4.analyzeOffice)(file, { engine: tpl.meta.engine, sampleData });
|
|
524
660
|
} catch (e) {
|
|
525
661
|
const code = e.code;
|
|
526
|
-
return [
|
|
662
|
+
return [
|
|
663
|
+
{
|
|
664
|
+
severity: "error",
|
|
665
|
+
code: typeof code === "string" ? code : "office-document-invalid",
|
|
666
|
+
message: e instanceof Error ? e.message : String(e),
|
|
667
|
+
range: nowhere
|
|
668
|
+
}
|
|
669
|
+
];
|
|
527
670
|
}
|
|
528
671
|
return analysis.diagnostics.map((d) => ({
|
|
529
672
|
severity: d.severity,
|
|
@@ -535,17 +678,34 @@ function diagnoseOffice(tpl, file, sampleData) {
|
|
|
535
678
|
}));
|
|
536
679
|
}
|
|
537
680
|
async function renderOfficeLocal(project, tpl, data, options = {}) {
|
|
538
|
-
if (!tpl.file)
|
|
539
|
-
|
|
681
|
+
if (!tpl.file)
|
|
682
|
+
throw new DevkitError(
|
|
683
|
+
`${tpl.slug} is not a Word or PowerPoint template`,
|
|
684
|
+
"validation"
|
|
685
|
+
);
|
|
686
|
+
const context = renderContext(
|
|
687
|
+
project,
|
|
688
|
+
tpl,
|
|
689
|
+
options.assetBaseUrl,
|
|
690
|
+
options.brand
|
|
691
|
+
);
|
|
540
692
|
if (options.locale) context.locale = options.locale;
|
|
541
|
-
return (0, import_engine4.renderOffice)(tpl.file.bytes, {
|
|
693
|
+
return (0, import_engine4.renderOffice)(tpl.file.bytes, {
|
|
694
|
+
engine: tpl.meta.engine,
|
|
695
|
+
data,
|
|
696
|
+
context,
|
|
697
|
+
images: options.images,
|
|
698
|
+
random: options.random
|
|
699
|
+
});
|
|
542
700
|
}
|
|
543
701
|
function htmlSnapshot(rendered) {
|
|
544
702
|
let snapshot = rendered.document;
|
|
545
|
-
if (rendered.headerHtml)
|
|
703
|
+
if (rendered.headerHtml)
|
|
704
|
+
snapshot += `
|
|
546
705
|
<!-- formfeed:header -->
|
|
547
706
|
${rendered.headerHtml}`;
|
|
548
|
-
if (rendered.footerHtml)
|
|
707
|
+
if (rendered.footerHtml)
|
|
708
|
+
snapshot += `
|
|
549
709
|
<!-- formfeed:footer -->
|
|
550
710
|
${rendered.footerHtml}`;
|
|
551
711
|
return snapshot;
|
|
@@ -598,7 +758,14 @@ async function renderLocal(project, tpl, data, options) {
|
|
|
598
758
|
const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
|
|
599
759
|
if (options.locale) ctx.locale = options.locale;
|
|
600
760
|
return (0, import_engine4.renderVersion)(
|
|
601
|
-
{
|
|
761
|
+
{
|
|
762
|
+
engine: tpl.meta.engine,
|
|
763
|
+
html: tpl.html,
|
|
764
|
+
css: tpl.css,
|
|
765
|
+
head: tpl.head,
|
|
766
|
+
settings: (0, import_engine4.mergeSettings)(tpl.settings),
|
|
767
|
+
kind
|
|
768
|
+
},
|
|
602
769
|
data,
|
|
603
770
|
ctx,
|
|
604
771
|
options.mode,
|
|
@@ -608,16 +775,26 @@ async function renderLocal(project, tpl, data, options) {
|
|
|
608
775
|
function renderedSettings(rendered) {
|
|
609
776
|
const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
|
|
610
777
|
const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
|
|
611
|
-
const { settings } = rendered;
|
|
778
|
+
const { design: _design, ...settings } = rendered.settings;
|
|
779
|
+
void _design;
|
|
612
780
|
return {
|
|
613
781
|
...settings,
|
|
614
782
|
...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
|
|
615
783
|
...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
|
|
616
|
-
...settings.pdf?.metadata?.title !== void 0 ? {
|
|
784
|
+
...settings.pdf?.metadata?.title !== void 0 ? {
|
|
785
|
+
pdf: {
|
|
786
|
+
...settings.pdf,
|
|
787
|
+
metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" }
|
|
788
|
+
}
|
|
789
|
+
} : {}
|
|
617
790
|
};
|
|
618
791
|
}
|
|
619
792
|
function htmlKind(tpl) {
|
|
620
|
-
if ((0, import_engine4.isOfficeKind)(tpl.meta.kind))
|
|
793
|
+
if ((0, import_engine4.isOfficeKind)(tpl.meta.kind))
|
|
794
|
+
throw new DevkitError(
|
|
795
|
+
`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`,
|
|
796
|
+
"validation"
|
|
797
|
+
);
|
|
621
798
|
return tpl.meta.kind;
|
|
622
799
|
}
|
|
623
800
|
function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
package/index.js
CHANGED
|
@@ -95,7 +95,15 @@ function writeProjectConfig(root, config) {
|
|
|
95
95
|
|
|
96
96
|
// packages/devkit/src/lib/project.ts
|
|
97
97
|
import { createHash } from "node:crypto";
|
|
98
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
existsSync as existsSync2,
|
|
100
|
+
mkdirSync,
|
|
101
|
+
readFileSync as readFileSync2,
|
|
102
|
+
readdirSync,
|
|
103
|
+
rmSync,
|
|
104
|
+
statSync,
|
|
105
|
+
writeFileSync as writeFileSync2
|
|
106
|
+
} from "node:fs";
|
|
99
107
|
import { basename, dirname as dirname2, extname, join as join2 } from "node:path";
|
|
100
108
|
import {
|
|
101
109
|
isOfficeKind,
|
|
@@ -149,16 +157,23 @@ function readJson(path, what) {
|
|
|
149
157
|
try {
|
|
150
158
|
return JSON.parse(text2);
|
|
151
159
|
} catch (e) {
|
|
152
|
-
throw new DevkitError(
|
|
160
|
+
throw new DevkitError(
|
|
161
|
+
`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`,
|
|
162
|
+
"validation"
|
|
163
|
+
);
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
var templateFileName = (kind) => isOfficeKind(kind) ? `template.${kind}` : "template.html";
|
|
156
167
|
function sourceFilesIn(dir) {
|
|
157
|
-
return ["template.html", ...officeKinds.map((k) => `template.${k}`)].filter(
|
|
168
|
+
return ["template.html", ...officeKinds.map((k) => `template.${k}`)].filter(
|
|
169
|
+
(name) => existsSync2(join2(dir, name))
|
|
170
|
+
);
|
|
158
171
|
}
|
|
159
172
|
function listTemplateSlugs(project) {
|
|
160
173
|
if (!existsSync2(project.templatesDir)) return [];
|
|
161
|
-
return readdirSync(project.templatesDir).filter(
|
|
174
|
+
return readdirSync(project.templatesDir).filter(
|
|
175
|
+
(name) => sourceFilesIn(join2(project.templatesDir, name)).length > 0
|
|
176
|
+
).sort();
|
|
162
177
|
}
|
|
163
178
|
function templateDir(project, slug) {
|
|
164
179
|
return join2(project.templatesDir, slug);
|
|
@@ -167,22 +182,41 @@ var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
|
167
182
|
function readOfficeFile(dir, name) {
|
|
168
183
|
const path = join2(dir, name);
|
|
169
184
|
const bytes = new Uint8Array(readFileSync2(path));
|
|
170
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
format: extname(name).slice(1),
|
|
187
|
+
path,
|
|
188
|
+
bytes,
|
|
189
|
+
sha256: sha256Of(bytes)
|
|
190
|
+
};
|
|
171
191
|
}
|
|
172
192
|
function readTemplate(project, slug) {
|
|
173
193
|
const dir = templateDir(project, slug);
|
|
174
194
|
const sources = sourceFilesIn(dir);
|
|
175
195
|
if (sources.length === 0)
|
|
176
|
-
throw new DevkitError(
|
|
196
|
+
throw new DevkitError(
|
|
197
|
+
`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`
|
|
198
|
+
);
|
|
177
199
|
if (sources.length > 1)
|
|
178
|
-
throw new DevkitError(
|
|
200
|
+
throw new DevkitError(
|
|
201
|
+
`${dir} holds ${sources.join(" and ")}; a template folder has one of them`,
|
|
202
|
+
"validation"
|
|
203
|
+
);
|
|
179
204
|
const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
|
|
180
205
|
const html = file ? "" : readText(join2(dir, "template.html")) ?? "";
|
|
181
|
-
const metaFile = readJson(
|
|
206
|
+
const metaFile = readJson(
|
|
207
|
+
join2(dir, "template.json"),
|
|
208
|
+
"template.json"
|
|
209
|
+
) ?? {};
|
|
182
210
|
if (file && metaFile.kind && metaFile.kind !== file.format)
|
|
183
|
-
throw new DevkitError(
|
|
211
|
+
throw new DevkitError(
|
|
212
|
+
`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${basename(file.path)}`,
|
|
213
|
+
"validation"
|
|
214
|
+
);
|
|
184
215
|
if (!file && isOfficeKind(metaFile.kind))
|
|
185
|
-
throw new DevkitError(
|
|
216
|
+
throw new DevkitError(
|
|
217
|
+
`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`,
|
|
218
|
+
"validation"
|
|
219
|
+
);
|
|
186
220
|
const meta = {
|
|
187
221
|
name: metaFile.name ?? titleFromSlug(slug),
|
|
188
222
|
kind: file?.format ?? metaFile.kind ?? "pdf",
|
|
@@ -190,19 +224,38 @@ function readTemplate(project, slug) {
|
|
|
190
224
|
description: metaFile.description ?? null,
|
|
191
225
|
tags: metaFile.tags ?? []
|
|
192
226
|
};
|
|
193
|
-
const settings = {
|
|
227
|
+
const settings = {
|
|
228
|
+
...readJson(
|
|
229
|
+
join2(dir, "settings.json"),
|
|
230
|
+
"settings.json"
|
|
231
|
+
) ?? {}
|
|
232
|
+
};
|
|
194
233
|
const header = file ? null : readText(join2(dir, "header.html"));
|
|
195
234
|
const footer = file ? null : readText(join2(dir, "footer.html"));
|
|
196
|
-
if (header !== null && header.trim())
|
|
197
|
-
|
|
235
|
+
if (header !== null && header.trim())
|
|
236
|
+
settings.header = { ...settings.header ?? {}, html: header };
|
|
237
|
+
if (footer !== null && footer.trim())
|
|
238
|
+
settings.footer = { ...settings.footer ?? {}, html: footer };
|
|
239
|
+
const design = readJson(
|
|
240
|
+
join2(dir, "design.json"),
|
|
241
|
+
"design.json"
|
|
242
|
+
);
|
|
243
|
+
if (design) settings.design = design;
|
|
198
244
|
const dataSets = {};
|
|
199
245
|
const dataDir = join2(dir, "data");
|
|
200
246
|
if (existsSync2(dataDir)) {
|
|
201
247
|
for (const file2 of readdirSync(dataDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
202
|
-
dataSets[basename(file2, ".json")] = readJson(
|
|
248
|
+
dataSets[basename(file2, ".json")] = readJson(
|
|
249
|
+
join2(dataDir, file2),
|
|
250
|
+
`data/${file2}`
|
|
251
|
+
);
|
|
203
252
|
}
|
|
204
253
|
}
|
|
205
|
-
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
|
|
254
|
+
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
|
|
255
|
+
html,
|
|
256
|
+
settings.header?.html,
|
|
257
|
+
settings.footer?.html
|
|
258
|
+
]);
|
|
206
259
|
return {
|
|
207
260
|
slug,
|
|
208
261
|
dir,
|
|
@@ -213,8 +266,14 @@ function readTemplate(project, slug) {
|
|
|
213
266
|
head: file ? "" : readText(join2(dir, "head.html")) ?? "",
|
|
214
267
|
settings,
|
|
215
268
|
dataSets,
|
|
216
|
-
dataSchema: readJson(
|
|
217
|
-
|
|
269
|
+
dataSchema: readJson(
|
|
270
|
+
join2(dir, "schema.json"),
|
|
271
|
+
"schema.json"
|
|
272
|
+
),
|
|
273
|
+
i18n: readJson(
|
|
274
|
+
join2(dir, "i18n.json"),
|
|
275
|
+
"i18n.json"
|
|
276
|
+
),
|
|
218
277
|
partials,
|
|
219
278
|
missingPartials: missing,
|
|
220
279
|
sharedPartials: shared
|
|
@@ -224,35 +283,68 @@ function defaultData(tpl, name) {
|
|
|
224
283
|
const names = Object.keys(tpl.dataSets);
|
|
225
284
|
const chosen = name ?? (names.includes("default") ? "default" : names[0]);
|
|
226
285
|
if (name && !(name in tpl.dataSets))
|
|
227
|
-
throw new DevkitError(
|
|
286
|
+
throw new DevkitError(
|
|
287
|
+
`Data set "${name}" not found; available: ${names.join(", ") || "none"}`
|
|
288
|
+
);
|
|
228
289
|
return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
|
|
229
290
|
}
|
|
230
291
|
function writeTemplate(project, slug, meta, version, file) {
|
|
231
292
|
const dir = templateDir(project, slug);
|
|
232
293
|
const office = isOfficeKind(meta.kind);
|
|
233
|
-
if (office && !file)
|
|
294
|
+
if (office && !file)
|
|
295
|
+
throw new DevkitError(
|
|
296
|
+
`${slug} is a ${meta.kind} template; its document is needed to write the folder`
|
|
297
|
+
);
|
|
234
298
|
mkdirSync(join2(dir, "data"), { recursive: true });
|
|
235
299
|
const settings = { ...version.settings ?? {} };
|
|
236
300
|
const header = office ? "" : settings.header?.html ?? "";
|
|
237
301
|
const footer = office ? "" : settings.footer?.html ?? "";
|
|
238
302
|
if (settings.header) settings.header = stripHtml(settings.header);
|
|
239
303
|
if (settings.footer) settings.footer = stripHtml(settings.footer);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
304
|
+
const design = settings.design;
|
|
305
|
+
delete settings.design;
|
|
306
|
+
writeFileSync2(
|
|
307
|
+
join2(dir, "template.json"),
|
|
308
|
+
JSON.stringify(meta, null, 2) + "\n"
|
|
309
|
+
);
|
|
310
|
+
for (const name of [
|
|
311
|
+
"template.html",
|
|
312
|
+
...officeKinds.map((k) => `template.${k}`)
|
|
313
|
+
])
|
|
314
|
+
if (name !== templateFileName(meta.kind) && existsSync2(join2(dir, name)))
|
|
315
|
+
rmSync(join2(dir, name));
|
|
243
316
|
if (office) writeFileSync2(join2(dir, templateFileName(meta.kind)), file);
|
|
244
317
|
else writeFileSync2(join2(dir, "template.html"), version.html ?? "");
|
|
245
318
|
writeOrRemove(join2(dir, "style.css"), office ? "" : version.css ?? "");
|
|
246
319
|
writeOrRemove(join2(dir, "head.html"), office ? "" : version.head ?? "");
|
|
247
320
|
writeOrRemove(join2(dir, "header.html"), header);
|
|
248
321
|
writeOrRemove(join2(dir, "footer.html"), footer);
|
|
249
|
-
writeFileSync2(
|
|
250
|
-
|
|
322
|
+
writeFileSync2(
|
|
323
|
+
join2(dir, "settings.json"),
|
|
324
|
+
JSON.stringify(settings, null, 2) + "\n"
|
|
325
|
+
);
|
|
326
|
+
writeOrRemove(
|
|
327
|
+
join2(dir, "design.json"),
|
|
328
|
+
design && typeof design === "object" ? JSON.stringify(design, null, 2) + "\n" : ""
|
|
329
|
+
);
|
|
330
|
+
writeFileSync2(
|
|
331
|
+
join2(dir, "data", "default.json"),
|
|
332
|
+
JSON.stringify(version.sample_data ?? {}, null, 2) + "\n"
|
|
333
|
+
);
|
|
251
334
|
for (const [name, data] of Object.entries(version.data_sets ?? {}))
|
|
252
335
|
if (name !== "default" && /^[\w.-]+$/.test(name))
|
|
253
|
-
writeFileSync2(
|
|
254
|
-
|
|
255
|
-
|
|
336
|
+
writeFileSync2(
|
|
337
|
+
join2(dir, "data", `${name}.json`),
|
|
338
|
+
JSON.stringify(data ?? {}, null, 2) + "\n"
|
|
339
|
+
);
|
|
340
|
+
writeOrRemove(
|
|
341
|
+
join2(dir, "schema.json"),
|
|
342
|
+
version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : ""
|
|
343
|
+
);
|
|
344
|
+
writeOrRemove(
|
|
345
|
+
join2(dir, "i18n.json"),
|
|
346
|
+
version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : ""
|
|
347
|
+
);
|
|
256
348
|
writePartials(project, version.partials ?? null);
|
|
257
349
|
return dir;
|
|
258
350
|
}
|
|
@@ -261,7 +353,10 @@ function writePartials(project, partials) {
|
|
|
261
353
|
const changed = [];
|
|
262
354
|
for (const [name, source] of Object.entries(partials ?? {})) {
|
|
263
355
|
if (!partialName.test(name)) continue;
|
|
264
|
-
const path = join2(
|
|
356
|
+
const path = join2(
|
|
357
|
+
project.partialsDir,
|
|
358
|
+
name.includes(".") ? name : `${name}.html`
|
|
359
|
+
);
|
|
265
360
|
if (readText(path) === source) continue;
|
|
266
361
|
mkdirSync(dirname2(path), { recursive: true });
|
|
267
362
|
writeFileSync2(path, source);
|
|
@@ -307,17 +402,36 @@ function versionPayload(tpl) {
|
|
|
307
402
|
function contentHash(tpl) {
|
|
308
403
|
if (tpl.file) {
|
|
309
404
|
const payload2 = officeVersionPayload(tpl);
|
|
310
|
-
const parts2 = [
|
|
405
|
+
const parts2 = [
|
|
406
|
+
tpl.file.format,
|
|
407
|
+
tpl.file.sha256,
|
|
408
|
+
payload2.settings,
|
|
409
|
+
payload2.sample_data,
|
|
410
|
+
payload2.data_sets,
|
|
411
|
+
payload2.data_schema,
|
|
412
|
+
payload2.i18n
|
|
413
|
+
];
|
|
311
414
|
return createHash("sha256").update(JSON.stringify(parts2)).digest("hex");
|
|
312
415
|
}
|
|
313
416
|
const payload = versionPayload(tpl);
|
|
314
|
-
const parts = [
|
|
417
|
+
const parts = [
|
|
418
|
+
payload.html,
|
|
419
|
+
payload.css,
|
|
420
|
+
payload.head,
|
|
421
|
+
payload.settings,
|
|
422
|
+
payload.sample_data,
|
|
423
|
+
payload.data_sets,
|
|
424
|
+
payload.data_schema,
|
|
425
|
+
payload.i18n
|
|
426
|
+
];
|
|
315
427
|
if (payload.partials) parts.push(payload.partials);
|
|
316
428
|
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
317
429
|
}
|
|
318
430
|
var statePath = (project) => join2(project.root, ".formfeed", "state.json");
|
|
319
431
|
function readState(project) {
|
|
320
|
-
return readJson(statePath(project), ".formfeed/state.json") ?? {
|
|
432
|
+
return readJson(statePath(project), ".formfeed/state.json") ?? {
|
|
433
|
+
templates: {}
|
|
434
|
+
};
|
|
321
435
|
}
|
|
322
436
|
function writeState(project, state) {
|
|
323
437
|
mkdirSync(join2(project.root, ".formfeed"), { recursive: true });
|
|
@@ -343,7 +457,8 @@ function partialResolver(project) {
|
|
|
343
457
|
return (name) => {
|
|
344
458
|
for (const candidate of [name, `${name}.html`]) {
|
|
345
459
|
const path = join2(project.partialsDir, candidate);
|
|
346
|
-
if (existsSync2(path) && statSync(path).isFile())
|
|
460
|
+
if (existsSync2(path) && statSync(path).isFile())
|
|
461
|
+
return readFileSync2(path, "utf8");
|
|
347
462
|
}
|
|
348
463
|
shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
|
|
349
464
|
const normalised = normalisePartialName2(name);
|
|
@@ -364,7 +479,12 @@ function recordSharedPartial(project, name, remote, source) {
|
|
|
364
479
|
const state = readState(project);
|
|
365
480
|
state.sharedPartials = {
|
|
366
481
|
...state.sharedPartials ?? {},
|
|
367
|
-
[name]: {
|
|
482
|
+
[name]: {
|
|
483
|
+
version: remote.version,
|
|
484
|
+
engine: remote.engine,
|
|
485
|
+
contentHash: partialContentHash(source),
|
|
486
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
487
|
+
}
|
|
368
488
|
};
|
|
369
489
|
writeState(project, state);
|
|
370
490
|
}
|
|
@@ -414,12 +534,14 @@ import {
|
|
|
414
534
|
analyzeOffice,
|
|
415
535
|
defaultHelpers,
|
|
416
536
|
defaultLimits,
|
|
537
|
+
designOutputHash,
|
|
417
538
|
flowDocument,
|
|
418
539
|
getEngine as getEngine2,
|
|
419
540
|
isOfficeKind as isOfficeKind2,
|
|
420
541
|
mergeSettings,
|
|
421
542
|
officeTextParts,
|
|
422
543
|
pagedDocument,
|
|
544
|
+
parseDesign,
|
|
423
545
|
renderOffice,
|
|
424
546
|
renderVersion
|
|
425
547
|
} from "@formfeed/engine";
|
|
@@ -435,7 +557,10 @@ function diagnose(tpl, sampleData) {
|
|
|
435
557
|
severity: "error",
|
|
436
558
|
code: "syntax-error",
|
|
437
559
|
message: e.message,
|
|
438
|
-
range: {
|
|
560
|
+
range: {
|
|
561
|
+
start: { line: e.line ?? 1, column: e.column ?? 1 },
|
|
562
|
+
end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 }
|
|
563
|
+
}
|
|
439
564
|
});
|
|
440
565
|
} else throw e;
|
|
441
566
|
}
|
|
@@ -445,7 +570,8 @@ function diagnose(tpl, sampleData) {
|
|
|
445
570
|
["footer", tpl.settings.footer?.html]
|
|
446
571
|
]) {
|
|
447
572
|
if (!source) continue;
|
|
448
|
-
for (const d of engine.analyze(source, { sampleData }).diagnostics)
|
|
573
|
+
for (const d of engine.analyze(source, { sampleData }).diagnostics)
|
|
574
|
+
diagnostics.push({ ...d, message: `${part}: ${d.message}` });
|
|
449
575
|
}
|
|
450
576
|
for (const name of tpl.missingPartials)
|
|
451
577
|
diagnostics.push({
|
|
@@ -455,8 +581,28 @@ function diagnose(tpl, sampleData) {
|
|
|
455
581
|
message: `No partial "${name}" in the project's partials folder; if it is shared by the organisation, formfeed partials pull brings it`,
|
|
456
582
|
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
|
|
457
583
|
});
|
|
584
|
+
const design = designDiagnostic(tpl);
|
|
585
|
+
if (design) diagnostics.push(design);
|
|
458
586
|
return diagnostics;
|
|
459
587
|
}
|
|
588
|
+
function designDiagnostic(tpl) {
|
|
589
|
+
if (tpl.settings.design === void 0) return null;
|
|
590
|
+
const parsed = parseDesign(tpl.settings.design);
|
|
591
|
+
if (!parsed.design)
|
|
592
|
+
return {
|
|
593
|
+
severity: "warning",
|
|
594
|
+
code: "design-invalid",
|
|
595
|
+
message: `design.json is not a design the app can open (${parsed.problems.slice(0, 3).join("; ")}); the template opens as code`,
|
|
596
|
+
range: nowhere
|
|
597
|
+
};
|
|
598
|
+
if (parsed.design.output === designOutputHash(tpl.html, tpl.css)) return null;
|
|
599
|
+
return {
|
|
600
|
+
severity: "warning",
|
|
601
|
+
code: "design-stale",
|
|
602
|
+
message: "template.html or style.css was edited after design.json was written; the app opens the template as code until you choose to keep the code or apply the design again",
|
|
603
|
+
range: nowhere
|
|
604
|
+
};
|
|
605
|
+
}
|
|
460
606
|
var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
|
|
461
607
|
function diagnoseOffice(tpl, file, sampleData) {
|
|
462
608
|
let analysis;
|
|
@@ -464,7 +610,14 @@ function diagnoseOffice(tpl, file, sampleData) {
|
|
|
464
610
|
analysis = analyzeOffice(file, { engine: tpl.meta.engine, sampleData });
|
|
465
611
|
} catch (e) {
|
|
466
612
|
const code = e.code;
|
|
467
|
-
return [
|
|
613
|
+
return [
|
|
614
|
+
{
|
|
615
|
+
severity: "error",
|
|
616
|
+
code: typeof code === "string" ? code : "office-document-invalid",
|
|
617
|
+
message: e instanceof Error ? e.message : String(e),
|
|
618
|
+
range: nowhere
|
|
619
|
+
}
|
|
620
|
+
];
|
|
468
621
|
}
|
|
469
622
|
return analysis.diagnostics.map((d) => ({
|
|
470
623
|
severity: d.severity,
|
|
@@ -476,17 +629,34 @@ function diagnoseOffice(tpl, file, sampleData) {
|
|
|
476
629
|
}));
|
|
477
630
|
}
|
|
478
631
|
async function renderOfficeLocal(project, tpl, data, options = {}) {
|
|
479
|
-
if (!tpl.file)
|
|
480
|
-
|
|
632
|
+
if (!tpl.file)
|
|
633
|
+
throw new DevkitError(
|
|
634
|
+
`${tpl.slug} is not a Word or PowerPoint template`,
|
|
635
|
+
"validation"
|
|
636
|
+
);
|
|
637
|
+
const context = renderContext(
|
|
638
|
+
project,
|
|
639
|
+
tpl,
|
|
640
|
+
options.assetBaseUrl,
|
|
641
|
+
options.brand
|
|
642
|
+
);
|
|
481
643
|
if (options.locale) context.locale = options.locale;
|
|
482
|
-
return renderOffice(tpl.file.bytes, {
|
|
644
|
+
return renderOffice(tpl.file.bytes, {
|
|
645
|
+
engine: tpl.meta.engine,
|
|
646
|
+
data,
|
|
647
|
+
context,
|
|
648
|
+
images: options.images,
|
|
649
|
+
random: options.random
|
|
650
|
+
});
|
|
483
651
|
}
|
|
484
652
|
function htmlSnapshot(rendered) {
|
|
485
653
|
let snapshot = rendered.document;
|
|
486
|
-
if (rendered.headerHtml)
|
|
654
|
+
if (rendered.headerHtml)
|
|
655
|
+
snapshot += `
|
|
487
656
|
<!-- formfeed:header -->
|
|
488
657
|
${rendered.headerHtml}`;
|
|
489
|
-
if (rendered.footerHtml)
|
|
658
|
+
if (rendered.footerHtml)
|
|
659
|
+
snapshot += `
|
|
490
660
|
<!-- formfeed:footer -->
|
|
491
661
|
${rendered.footerHtml}`;
|
|
492
662
|
return snapshot;
|
|
@@ -539,7 +709,14 @@ async function renderLocal(project, tpl, data, options) {
|
|
|
539
709
|
const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
|
|
540
710
|
if (options.locale) ctx.locale = options.locale;
|
|
541
711
|
return renderVersion(
|
|
542
|
-
{
|
|
712
|
+
{
|
|
713
|
+
engine: tpl.meta.engine,
|
|
714
|
+
html: tpl.html,
|
|
715
|
+
css: tpl.css,
|
|
716
|
+
head: tpl.head,
|
|
717
|
+
settings: mergeSettings(tpl.settings),
|
|
718
|
+
kind
|
|
719
|
+
},
|
|
543
720
|
data,
|
|
544
721
|
ctx,
|
|
545
722
|
options.mode,
|
|
@@ -549,16 +726,26 @@ async function renderLocal(project, tpl, data, options) {
|
|
|
549
726
|
function renderedSettings(rendered) {
|
|
550
727
|
const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
|
|
551
728
|
const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
|
|
552
|
-
const { settings } = rendered;
|
|
729
|
+
const { design: _design, ...settings } = rendered.settings;
|
|
730
|
+
void _design;
|
|
553
731
|
return {
|
|
554
732
|
...settings,
|
|
555
733
|
...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
|
|
556
734
|
...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
|
|
557
|
-
...settings.pdf?.metadata?.title !== void 0 ? {
|
|
735
|
+
...settings.pdf?.metadata?.title !== void 0 ? {
|
|
736
|
+
pdf: {
|
|
737
|
+
...settings.pdf,
|
|
738
|
+
metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" }
|
|
739
|
+
}
|
|
740
|
+
} : {}
|
|
558
741
|
};
|
|
559
742
|
}
|
|
560
743
|
function htmlKind(tpl) {
|
|
561
|
-
if (isOfficeKind2(tpl.meta.kind))
|
|
744
|
+
if (isOfficeKind2(tpl.meta.kind))
|
|
745
|
+
throw new DevkitError(
|
|
746
|
+
`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`,
|
|
747
|
+
"validation"
|
|
748
|
+
);
|
|
562
749
|
return tpl.meta.kind;
|
|
563
750
|
}
|
|
564
751
|
function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formfeed/devkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Formfeed template folders, project config and local rendering, shared by the CLI and @formfeed/testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
],
|
|
31
31
|
"sideEffects": false,
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@formfeed/engine": "0.3.
|
|
34
|
-
"@formfeed/sdk": "0.3.
|
|
33
|
+
"@formfeed/engine": "0.3.11",
|
|
34
|
+
"@formfeed/sdk": "0.3.11"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"formfeed",
|
package/types/lib/project.d.cts
CHANGED
|
@@ -5,7 +5,8 @@ import type { Project } from './project-config.cjs';
|
|
|
5
5
|
* The template folder of spec 15 §2. `template.json` (name, kind, engine, description, tags) is
|
|
6
6
|
* the one addition to the spec's layout: `push` needs it to create a template that does not exist
|
|
7
7
|
* remotely, `pull` writes it. Header and footer live in their own files and fold back into
|
|
8
|
-
* `settings.header.html` / `settings.footer.html`, as the editor's tabs do
|
|
8
|
+
* `settings.header.html` / `settings.footer.html`, as the editor's tabs do; a visual design lives in
|
|
9
|
+
* `design.json` and folds back into `settings.design`.
|
|
9
10
|
*/
|
|
10
11
|
export interface TemplateMeta {
|
|
11
12
|
name: string;
|
package/types/lib/project.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type { Project } from './project-config.js';
|
|
|
5
5
|
* The template folder of spec 15 §2. `template.json` (name, kind, engine, description, tags) is
|
|
6
6
|
* the one addition to the spec's layout: `push` needs it to create a template that does not exist
|
|
7
7
|
* remotely, `pull` writes it. Header and footer live in their own files and fold back into
|
|
8
|
-
* `settings.header.html` / `settings.footer.html`, as the editor's tabs do
|
|
8
|
+
* `settings.header.html` / `settings.footer.html`, as the editor's tabs do; a visual design lives in
|
|
9
|
+
* `design.json` and folds back into `settings.design`.
|
|
9
10
|
*/
|
|
10
11
|
export interface TemplateMeta {
|
|
11
12
|
name: string;
|