@formfeed/devkit 0.1.0 → 0.3.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/index.cjs +963 -42
- package/index.js +941 -38
- package/package.json +3 -3
- package/types/index.d.cts +12 -3
- package/types/index.d.ts +12 -3
- package/types/lib/brand.d.cts +14 -0
- package/types/lib/brand.d.ts +14 -0
- package/types/lib/files.d.cts +32 -0
- package/types/lib/files.d.ts +32 -0
- package/types/lib/local-render.d.cts +39 -3
- package/types/lib/local-render.d.ts +39 -3
- package/types/lib/partials.d.cts +19 -0
- package/types/lib/partials.d.ts +19 -0
- package/types/lib/project-config.d.cts +16 -0
- package/types/lib/project-config.d.ts +16 -0
- package/types/lib/project.d.cts +83 -7
- package/types/lib/project.d.ts +83 -7
- package/types/lib/redact.d.cts +19 -0
- package/types/lib/redact.d.ts +19 -0
- package/types/lib/types/emit-python.d.cts +6 -0
- package/types/lib/types/emit-python.d.ts +6 -0
- package/types/lib/types/emit-ts.d.cts +10 -0
- package/types/lib/types/emit-ts.d.ts +10 -0
- package/types/lib/types/index.d.cts +15 -0
- package/types/lib/types/index.d.ts +15 -0
- package/types/lib/types/model.d.cts +81 -0
- package/types/lib/types/model.d.ts +81 -0
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
13
13
|
var defaultProjectConfig = {
|
|
14
14
|
templatesDir: "templates",
|
|
15
15
|
partialsDir: "partials",
|
|
16
|
+
filesDir: "files",
|
|
16
17
|
engine: "jinja2",
|
|
17
18
|
ignore: ["**/drafts/**"]
|
|
18
19
|
};
|
|
@@ -35,7 +36,14 @@ function loadProject(configPath) {
|
|
|
35
36
|
}
|
|
36
37
|
const config = { ...defaultProjectConfig, ...raw };
|
|
37
38
|
const root = dirname(configPath);
|
|
38
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
root,
|
|
41
|
+
configPath,
|
|
42
|
+
config,
|
|
43
|
+
templatesDir: resolve(root, config.templatesDir),
|
|
44
|
+
partialsDir: resolve(root, config.partialsDir),
|
|
45
|
+
filesDir: resolve(root, config.filesDir)
|
|
46
|
+
};
|
|
39
47
|
}
|
|
40
48
|
function projectAround(templateFolder) {
|
|
41
49
|
const dir = resolve(templateFolder);
|
|
@@ -46,9 +54,10 @@ function projectAround(templateFolder) {
|
|
|
46
54
|
return {
|
|
47
55
|
root,
|
|
48
56
|
configPath: null,
|
|
49
|
-
config: { ...defaultProjectConfig, templatesDir, partialsDir: join(root, "partials") },
|
|
57
|
+
config: { ...defaultProjectConfig, templatesDir, partialsDir: join(root, "partials"), filesDir: join(root, "files") },
|
|
50
58
|
templatesDir,
|
|
51
|
-
partialsDir: join(root, "partials")
|
|
59
|
+
partialsDir: join(root, "partials"),
|
|
60
|
+
filesDir: join(root, "files")
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
function writeProjectConfig(root, config) {
|
|
@@ -61,59 +70,128 @@ function writeProjectConfig(root, config) {
|
|
|
61
70
|
// packages/devkit/src/lib/project.ts
|
|
62
71
|
import { createHash } from "node:crypto";
|
|
63
72
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
64
|
-
import { basename, join as join2 } from "node:path";
|
|
73
|
+
import { basename, dirname as dirname2, extname, join as join2 } from "node:path";
|
|
74
|
+
import {
|
|
75
|
+
isOfficeKind,
|
|
76
|
+
normalisePartialName as normalisePartialName2,
|
|
77
|
+
officeKinds
|
|
78
|
+
} from "@formfeed/engine";
|
|
79
|
+
|
|
80
|
+
// packages/devkit/src/lib/partials.ts
|
|
81
|
+
import { getEngine, normalisePartialName } from "@formfeed/engine";
|
|
82
|
+
function includeNames(engine, source) {
|
|
83
|
+
try {
|
|
84
|
+
return getEngine(engine).analyze(source).includes.map((include) => include.name);
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function collectPartials(project, engine, sources) {
|
|
90
|
+
const resolve3 = partialResolver(project);
|
|
91
|
+
const sharedNames = sharedPartialNames(project, engine);
|
|
92
|
+
const partials = {};
|
|
93
|
+
const missing = /* @__PURE__ */ new Set();
|
|
94
|
+
const shared = /* @__PURE__ */ new Set();
|
|
95
|
+
const queue = sources.filter((source) => Boolean(source));
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
while (queue.length) {
|
|
98
|
+
for (const name of includeNames(engine, queue.shift())) {
|
|
99
|
+
if (seen.has(name)) continue;
|
|
100
|
+
seen.add(name);
|
|
101
|
+
const source = resolve3(name);
|
|
102
|
+
if (sharedNames.has(normalisePartialName(name))) {
|
|
103
|
+
shared.add(normalisePartialName(name));
|
|
104
|
+
if (source !== void 0) queue.push(source);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (source === void 0) {
|
|
108
|
+
missing.add(name);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
partials[name] = source;
|
|
112
|
+
queue.push(source);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { partials, missing: [...missing].sort(), shared: [...shared].sort() };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// packages/devkit/src/lib/project.ts
|
|
65
119
|
var readText = (path) => existsSync2(path) ? readFileSync2(path, "utf8") : null;
|
|
66
120
|
function readJson(path, what) {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
121
|
+
const text2 = readText(path);
|
|
122
|
+
if (text2 === null) return null;
|
|
69
123
|
try {
|
|
70
|
-
return JSON.parse(
|
|
124
|
+
return JSON.parse(text2);
|
|
71
125
|
} catch (e) {
|
|
72
126
|
throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
|
|
73
127
|
}
|
|
74
128
|
}
|
|
129
|
+
var templateFileName = (kind) => isOfficeKind(kind) ? `template.${kind}` : "template.html";
|
|
130
|
+
function sourceFilesIn(dir) {
|
|
131
|
+
return ["template.html", ...officeKinds.map((k) => `template.${k}`)].filter((name) => existsSync2(join2(dir, name)));
|
|
132
|
+
}
|
|
75
133
|
function listTemplateSlugs(project) {
|
|
76
134
|
if (!existsSync2(project.templatesDir)) return [];
|
|
77
|
-
return readdirSync(project.templatesDir).filter((name) =>
|
|
135
|
+
return readdirSync(project.templatesDir).filter((name) => sourceFilesIn(join2(project.templatesDir, name)).length > 0).sort();
|
|
78
136
|
}
|
|
79
137
|
function templateDir(project, slug) {
|
|
80
138
|
return join2(project.templatesDir, slug);
|
|
81
139
|
}
|
|
140
|
+
var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
141
|
+
function readOfficeFile(dir, name) {
|
|
142
|
+
const path = join2(dir, name);
|
|
143
|
+
const bytes = new Uint8Array(readFileSync2(path));
|
|
144
|
+
return { format: extname(name).slice(1), path, bytes, sha256: sha256Of(bytes) };
|
|
145
|
+
}
|
|
82
146
|
function readTemplate(project, slug) {
|
|
83
147
|
const dir = templateDir(project, slug);
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
148
|
+
const sources = sourceFilesIn(dir);
|
|
149
|
+
if (sources.length === 0)
|
|
150
|
+
throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`);
|
|
151
|
+
if (sources.length > 1)
|
|
152
|
+
throw new DevkitError(`${dir} holds ${sources.join(" and ")}; a template folder has one of them`, "validation");
|
|
153
|
+
const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
|
|
154
|
+
const html = file ? "" : readText(join2(dir, "template.html")) ?? "";
|
|
86
155
|
const metaFile = readJson(join2(dir, "template.json"), "template.json") ?? {};
|
|
156
|
+
if (file && metaFile.kind && metaFile.kind !== file.format)
|
|
157
|
+
throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${basename(file.path)}`, "validation");
|
|
158
|
+
if (!file && isOfficeKind(metaFile.kind))
|
|
159
|
+
throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`, "validation");
|
|
87
160
|
const meta = {
|
|
88
161
|
name: metaFile.name ?? titleFromSlug(slug),
|
|
89
|
-
kind: metaFile.kind ?? "pdf",
|
|
162
|
+
kind: file?.format ?? metaFile.kind ?? "pdf",
|
|
90
163
|
engine: metaFile.engine ?? project.config.engine,
|
|
91
164
|
description: metaFile.description ?? null,
|
|
92
165
|
tags: metaFile.tags ?? []
|
|
93
166
|
};
|
|
94
167
|
const settings = { ...readJson(join2(dir, "settings.json"), "settings.json") ?? {} };
|
|
95
|
-
const header = readText(join2(dir, "header.html"));
|
|
96
|
-
const footer = readText(join2(dir, "footer.html"));
|
|
168
|
+
const header = file ? null : readText(join2(dir, "header.html"));
|
|
169
|
+
const footer = file ? null : readText(join2(dir, "footer.html"));
|
|
97
170
|
if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
|
|
98
171
|
if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
|
|
99
172
|
const dataSets = {};
|
|
100
173
|
const dataDir = join2(dir, "data");
|
|
101
174
|
if (existsSync2(dataDir)) {
|
|
102
|
-
for (const
|
|
103
|
-
dataSets[basename(
|
|
175
|
+
for (const file2 of readdirSync(dataDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
176
|
+
dataSets[basename(file2, ".json")] = readJson(join2(dataDir, file2), `data/${file2}`);
|
|
104
177
|
}
|
|
105
178
|
}
|
|
179
|
+
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
|
|
106
180
|
return {
|
|
107
181
|
slug,
|
|
108
182
|
dir,
|
|
109
183
|
meta,
|
|
184
|
+
file,
|
|
110
185
|
html,
|
|
111
|
-
css: readText(join2(dir, "style.css")) ?? "",
|
|
112
|
-
head: readText(join2(dir, "head.html")) ?? "",
|
|
186
|
+
css: file ? "" : readText(join2(dir, "style.css")) ?? "",
|
|
187
|
+
head: file ? "" : readText(join2(dir, "head.html")) ?? "",
|
|
113
188
|
settings,
|
|
114
189
|
dataSets,
|
|
115
190
|
dataSchema: readJson(join2(dir, "schema.json"), "schema.json"),
|
|
116
|
-
i18n: readJson(join2(dir, "i18n.json"), "i18n.json")
|
|
191
|
+
i18n: readJson(join2(dir, "i18n.json"), "i18n.json"),
|
|
192
|
+
partials,
|
|
193
|
+
missingPartials: missing,
|
|
194
|
+
sharedPartials: shared
|
|
117
195
|
};
|
|
118
196
|
}
|
|
119
197
|
function defaultData(tpl, name) {
|
|
@@ -123,18 +201,23 @@ function defaultData(tpl, name) {
|
|
|
123
201
|
throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
|
|
124
202
|
return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
|
|
125
203
|
}
|
|
126
|
-
function writeTemplate(project, slug, meta, version) {
|
|
204
|
+
function writeTemplate(project, slug, meta, version, file) {
|
|
127
205
|
const dir = templateDir(project, slug);
|
|
206
|
+
const office = isOfficeKind(meta.kind);
|
|
207
|
+
if (office && !file) throw new DevkitError(`${slug} is a ${meta.kind} template; its document is needed to write the folder`);
|
|
128
208
|
mkdirSync(join2(dir, "data"), { recursive: true });
|
|
129
209
|
const settings = { ...version.settings ?? {} };
|
|
130
|
-
const header = settings.header?.html ?? "";
|
|
131
|
-
const footer = settings.footer?.html ?? "";
|
|
210
|
+
const header = office ? "" : settings.header?.html ?? "";
|
|
211
|
+
const footer = office ? "" : settings.footer?.html ?? "";
|
|
132
212
|
if (settings.header) settings.header = stripHtml(settings.header);
|
|
133
213
|
if (settings.footer) settings.footer = stripHtml(settings.footer);
|
|
134
214
|
writeFileSync2(join2(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
215
|
+
for (const name of ["template.html", ...officeKinds.map((k) => `template.${k}`)])
|
|
216
|
+
if (name !== templateFileName(meta.kind) && existsSync2(join2(dir, name))) rmSync(join2(dir, name));
|
|
217
|
+
if (office) writeFileSync2(join2(dir, templateFileName(meta.kind)), file);
|
|
218
|
+
else writeFileSync2(join2(dir, "template.html"), version.html ?? "");
|
|
219
|
+
writeOrRemove(join2(dir, "style.css"), office ? "" : version.css ?? "");
|
|
220
|
+
writeOrRemove(join2(dir, "head.html"), office ? "" : version.head ?? "");
|
|
138
221
|
writeOrRemove(join2(dir, "header.html"), header);
|
|
139
222
|
writeOrRemove(join2(dir, "footer.html"), footer);
|
|
140
223
|
writeFileSync2(join2(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
@@ -144,8 +227,22 @@ function writeTemplate(project, slug, meta, version) {
|
|
|
144
227
|
writeFileSync2(join2(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
|
|
145
228
|
writeOrRemove(join2(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
|
|
146
229
|
writeOrRemove(join2(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
|
|
230
|
+
writePartials(project, version.partials ?? null);
|
|
147
231
|
return dir;
|
|
148
232
|
}
|
|
233
|
+
var partialName = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/;
|
|
234
|
+
function writePartials(project, partials) {
|
|
235
|
+
const changed = [];
|
|
236
|
+
for (const [name, source] of Object.entries(partials ?? {})) {
|
|
237
|
+
if (!partialName.test(name)) continue;
|
|
238
|
+
const path = join2(project.partialsDir, name.includes(".") ? name : `${name}.html`);
|
|
239
|
+
if (readText(path) === source) continue;
|
|
240
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
241
|
+
writeFileSync2(path, source);
|
|
242
|
+
changed.push(path);
|
|
243
|
+
}
|
|
244
|
+
return changed;
|
|
245
|
+
}
|
|
149
246
|
function stripHtml(part) {
|
|
150
247
|
const { html: _html, ...rest } = part;
|
|
151
248
|
void _html;
|
|
@@ -155,15 +252,12 @@ function writeOrRemove(path, content) {
|
|
|
155
252
|
if (content) writeFileSync2(path, content);
|
|
156
253
|
else if (existsSync2(path)) rmSync(path);
|
|
157
254
|
}
|
|
158
|
-
function
|
|
255
|
+
function commonPayload(tpl) {
|
|
159
256
|
const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
|
|
160
257
|
const named = Object.fromEntries(
|
|
161
258
|
Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
|
|
162
259
|
);
|
|
163
260
|
return {
|
|
164
|
-
html: tpl.html,
|
|
165
|
-
css: tpl.css,
|
|
166
|
-
head: tpl.head,
|
|
167
261
|
settings: tpl.settings,
|
|
168
262
|
sample_data: sample ?? {},
|
|
169
263
|
data_sets: named,
|
|
@@ -171,9 +265,29 @@ function versionPayload(tpl) {
|
|
|
171
265
|
i18n: tpl.i18n
|
|
172
266
|
};
|
|
173
267
|
}
|
|
268
|
+
function officeVersionPayload(tpl) {
|
|
269
|
+
return commonPayload(tpl);
|
|
270
|
+
}
|
|
271
|
+
function versionPayload(tpl) {
|
|
272
|
+
return {
|
|
273
|
+
html: tpl.html,
|
|
274
|
+
css: tpl.css,
|
|
275
|
+
head: tpl.head,
|
|
276
|
+
...commonPayload(tpl),
|
|
277
|
+
// only when the template includes one, so a project without partials sends what it always sent
|
|
278
|
+
...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
174
281
|
function contentHash(tpl) {
|
|
282
|
+
if (tpl.file) {
|
|
283
|
+
const payload2 = officeVersionPayload(tpl);
|
|
284
|
+
const parts2 = [tpl.file.format, tpl.file.sha256, payload2.settings, payload2.sample_data, payload2.data_sets, payload2.data_schema, payload2.i18n];
|
|
285
|
+
return createHash("sha256").update(JSON.stringify(parts2)).digest("hex");
|
|
286
|
+
}
|
|
175
287
|
const payload = versionPayload(tpl);
|
|
176
|
-
|
|
288
|
+
const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
|
|
289
|
+
if (payload.partials) parts.push(payload.partials);
|
|
290
|
+
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
177
291
|
}
|
|
178
292
|
var statePath = (project) => join2(project.root, ".formfeed", "state.json");
|
|
179
293
|
function readState(project) {
|
|
@@ -190,6 +304,7 @@ function recordSync(project, slug, version, tpl) {
|
|
|
190
304
|
number: version.number,
|
|
191
305
|
status: version.status,
|
|
192
306
|
contentHash: contentHash(tpl),
|
|
307
|
+
...tpl.file ? { fileSha256: tpl.file.sha256 } : {},
|
|
193
308
|
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
194
309
|
};
|
|
195
310
|
writeState(project, state);
|
|
@@ -198,28 +313,93 @@ function titleFromSlug(slug) {
|
|
|
198
313
|
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
199
314
|
}
|
|
200
315
|
function partialResolver(project) {
|
|
316
|
+
let shared = null;
|
|
201
317
|
return (name) => {
|
|
202
318
|
for (const candidate of [name, `${name}.html`]) {
|
|
203
319
|
const path = join2(project.partialsDir, candidate);
|
|
204
320
|
if (existsSync2(path) && statSync(path).isFile()) return readFileSync2(path, "utf8");
|
|
205
321
|
}
|
|
206
|
-
|
|
322
|
+
shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
|
|
323
|
+
const normalised = normalisePartialName2(name);
|
|
324
|
+
if (!shared.has(normalised)) return void 0;
|
|
325
|
+
return readText(sharedPartialPath(project, normalised)) ?? void 0;
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function sharedPartialPath(project, name) {
|
|
329
|
+
return join2(project.partialsDir, `${name}.html`);
|
|
330
|
+
}
|
|
331
|
+
function sharedPartialNames(project, engine) {
|
|
332
|
+
return new Set(
|
|
333
|
+
Object.entries(readState(project).sharedPartials ?? {}).filter(([, partial]) => partial.engine === engine).map(([name]) => normalisePartialName2(name))
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
var partialContentHash = (source) => createHash("sha256").update(source).digest("hex");
|
|
337
|
+
function recordSharedPartial(project, name, remote, source) {
|
|
338
|
+
const state = readState(project);
|
|
339
|
+
state.sharedPartials = {
|
|
340
|
+
...state.sharedPartials ?? {},
|
|
341
|
+
[name]: { version: remote.version, engine: remote.engine, contentHash: partialContentHash(source), syncedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
207
342
|
};
|
|
343
|
+
writeState(project, state);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// packages/devkit/src/lib/brand.ts
|
|
347
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
348
|
+
import { join as join3 } from "node:path";
|
|
349
|
+
import { emptyBrand } from "@formfeed/engine";
|
|
350
|
+
var brandPath = (project) => join3(project.root, ".formfeed", "brand.json");
|
|
351
|
+
var text = (value) => typeof value === "string" ? value : null;
|
|
352
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
353
|
+
function brandFromJson(json) {
|
|
354
|
+
const body = record(json);
|
|
355
|
+
const empty = emptyBrand();
|
|
356
|
+
const colors = Object.fromEntries(Object.entries(record(body["colors"])).filter((entry) => typeof entry[1] === "string"));
|
|
357
|
+
const fonts = record(body["fonts"]);
|
|
358
|
+
const logo = record(body["logo"]);
|
|
359
|
+
return {
|
|
360
|
+
version: typeof body["version"] === "number" ? body["version"] : empty.version,
|
|
361
|
+
name: text(body["name"]),
|
|
362
|
+
colors,
|
|
363
|
+
fonts: { heading: text(fonts["heading"]), body: text(fonts["body"]) },
|
|
364
|
+
font_size: text(body["font_size"]),
|
|
365
|
+
logo: { primary: text(logo["primary"]), inverse: text(logo["inverse"]), mark: text(logo["mark"]) },
|
|
366
|
+
legal_footer: text(body["legal_footer"])
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function readBrand(project) {
|
|
370
|
+
const path = brandPath(project);
|
|
371
|
+
if (!existsSync3(path)) return emptyBrand();
|
|
372
|
+
try {
|
|
373
|
+
return brandFromJson(JSON.parse(readFileSync3(path, "utf8")));
|
|
374
|
+
} catch (e) {
|
|
375
|
+
throw new DevkitError(`.formfeed/brand.json is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function writeBrand(project, brand) {
|
|
379
|
+
mkdirSync2(join3(project.root, ".formfeed"), { recursive: true });
|
|
380
|
+
const path = brandPath(project);
|
|
381
|
+
writeFileSync3(path, JSON.stringify(brand, null, 2) + "\n");
|
|
382
|
+
return path;
|
|
208
383
|
}
|
|
209
384
|
|
|
210
385
|
// packages/devkit/src/lib/local-render.ts
|
|
211
386
|
import {
|
|
212
387
|
EngineSyntaxError,
|
|
388
|
+
analyzeOffice,
|
|
213
389
|
defaultHelpers,
|
|
214
390
|
defaultLimits,
|
|
215
391
|
flowDocument,
|
|
216
|
-
getEngine,
|
|
392
|
+
getEngine as getEngine2,
|
|
393
|
+
isOfficeKind as isOfficeKind2,
|
|
217
394
|
mergeSettings,
|
|
395
|
+
officeTextParts,
|
|
218
396
|
pagedDocument,
|
|
397
|
+
renderOffice,
|
|
219
398
|
renderVersion
|
|
220
399
|
} from "@formfeed/engine";
|
|
221
400
|
function diagnose(tpl, sampleData) {
|
|
222
|
-
|
|
401
|
+
if (tpl.file) return diagnoseOffice(tpl, tpl.file.bytes, sampleData);
|
|
402
|
+
const engine = getEngine2(tpl.meta.engine);
|
|
223
403
|
const diagnostics = [];
|
|
224
404
|
try {
|
|
225
405
|
engine.compile(tpl.html, { name: tpl.slug });
|
|
@@ -241,9 +421,68 @@ function diagnose(tpl, sampleData) {
|
|
|
241
421
|
if (!source) continue;
|
|
242
422
|
for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
|
|
243
423
|
}
|
|
424
|
+
for (const name of tpl.missingPartials)
|
|
425
|
+
diagnostics.push({
|
|
426
|
+
severity: "error",
|
|
427
|
+
code: "missing-partial",
|
|
428
|
+
message: `No partial "${name}" in the project's partials folder`,
|
|
429
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
|
|
430
|
+
});
|
|
244
431
|
return diagnostics;
|
|
245
432
|
}
|
|
246
|
-
|
|
433
|
+
var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
|
|
434
|
+
function diagnoseOffice(tpl, file, sampleData) {
|
|
435
|
+
let analysis;
|
|
436
|
+
try {
|
|
437
|
+
analysis = analyzeOffice(file, { engine: tpl.meta.engine, sampleData });
|
|
438
|
+
} catch (e) {
|
|
439
|
+
const code = e.code;
|
|
440
|
+
return [{ severity: "error", code: typeof code === "string" ? code : "office-document-invalid", message: e instanceof Error ? e.message : String(e), range: nowhere }];
|
|
441
|
+
}
|
|
442
|
+
return analysis.diagnostics.map((d) => ({
|
|
443
|
+
severity: d.severity,
|
|
444
|
+
code: d.code,
|
|
445
|
+
message: d.text ? `${d.message} (${d.text})` : d.message,
|
|
446
|
+
range: nowhere,
|
|
447
|
+
part: d.part,
|
|
448
|
+
paragraph: d.paragraph
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
async function renderOfficeLocal(project, tpl, data, options = {}) {
|
|
452
|
+
if (!tpl.file) throw new DevkitError(`${tpl.slug} is not a Word or PowerPoint template`, "validation");
|
|
453
|
+
const context = renderContext(project, tpl, options.assetBaseUrl, options.brand);
|
|
454
|
+
if (options.locale) context.locale = options.locale;
|
|
455
|
+
return renderOffice(tpl.file.bytes, { engine: tpl.meta.engine, data, context, images: options.images, random: options.random });
|
|
456
|
+
}
|
|
457
|
+
function officeSnapshot(filled) {
|
|
458
|
+
return officeTextParts(filled).map((part) => `--- ${part.name} ---
|
|
459
|
+
${prettyXml(part.xml)}`).join("\n");
|
|
460
|
+
}
|
|
461
|
+
function prettyXml(xml) {
|
|
462
|
+
const lines = [];
|
|
463
|
+
let depth = 0;
|
|
464
|
+
let open = false;
|
|
465
|
+
for (const token of xml.match(/<[^>]*>|[^<]+/g) ?? []) {
|
|
466
|
+
if (token.startsWith("</")) {
|
|
467
|
+
depth = Math.max(0, depth - 1);
|
|
468
|
+
if (open && lines.length) lines[lines.length - 1] += token;
|
|
469
|
+
else lines.push(" ".repeat(depth) + token);
|
|
470
|
+
open = false;
|
|
471
|
+
} else if (token.startsWith("<?") || token.startsWith("<!")) {
|
|
472
|
+
lines.push(" ".repeat(depth) + token);
|
|
473
|
+
open = false;
|
|
474
|
+
} else if (token.startsWith("<")) {
|
|
475
|
+
lines.push(" ".repeat(depth) + token);
|
|
476
|
+
open = !token.endsWith("/>");
|
|
477
|
+
if (open) depth++;
|
|
478
|
+
} else if (token.trim() || open) {
|
|
479
|
+
if (lines.length) lines[lines.length - 1] += token;
|
|
480
|
+
else lines.push(token);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return lines.join("\n") + "\n";
|
|
484
|
+
}
|
|
485
|
+
function renderContext(project, tpl, assetBaseUrl, brand) {
|
|
247
486
|
const settings = mergeSettings(tpl.settings);
|
|
248
487
|
return {
|
|
249
488
|
locale: settings.locale ?? "en",
|
|
@@ -252,50 +491,714 @@ function renderContext(project, tpl) {
|
|
|
252
491
|
partials: partialResolver(project),
|
|
253
492
|
helpers: defaultHelpers(),
|
|
254
493
|
limits: defaultLimits,
|
|
255
|
-
i18n: tpl.i18n ?? void 0
|
|
494
|
+
i18n: tpl.i18n ?? void 0,
|
|
495
|
+
assetBaseUrl,
|
|
496
|
+
// the organisation's kit from `formfeed brand pull`, else the empty kit the server uses too
|
|
497
|
+
brand: brand ?? readBrand(project)
|
|
256
498
|
};
|
|
257
499
|
}
|
|
258
500
|
async function renderLocal(project, tpl, data, options) {
|
|
259
|
-
const
|
|
501
|
+
const kind = htmlKind(tpl);
|
|
502
|
+
const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
|
|
260
503
|
if (options.locale) ctx.locale = options.locale;
|
|
261
504
|
return renderVersion(
|
|
262
|
-
{ engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: mergeSettings(tpl.settings), kind
|
|
505
|
+
{ engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: mergeSettings(tpl.settings), kind },
|
|
263
506
|
data,
|
|
264
507
|
ctx,
|
|
265
508
|
options.mode,
|
|
266
509
|
options.vendor ? { vendor: options.vendor } : {}
|
|
267
510
|
);
|
|
268
511
|
}
|
|
512
|
+
function htmlKind(tpl) {
|
|
513
|
+
if (isOfficeKind2(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
|
|
514
|
+
return tpl.meta.kind;
|
|
515
|
+
}
|
|
269
516
|
function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
|
517
|
+
const kind = htmlKind(tpl);
|
|
270
518
|
const draft = {
|
|
271
519
|
document: rendered.document,
|
|
272
520
|
headerHtml: rendered.headerHtml,
|
|
273
521
|
footerHtml: rendered.footerHtml,
|
|
274
522
|
settings: rendered.settings,
|
|
275
|
-
kind
|
|
523
|
+
kind
|
|
276
524
|
};
|
|
277
|
-
return mode === "paged" &&
|
|
525
|
+
return mode === "paged" && kind === "pdf" ? pagedDocument(draft, { pagedScriptUrl }) : flowDocument(draft);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// packages/devkit/src/lib/files.ts
|
|
529
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
530
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
531
|
+
import { dirname as dirname3, join as join4, relative, resolve as resolve2, sep } from "node:path";
|
|
532
|
+
|
|
533
|
+
// packages/api-types/src/lib/assets.ts
|
|
534
|
+
var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
535
|
+
var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
|
|
536
|
+
var ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
537
|
+
var ASSET_CONTENT_TYPES = {
|
|
538
|
+
"image/png": "png",
|
|
539
|
+
"image/jpeg": "jpg",
|
|
540
|
+
"image/webp": "webp",
|
|
541
|
+
"image/gif": "gif",
|
|
542
|
+
"image/svg+xml": "svg",
|
|
543
|
+
"image/avif": "avif",
|
|
544
|
+
"application/pdf": "pdf"
|
|
545
|
+
};
|
|
546
|
+
function isAssetName(name) {
|
|
547
|
+
return name.length > 0 && name.length <= 200 && ASSET_NAME_PATTERN.test(name) && !name.split("/").includes("..");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// packages/devkit/src/lib/files.ts
|
|
551
|
+
var byExtension = Object.fromEntries(
|
|
552
|
+
Object.entries(ASSET_CONTENT_TYPES).map(([type, ext]) => [ext, type])
|
|
553
|
+
);
|
|
554
|
+
byExtension["jpeg"] = "image/jpeg";
|
|
555
|
+
function contentTypeFor(name) {
|
|
556
|
+
return byExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? null;
|
|
557
|
+
}
|
|
558
|
+
function sha256File(bytes) {
|
|
559
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
560
|
+
}
|
|
561
|
+
function listLocalFiles(project) {
|
|
562
|
+
const listing = { files: [], skipped: [] };
|
|
563
|
+
if (!existsSync4(project.filesDir)) return listing;
|
|
564
|
+
const walk2 = (dir) => {
|
|
565
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
566
|
+
if (entry.name.startsWith(".")) continue;
|
|
567
|
+
const path = join4(dir, entry.name);
|
|
568
|
+
if (entry.isDirectory()) {
|
|
569
|
+
walk2(path);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (!entry.isFile()) continue;
|
|
573
|
+
const name = relative(project.filesDir, path).split(sep).join("/");
|
|
574
|
+
const size = statSync2(path).size;
|
|
575
|
+
const contentType = contentTypeFor(name);
|
|
576
|
+
const reason = !isAssetName(name) ? "the name has characters a library name cannot have (letters, digits, dot, dash, underscore)" : !contentType ? "not an image or a PDF" : size === 0 ? "empty" : size > ASSET_MAX_BYTES ? `larger than ${ASSET_MAX_BYTES / 1024 / 1024} MB` : null;
|
|
577
|
+
if (reason) {
|
|
578
|
+
listing.skipped.push({ path, reason });
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const bytes = readFileSync4(path);
|
|
582
|
+
listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
walk2(project.filesDir);
|
|
586
|
+
listing.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
587
|
+
return listing;
|
|
588
|
+
}
|
|
589
|
+
function localFilePath(project, name) {
|
|
590
|
+
if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
|
|
591
|
+
const path = resolve2(project.filesDir, ...name.split("/"));
|
|
592
|
+
if (!path.startsWith(resolve2(project.filesDir) + sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
|
|
593
|
+
return path;
|
|
594
|
+
}
|
|
595
|
+
function writeLocalFile(project, name, bytes) {
|
|
596
|
+
const path = localFilePath(project, name);
|
|
597
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
598
|
+
writeFileSync4(path, bytes);
|
|
599
|
+
return path;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// packages/devkit/src/lib/redact.ts
|
|
603
|
+
function redactData(value, paths) {
|
|
604
|
+
if (!paths || paths.length === 0) return redactAll(value);
|
|
605
|
+
return walk(value, paths.map(parseRedactPath));
|
|
606
|
+
}
|
|
607
|
+
function redactString(text2) {
|
|
608
|
+
let out = "";
|
|
609
|
+
for (const ch of text2) {
|
|
610
|
+
if (/\p{Lu}/u.test(ch)) out += "A";
|
|
611
|
+
else if (/\p{L}/u.test(ch)) out += "a";
|
|
612
|
+
else if (/\p{N}/u.test(ch)) out += "0";
|
|
613
|
+
else out += ch;
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
function parseRedactPath(path) {
|
|
618
|
+
const segments = [];
|
|
619
|
+
for (const part of path.trim().split(".")) {
|
|
620
|
+
if (part === "") continue;
|
|
621
|
+
const match = /^([^[\]]*)((?:\[[^\]]*\])*)$/.exec(part);
|
|
622
|
+
if (!match) {
|
|
623
|
+
segments.push(part);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (match[1]) segments.push(match[1]);
|
|
627
|
+
for (const index of match[2].match(/\[[^\]]*\]/g) ?? []) {
|
|
628
|
+
const inner = index.slice(1, -1).trim();
|
|
629
|
+
segments.push(inner === "" || inner === "*" ? "*" : inner);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return segments;
|
|
633
|
+
}
|
|
634
|
+
function redactAll(value) {
|
|
635
|
+
if (typeof value === "string") return redactString(value);
|
|
636
|
+
if (Array.isArray(value)) return value.map(redactAll);
|
|
637
|
+
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactAll(v)]));
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
function walk(value, patterns) {
|
|
641
|
+
if (patterns.some((p) => p.length === 0)) return redactAll(value);
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
return value.map((item, index) => {
|
|
644
|
+
const next = advance(patterns, String(index));
|
|
645
|
+
return next.length ? walk(item, next) : item;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (isRecord(value)) {
|
|
649
|
+
return Object.fromEntries(
|
|
650
|
+
Object.entries(value).map(([key, item]) => {
|
|
651
|
+
const next = advance(patterns, key);
|
|
652
|
+
return [key, next.length ? walk(item, next) : item];
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
function advance(patterns, key) {
|
|
659
|
+
return patterns.filter((p) => p[0] === "*" || p[0] === key).map((p) => p.slice(1));
|
|
660
|
+
}
|
|
661
|
+
function isRecord(value) {
|
|
662
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// packages/devkit/src/lib/types/index.ts
|
|
666
|
+
import { inferSchemaFromDataSets } from "@formfeed/engine";
|
|
667
|
+
|
|
668
|
+
// packages/devkit/src/lib/types/model.ts
|
|
669
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
670
|
+
var UNSUPPORTED = [
|
|
671
|
+
"not",
|
|
672
|
+
"if",
|
|
673
|
+
"then",
|
|
674
|
+
"else",
|
|
675
|
+
"patternProperties",
|
|
676
|
+
"prefixItems",
|
|
677
|
+
"contains",
|
|
678
|
+
"dependentSchemas",
|
|
679
|
+
"dependentRequired",
|
|
680
|
+
"unevaluatedProperties",
|
|
681
|
+
"unevaluatedItems",
|
|
682
|
+
"propertyNames"
|
|
683
|
+
];
|
|
684
|
+
function pascalCase(text2) {
|
|
685
|
+
const words = text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
686
|
+
const joined = words.map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
687
|
+
if (!joined) return "Field";
|
|
688
|
+
return /^[0-9]/.test(joined) ? `T${joined}` : joined;
|
|
689
|
+
}
|
|
690
|
+
function singular(word) {
|
|
691
|
+
if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
|
|
692
|
+
if (/(ss|x|ch|sh)es$/i.test(word)) return word.slice(0, -2);
|
|
693
|
+
if (/[^su]s$/i.test(word)) return word.slice(0, -1);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
var NameRegistry = class {
|
|
697
|
+
used = /* @__PURE__ */ new Set();
|
|
698
|
+
constructor(reserved = []) {
|
|
699
|
+
for (const name of reserved) this.used.add(name);
|
|
700
|
+
}
|
|
701
|
+
claim(wanted) {
|
|
702
|
+
let name = wanted;
|
|
703
|
+
for (let i = 2; this.used.has(name); i++) name = `${wanted}${i}`;
|
|
704
|
+
this.used.add(name);
|
|
705
|
+
return name;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
function relaxRequired(schema) {
|
|
709
|
+
if (Array.isArray(schema)) return schema.map(relaxRequired);
|
|
710
|
+
if (!isObject(schema)) return schema;
|
|
711
|
+
const out = {};
|
|
712
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
713
|
+
if (key === "required") continue;
|
|
714
|
+
if (key === "properties" || key === "$defs" || key === "definitions") {
|
|
715
|
+
out[key] = isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, relaxRequired(v)])) : value;
|
|
716
|
+
} else if (key === "examples" || key === "enum" || key === "const" || key === "default") {
|
|
717
|
+
out[key] = value;
|
|
718
|
+
} else {
|
|
719
|
+
out[key] = relaxRequired(value);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
function docOf(schema) {
|
|
725
|
+
const lines = [];
|
|
726
|
+
if (typeof schema["title"] === "string" && schema["title"] !== schema["description"]) lines.push(schema["title"]);
|
|
727
|
+
if (typeof schema["description"] === "string") lines.push(...schema["description"].split("\n"));
|
|
728
|
+
if (typeof schema["format"] === "string") lines.push(`Format: ${schema["format"]}`);
|
|
729
|
+
const unsupported = UNSUPPORTED.filter((k) => k in schema);
|
|
730
|
+
if (unsupported.length) lines.push(`Not expressed in the types: ${unsupported.join(", ")}`);
|
|
731
|
+
if (Array.isArray(schema["examples"]) && schema["examples"].length)
|
|
732
|
+
lines.push(`@example ${JSON.stringify(schema["examples"][0])}`);
|
|
733
|
+
if (schema["deprecated"] === true) lines.push("@deprecated");
|
|
734
|
+
return lines;
|
|
735
|
+
}
|
|
736
|
+
var Builder = class {
|
|
737
|
+
constructor(root, base, names) {
|
|
738
|
+
this.root = root;
|
|
739
|
+
this.base = base;
|
|
740
|
+
this.names = names;
|
|
741
|
+
}
|
|
742
|
+
objects = [];
|
|
743
|
+
defs = /* @__PURE__ */ new Map();
|
|
744
|
+
rootName = "";
|
|
745
|
+
/**
|
|
746
|
+
* A plain object schema becomes the interface `rootName` itself; anything else (a nullable object,
|
|
747
|
+
* a union, an array) becomes an alias of that name, so the root never names two things.
|
|
748
|
+
*/
|
|
749
|
+
build(rootName) {
|
|
750
|
+
this.rootName = rootName;
|
|
751
|
+
const s = this.root;
|
|
752
|
+
const plainObject = (s["type"] === "object" || s["type"] === void 0 && isObject(s["properties"])) && isObject(s["properties"]) && Object.keys(s["properties"]).length > 0 && !["$ref", "const", "enum", "anyOf", "oneOf", "allOf"].some((k) => k in s);
|
|
753
|
+
return this.convert(s, null, null, plainObject ? rootName : void 0);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* `owner` + `key` name the objects found below: property `customer` of `Invoice` is
|
|
757
|
+
* `InvoiceCustomer`, an element of `items` is `InvoiceItem`. `exact` forces a name (the root).
|
|
758
|
+
*/
|
|
759
|
+
convert(raw, owner, key, exact, depth = 0) {
|
|
760
|
+
if (raw === true || raw === void 0) return { kind: "unknown" };
|
|
761
|
+
if (raw === false) return { kind: "never" };
|
|
762
|
+
if (!isObject(raw) || depth > 48) return { kind: "unknown" };
|
|
763
|
+
const schema = raw;
|
|
764
|
+
if (typeof schema["$ref"] === "string") return this.reference(schema["$ref"]);
|
|
765
|
+
if ("const" in schema) return literal(schema["const"]);
|
|
766
|
+
if (Array.isArray(schema["enum"])) {
|
|
767
|
+
const members2 = schema["enum"].map(literal);
|
|
768
|
+
return members2.length === 1 ? members2[0] : { kind: "union", members: members2 };
|
|
769
|
+
}
|
|
770
|
+
for (const combinator of ["anyOf", "oneOf"]) {
|
|
771
|
+
const list = schema[combinator];
|
|
772
|
+
if (Array.isArray(list) && list.length) {
|
|
773
|
+
const members2 = list.map(
|
|
774
|
+
(option, i) => this.convert(option, owner, key === null ? `Option${i + 1}` : `${key}Option${i + 1}`, void 0, depth + 1)
|
|
775
|
+
);
|
|
776
|
+
return union(members2);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (Array.isArray(schema["allOf"]) && schema["allOf"].length) {
|
|
780
|
+
const members2 = schema["allOf"].map(
|
|
781
|
+
(part, i) => this.convert(part, owner, key === null ? `Part${i + 1}` : `${key}Part${i + 1}`, void 0, depth + 1)
|
|
782
|
+
);
|
|
783
|
+
return members2.length === 1 ? members2[0] : { kind: "intersection", members: members2 };
|
|
784
|
+
}
|
|
785
|
+
const declared = typeof schema["type"] === "string" ? [schema["type"]] : Array.isArray(schema["type"]) ? schema["type"] : null;
|
|
786
|
+
const types = declared ?? (isObject(schema["properties"]) || isObject(schema["additionalProperties"]) ? ["object"] : "items" in schema ? ["array"] : null);
|
|
787
|
+
if (!types) {
|
|
788
|
+
const unsupported = UNSUPPORTED.filter((k) => k in schema);
|
|
789
|
+
return unsupported.length ? { kind: "unknown", note: unsupported.join(", ") } : { kind: "unknown" };
|
|
790
|
+
}
|
|
791
|
+
const members = types.map((type) => {
|
|
792
|
+
switch (type) {
|
|
793
|
+
case "object":
|
|
794
|
+
return this.object(schema, owner, key, exact, depth);
|
|
795
|
+
case "array":
|
|
796
|
+
return { kind: "array", items: this.convert(schema["items"], owner, itemKey(key), void 0, depth + 1) };
|
|
797
|
+
case "string":
|
|
798
|
+
case "number":
|
|
799
|
+
case "integer":
|
|
800
|
+
case "boolean":
|
|
801
|
+
case "null":
|
|
802
|
+
return { kind: "primitive", name: type };
|
|
803
|
+
default:
|
|
804
|
+
return { kind: "unknown" };
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
return union(members);
|
|
808
|
+
}
|
|
809
|
+
object(schema, owner, key, exact, depth) {
|
|
810
|
+
const properties = isObject(schema["properties"]) ? schema["properties"] : {};
|
|
811
|
+
const additional = schema["additionalProperties"];
|
|
812
|
+
const extra = isObject(additional) ? this.convert(additional, owner, `${key ?? ""}Value`, void 0, depth + 1) : null;
|
|
813
|
+
if (Object.keys(properties).length === 0) return { kind: "record", values: extra ?? { kind: "unknown" } };
|
|
814
|
+
const name = exact ?? this.names.claim(`${owner ?? this.base}${key === null ? "" : pascalCase(key)}`);
|
|
815
|
+
const target = { name, doc: docOf(schema), fields: [], extra };
|
|
816
|
+
this.objects.push(target);
|
|
817
|
+
const required = new Set(Array.isArray(schema["required"]) ? schema["required"] : []);
|
|
818
|
+
const nestedOwner = name === this.rootName ? this.base : name;
|
|
819
|
+
for (const [field, child] of Object.entries(properties)) {
|
|
820
|
+
target.fields.push({
|
|
821
|
+
key: field,
|
|
822
|
+
type: this.convert(child, nestedOwner, field, void 0, depth + 1),
|
|
823
|
+
required: required.has(field),
|
|
824
|
+
doc: isObject(child) ? docOf(child) : []
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return { kind: "ref", name };
|
|
828
|
+
}
|
|
829
|
+
reference(ref) {
|
|
830
|
+
const match = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
|
|
831
|
+
if (!match) return { kind: "unknown", note: `$ref ${ref}` };
|
|
832
|
+
const known = this.defs.get(ref);
|
|
833
|
+
if (known) return { kind: "ref", name: known };
|
|
834
|
+
const section = this.root[match[1]];
|
|
835
|
+
const target = isObject(section) ? section[decodePointer(match[2])] : void 0;
|
|
836
|
+
if (!isObject(target)) return { kind: "unknown", note: `$ref ${ref}` };
|
|
837
|
+
const hasProperties = isObject(target["properties"]) && Object.keys(target["properties"]).length > 0;
|
|
838
|
+
if (!hasProperties) return this.convert(target, this.base, decodePointer(match[2]));
|
|
839
|
+
const name = this.names.claim(`${this.base}${pascalCase(decodePointer(match[2]))}`);
|
|
840
|
+
this.defs.set(ref, name);
|
|
841
|
+
this.convert(target, null, null, name);
|
|
842
|
+
return { kind: "ref", name };
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
var decodePointer = (part) => part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
846
|
+
function itemKey(key) {
|
|
847
|
+
if (key === null) return "Item";
|
|
848
|
+
return singular(key) ?? `${key}Item`;
|
|
849
|
+
}
|
|
850
|
+
function literal(value) {
|
|
851
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
852
|
+
return { kind: "literal", value };
|
|
853
|
+
return { kind: "unknown" };
|
|
854
|
+
}
|
|
855
|
+
function union(members) {
|
|
856
|
+
const flat = [];
|
|
857
|
+
for (const member of members) {
|
|
858
|
+
if (member.kind === "union") flat.push(...member.members);
|
|
859
|
+
else flat.push(member);
|
|
860
|
+
}
|
|
861
|
+
if (flat.some((m) => m.kind === "unknown")) return { kind: "unknown" };
|
|
862
|
+
const unique = flat.filter((m, i) => flat.findIndex((n) => JSON.stringify(n) === JSON.stringify(m)) === i);
|
|
863
|
+
return unique.length === 1 ? unique[0] : { kind: "union", members: unique };
|
|
864
|
+
}
|
|
865
|
+
function buildTypes(sources, reserved = []) {
|
|
866
|
+
const names = new NameRegistry(reserved);
|
|
867
|
+
return [...sources].sort((a, b) => a.slug.localeCompare(b.slug)).map((source) => {
|
|
868
|
+
const schema = source.origin === "inferred" ? relaxRequired(source.schema) : source.schema;
|
|
869
|
+
const base = pascalCase(source.slug);
|
|
870
|
+
const rootName = names.claim(`${base}Data`);
|
|
871
|
+
const builder = new Builder(schema, base, names);
|
|
872
|
+
const root = builder.build(rootName);
|
|
873
|
+
return {
|
|
874
|
+
slug: source.slug,
|
|
875
|
+
origin: source.origin,
|
|
876
|
+
version: source.version ?? null,
|
|
877
|
+
channel: source.channel ?? null,
|
|
878
|
+
rootName,
|
|
879
|
+
root,
|
|
880
|
+
objects: builder.objects
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
function originNote(t) {
|
|
885
|
+
switch (t.origin) {
|
|
886
|
+
case "local":
|
|
887
|
+
return "schema.json of the project";
|
|
888
|
+
case "stored":
|
|
889
|
+
return `stored schema of version ${t.version ?? "?"}${t.channel ? ` (channel ${t.channel})` : ""}`;
|
|
890
|
+
case "inferred":
|
|
891
|
+
return "inferred from sample data, every field optional: store a schema to make this a contract";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// packages/devkit/src/lib/types/emit-python.ts
|
|
896
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
897
|
+
"False",
|
|
898
|
+
"None",
|
|
899
|
+
"True",
|
|
900
|
+
"and",
|
|
901
|
+
"as",
|
|
902
|
+
"assert",
|
|
903
|
+
"async",
|
|
904
|
+
"await",
|
|
905
|
+
"break",
|
|
906
|
+
"class",
|
|
907
|
+
"continue",
|
|
908
|
+
"def",
|
|
909
|
+
"del",
|
|
910
|
+
"elif",
|
|
911
|
+
"else",
|
|
912
|
+
"except",
|
|
913
|
+
"finally",
|
|
914
|
+
"for",
|
|
915
|
+
"from",
|
|
916
|
+
"global",
|
|
917
|
+
"if",
|
|
918
|
+
"import",
|
|
919
|
+
"in",
|
|
920
|
+
"is",
|
|
921
|
+
"lambda",
|
|
922
|
+
"nonlocal",
|
|
923
|
+
"not",
|
|
924
|
+
"or",
|
|
925
|
+
"pass",
|
|
926
|
+
"raise",
|
|
927
|
+
"return",
|
|
928
|
+
"try",
|
|
929
|
+
"while",
|
|
930
|
+
"with",
|
|
931
|
+
"yield"
|
|
932
|
+
]);
|
|
933
|
+
var isIdentifier = (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && !KEYWORDS.has(key);
|
|
934
|
+
function methodName(slug) {
|
|
935
|
+
const name = slug.replace(/[^A-Za-z0-9_]+/g, "_").toLowerCase();
|
|
936
|
+
return isIdentifier(name) ? name : `${/^[0-9]/.test(name) ? "t_" : ""}${name}${KEYWORDS.has(name) ? "_" : ""}`;
|
|
937
|
+
}
|
|
938
|
+
function expression(node, quoteRefs) {
|
|
939
|
+
switch (node.kind) {
|
|
940
|
+
case "unknown":
|
|
941
|
+
return "Any";
|
|
942
|
+
case "never":
|
|
943
|
+
return "Never";
|
|
944
|
+
case "primitive":
|
|
945
|
+
return { string: "str", number: "float", integer: "int", boolean: "bool", null: "None" }[node.name];
|
|
946
|
+
case "literal":
|
|
947
|
+
return node.value === null ? "None" : `Literal[${pythonLiteral(node.value)}]`;
|
|
948
|
+
case "array":
|
|
949
|
+
return `list[${expression(node.items, quoteRefs)}]`;
|
|
950
|
+
case "record":
|
|
951
|
+
return `dict[str, ${expression(node.values, quoteRefs)}]`;
|
|
952
|
+
case "ref":
|
|
953
|
+
return quoteRefs ? JSON.stringify(node.name) : node.name;
|
|
954
|
+
case "union": {
|
|
955
|
+
const literals = node.members.filter(
|
|
956
|
+
(m) => m.kind === "literal" && m.value !== null
|
|
957
|
+
);
|
|
958
|
+
const rest = node.members.filter((m) => !(m.kind === "literal" && m.value !== null));
|
|
959
|
+
const parts = [
|
|
960
|
+
...literals.length ? [`Literal[${literals.map((l) => pythonLiteral(l.value)).join(", ")}]`] : [],
|
|
961
|
+
...rest.map((m) => expression(m, quoteRefs))
|
|
962
|
+
];
|
|
963
|
+
if (quoteRefs && rest.some((m) => m.kind === "ref")) return `Union[${parts.join(", ")}]`;
|
|
964
|
+
return parts.join(" | ");
|
|
965
|
+
}
|
|
966
|
+
case "intersection":
|
|
967
|
+
return expression(node.members[0] ?? { kind: "unknown" }, quoteRefs);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function pythonLiteral(value) {
|
|
971
|
+
if (value === null) return "None";
|
|
972
|
+
if (typeof value === "boolean") return value ? "True" : "False";
|
|
973
|
+
return JSON.stringify(value);
|
|
974
|
+
}
|
|
975
|
+
function docstring(lines, indent) {
|
|
976
|
+
if (lines.length === 0) return [];
|
|
977
|
+
const safe = lines.map((line) => line.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"'));
|
|
978
|
+
if (safe.length === 1) return [`${indent}"""${safe[0]}"""`];
|
|
979
|
+
return [`${indent}"""${safe[0]}`, ...safe.slice(1).map((line) => `${indent}${line}`.trimEnd()), `${indent}"""`];
|
|
980
|
+
}
|
|
981
|
+
function emitObject(object) {
|
|
982
|
+
const functional = object.fields.some((f) => !isIdentifier(f.key));
|
|
983
|
+
const field = (type, required, quote) => {
|
|
984
|
+
const text2 = expression(type, quote);
|
|
985
|
+
return required ? text2 : `NotRequired[${text2}]`;
|
|
986
|
+
};
|
|
987
|
+
if (functional) {
|
|
988
|
+
const out2 = [`${object.name} = TypedDict(`, ` ${JSON.stringify(object.name)},`, " {"];
|
|
989
|
+
for (const f of object.fields) out2.push(` ${JSON.stringify(f.key)}: ${field(f.type, f.required, true)},`);
|
|
990
|
+
out2.push(" },", ")");
|
|
991
|
+
if (object.doc.length) out2.unshift(...object.doc.map((line) => `# ${line}`));
|
|
992
|
+
return out2;
|
|
993
|
+
}
|
|
994
|
+
const out = [`class ${object.name}(TypedDict):`, ...docstring(object.doc, " ")];
|
|
995
|
+
for (const f of object.fields) {
|
|
996
|
+
out.push(` ${f.key}: ${field(f.type, f.required, false)}`);
|
|
997
|
+
out.push(...docstring(f.doc, " "));
|
|
998
|
+
}
|
|
999
|
+
if (object.fields.length === 0) out.push(" pass");
|
|
1000
|
+
return out;
|
|
1001
|
+
}
|
|
1002
|
+
function emitPython(templates) {
|
|
1003
|
+
const lines = [
|
|
1004
|
+
"# Generated by `formfeed types`. Do not edit: run it again when a template schema changes.",
|
|
1005
|
+
"#",
|
|
1006
|
+
...templates.map((t) => `# ${t.slug}: ${originNote(t)}`),
|
|
1007
|
+
"",
|
|
1008
|
+
"from __future__ import annotations",
|
|
1009
|
+
"",
|
|
1010
|
+
"from typing import Any, Literal, Union, cast",
|
|
1011
|
+
"",
|
|
1012
|
+
"from typing_extensions import Never, NotRequired, TypedDict",
|
|
1013
|
+
"",
|
|
1014
|
+
"from formfeed import AsyncFormfeed, Formfeed, Render",
|
|
1015
|
+
"",
|
|
1016
|
+
"__all__ = [",
|
|
1017
|
+
...templates.map((t) => ` ${JSON.stringify(t.rootName)},`),
|
|
1018
|
+
' "TypedRenders",',
|
|
1019
|
+
' "AsyncTypedRenders",',
|
|
1020
|
+
' "typed",',
|
|
1021
|
+
' "typed_async",',
|
|
1022
|
+
"]"
|
|
1023
|
+
];
|
|
1024
|
+
for (const template of templates) {
|
|
1025
|
+
lines.push("", "", `# --- ${template.slug}`);
|
|
1026
|
+
for (const object of [...template.objects].reverse()) lines.push("", ...emitObject(object));
|
|
1027
|
+
const isClass = template.root.kind === "ref" && template.root.name === template.rootName;
|
|
1028
|
+
if (!isClass) lines.push("", `${template.rootName} = ${expression(template.root, true)}`);
|
|
1029
|
+
}
|
|
1030
|
+
const wrapper = (name, isAsync) => {
|
|
1031
|
+
const example = templates[0] ? methodName(templates[0].slug) : "invoice";
|
|
1032
|
+
const out = [
|
|
1033
|
+
"",
|
|
1034
|
+
"",
|
|
1035
|
+
`class ${name}:`,
|
|
1036
|
+
` """Renders with \`data\` checked per template: \`${isAsync ? "await typed_async(client)" : "typed(client)"}.${example}(data, **options)\`."""`,
|
|
1037
|
+
"",
|
|
1038
|
+
` def __init__(self, client: ${isAsync ? "AsyncFormfeed" : "Formfeed"}) -> None:`,
|
|
1039
|
+
" self._renders: Any = client.renders"
|
|
1040
|
+
];
|
|
1041
|
+
for (const t of templates) {
|
|
1042
|
+
out.push(
|
|
1043
|
+
"",
|
|
1044
|
+
` ${isAsync ? "async " : ""}def ${methodName(t.slug)}(self, data: ${t.rootName}, **options: Any) -> Render:`,
|
|
1045
|
+
` return cast(Render, ${isAsync ? "await " : ""}self._renders.create(template=${JSON.stringify(t.slug)}, data=data, **options))`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
return out;
|
|
1049
|
+
};
|
|
1050
|
+
lines.push(...wrapper("TypedRenders", false), ...wrapper("AsyncTypedRenders", true));
|
|
1051
|
+
lines.push(
|
|
1052
|
+
"",
|
|
1053
|
+
"",
|
|
1054
|
+
"def typed(client: Formfeed) -> TypedRenders:",
|
|
1055
|
+
" return TypedRenders(client)",
|
|
1056
|
+
"",
|
|
1057
|
+
"",
|
|
1058
|
+
"def typed_async(client: AsyncFormfeed) -> AsyncTypedRenders:",
|
|
1059
|
+
" return AsyncTypedRenders(client)"
|
|
1060
|
+
);
|
|
1061
|
+
return lines.join("\n") + "\n";
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// packages/devkit/src/lib/types/emit-ts.ts
|
|
1065
|
+
var identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1066
|
+
var quoteKey = (key) => identifier.test(key) ? key : JSON.stringify(key);
|
|
1067
|
+
function expression2(node, nested = false) {
|
|
1068
|
+
switch (node.kind) {
|
|
1069
|
+
case "unknown":
|
|
1070
|
+
return "unknown";
|
|
1071
|
+
case "never":
|
|
1072
|
+
return "never";
|
|
1073
|
+
case "primitive":
|
|
1074
|
+
return node.name === "integer" ? "number" : node.name;
|
|
1075
|
+
case "literal":
|
|
1076
|
+
return JSON.stringify(node.value);
|
|
1077
|
+
case "array": {
|
|
1078
|
+
const inner = expression2(node.items, true);
|
|
1079
|
+
return /[|&]/.test(inner) ? `Array<${expression2(node.items)}>` : `${inner}[]`;
|
|
1080
|
+
}
|
|
1081
|
+
case "record":
|
|
1082
|
+
return `Record<string, ${expression2(node.values)}>`;
|
|
1083
|
+
case "ref":
|
|
1084
|
+
return node.name;
|
|
1085
|
+
case "union": {
|
|
1086
|
+
const text2 = node.members.map((m) => expression2(m, true)).join(" | ");
|
|
1087
|
+
return nested ? `(${text2})` : text2;
|
|
1088
|
+
}
|
|
1089
|
+
case "intersection": {
|
|
1090
|
+
const text2 = node.members.map((m) => expression2(m, true)).join(" & ");
|
|
1091
|
+
return nested ? `(${text2})` : text2;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function docBlock(lines, indent) {
|
|
1096
|
+
if (lines.length === 0) return [];
|
|
1097
|
+
const safe = lines.map((line) => line.replace(/\*\//g, "*\\/"));
|
|
1098
|
+
if (safe.length === 1) return [`${indent}/** ${safe[0]} */`];
|
|
1099
|
+
return [`${indent}/**`, ...safe.map((line) => `${indent} * ${line}`.trimEnd()), `${indent} */`];
|
|
1100
|
+
}
|
|
1101
|
+
function emitObject2(object) {
|
|
1102
|
+
const out = [...docBlock(object.doc, ""), `export interface ${object.name} {`];
|
|
1103
|
+
for (const field of object.fields) {
|
|
1104
|
+
out.push(...docBlock(field.doc, " "));
|
|
1105
|
+
out.push(` ${quoteKey(field.key)}${field.required ? "" : "?"}: ${expression2(field.type)};`);
|
|
1106
|
+
}
|
|
1107
|
+
if (object.extra) {
|
|
1108
|
+
out.push(` /** Further fields: ${expression2(object.extra).replace(/\*\//g, "*\\/")} */`);
|
|
1109
|
+
out.push(" [key: string]: unknown;");
|
|
1110
|
+
}
|
|
1111
|
+
out.push("}");
|
|
1112
|
+
return out;
|
|
1113
|
+
}
|
|
1114
|
+
function emitTypeScript(templates, options = {}) {
|
|
1115
|
+
const sdk = options.sdkModule ?? "@formfeed/sdk";
|
|
1116
|
+
const lines = [
|
|
1117
|
+
"// Generated by `formfeed types`. Do not edit: run it again when a template schema changes.",
|
|
1118
|
+
"//",
|
|
1119
|
+
...templates.map((t) => `// ${t.slug}: ${originNote(t)}`),
|
|
1120
|
+
"",
|
|
1121
|
+
`declare module ${JSON.stringify(sdk)} {`,
|
|
1122
|
+
" interface FormfeedTemplates {",
|
|
1123
|
+
...templates.map((t) => ` ${JSON.stringify(t.slug)}: ${t.rootName};`),
|
|
1124
|
+
" }",
|
|
1125
|
+
"}"
|
|
1126
|
+
];
|
|
1127
|
+
for (const template of templates) {
|
|
1128
|
+
lines.push("", `// --- ${template.slug}`);
|
|
1129
|
+
const isInterface = template.root.kind === "ref" && template.root.name === template.rootName;
|
|
1130
|
+
if (!isInterface) lines.push("", `export type ${template.rootName} = ${expression2(template.root)};`);
|
|
1131
|
+
for (const object of template.objects) lines.push("", ...emitObject2(object));
|
|
1132
|
+
}
|
|
1133
|
+
if (templates.length === 0) lines.push("", "export {};");
|
|
1134
|
+
return lines.join("\n") + "\n";
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// packages/devkit/src/lib/types/index.ts
|
|
1138
|
+
var defaultTypesFile = {
|
|
1139
|
+
ts: "formfeed.d.ts",
|
|
1140
|
+
python: "formfeed_templates.py"
|
|
1141
|
+
};
|
|
1142
|
+
function localTypeSource(tpl) {
|
|
1143
|
+
if (tpl.dataSchema) return { slug: tpl.slug, schema: tpl.dataSchema, origin: "local" };
|
|
1144
|
+
const sets = Object.values(tpl.dataSets);
|
|
1145
|
+
return {
|
|
1146
|
+
slug: tpl.slug,
|
|
1147
|
+
schema: inferSchemaFromDataSets(sets.length ? sets : [{}]),
|
|
1148
|
+
origin: "inferred"
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function generateTypes(sources, language, options = {}) {
|
|
1152
|
+
const reserved = language === "python" ? ["TypedRenders", "AsyncTypedRenders", "Formfeed", "AsyncFormfeed", "Render", "Any", "Literal", "Union", "Never", "NotRequired", "TypedDict"] : ["FormfeedTemplates"];
|
|
1153
|
+
const model = buildTypes(sources, reserved);
|
|
1154
|
+
return language === "python" ? emitPython(model) : emitTypeScript(model, options);
|
|
278
1155
|
}
|
|
279
1156
|
export {
|
|
280
1157
|
DevkitError,
|
|
1158
|
+
brandFromJson,
|
|
1159
|
+
brandPath,
|
|
1160
|
+
collectPartials,
|
|
281
1161
|
contentHash,
|
|
1162
|
+
contentTypeFor,
|
|
282
1163
|
defaultData,
|
|
283
1164
|
defaultProjectConfig,
|
|
1165
|
+
defaultTypesFile,
|
|
284
1166
|
diagnose,
|
|
285
1167
|
findProject,
|
|
1168
|
+
generateTypes,
|
|
1169
|
+
listLocalFiles,
|
|
286
1170
|
listTemplateSlugs,
|
|
287
1171
|
loadProject,
|
|
1172
|
+
localFilePath,
|
|
1173
|
+
localTypeSource,
|
|
1174
|
+
officeSnapshot,
|
|
1175
|
+
officeVersionPayload,
|
|
1176
|
+
parseRedactPath,
|
|
1177
|
+
partialContentHash,
|
|
288
1178
|
partialResolver,
|
|
1179
|
+
prettyXml,
|
|
289
1180
|
previewDocument,
|
|
290
1181
|
projectAround,
|
|
1182
|
+
readBrand,
|
|
291
1183
|
readState,
|
|
292
1184
|
readTemplate,
|
|
1185
|
+
recordSharedPartial,
|
|
293
1186
|
recordSync,
|
|
1187
|
+
redactData,
|
|
1188
|
+
redactString,
|
|
294
1189
|
renderContext,
|
|
295
1190
|
renderLocal,
|
|
1191
|
+
renderOfficeLocal,
|
|
1192
|
+
sha256File,
|
|
1193
|
+
sharedPartialNames,
|
|
1194
|
+
sharedPartialPath,
|
|
296
1195
|
templateDir,
|
|
1196
|
+
templateFileName,
|
|
297
1197
|
titleFromSlug,
|
|
298
1198
|
versionPayload,
|
|
1199
|
+
writeBrand,
|
|
1200
|
+
writeLocalFile,
|
|
1201
|
+
writePartials,
|
|
299
1202
|
writeProjectConfig,
|
|
300
1203
|
writeState,
|
|
301
1204
|
writeTemplate
|