@formfeed/devkit 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install @formfeed/devkit
13
13
  ```ts
14
14
  import { defaultData, diagnose, projectAround, readTemplate, renderLocal } from '@formfeed/devkit';
15
15
 
16
- // a folder with template.html, template.json, styles.css, data/*.json (see the project layout docs)
16
+ // a folder with template.html, template.json, style.css, data/*.json (see the project layout docs)
17
17
  const project = projectAround('templates/invoice-de');
18
18
  const tpl = readTemplate(project, 'invoice-de');
19
19
  const { data } = defaultData(tpl); // data/default.json, or the first data set
@@ -22,7 +22,22 @@ console.log(diagnose(tpl, data)); // the editor's diagnostics: syntax errors, un
22
22
  const { document } = await renderLocal(project, tpl, data, { mode: 'print' }); // complete HTML, no browser
23
23
  ```
24
24
 
25
- Errors are `DevkitError` instances with a `code`, which the CLI maps to its exit codes.
25
+ A folder can hold a Word or PowerPoint document (`template.docx` or `template.pptx`) instead of the
26
+ HTML files; `readTemplate` returns it as `tpl.file` and `templateFileName(kind)` names the file for
27
+ a kind. `diagnose` lists such a template's findings by part and paragraph, `renderOfficeLocal` fills
28
+ it (`bytes`, `warnings`) and `officeSnapshot(bytes)` turns the filled file into readable XML for
29
+ snapshot tests:
30
+
31
+ ```ts
32
+ import { officeSnapshot, readTemplate, renderOfficeLocal } from '@formfeed/devkit';
33
+
34
+ const offer = readTemplate(project, 'offer');
35
+ const filled = await renderOfficeLocal(project, offer, { customer: { name: 'Olvarest GmbH' } });
36
+ console.log(filled.warnings, officeSnapshot(filled.bytes));
37
+ ```
38
+
39
+ `renderLocal` works on HTML templates only. Errors are `DevkitError` instances with a `code`, which
40
+ the CLI maps to its exit codes.
26
41
 
27
42
  ## Documentation
28
43
 
package/index.cjs CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  diagnose: () => diagnose,
33
33
  findProject: () => findProject,
34
34
  generateTypes: () => generateTypes,
35
+ isIgnored: () => isIgnored,
35
36
  listLocalFiles: () => listLocalFiles,
36
37
  listTemplateSlugs: () => listTemplateSlugs,
37
38
  loadProject: () => loadProject,
@@ -55,6 +56,7 @@ __export(index_exports, {
55
56
  renderContext: () => renderContext,
56
57
  renderLocal: () => renderLocal,
57
58
  renderOfficeLocal: () => renderOfficeLocal,
59
+ renderedSettings: () => renderedSettings,
58
60
  sha256File: () => sha256File,
59
61
  sharedPartialNames: () => sharedPartialNames,
60
62
  sharedPartialPath: () => sharedPartialPath,
@@ -90,6 +92,32 @@ var defaultProjectConfig = {
90
92
  engine: "jinja2",
91
93
  ignore: ["**/drafts/**"]
92
94
  };
95
+ function globRegExp(pattern) {
96
+ let glob = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
97
+ if (!glob.includes("/")) glob = `**/${glob}`;
98
+ let source = "";
99
+ for (let i = 0; i < glob.length; i++) {
100
+ const ch = glob[i];
101
+ if (ch === "*" && glob[i + 1] === "*") {
102
+ const slash = glob[i + 2] === "/";
103
+ source += slash ? "(?:.*/)?" : ".*";
104
+ i += slash ? 2 : 1;
105
+ } else if (ch === "*") source += "[^/]*";
106
+ else if (ch === "?") source += "[^/]";
107
+ else source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
108
+ }
109
+ return new RegExp(`^${source.replace(/^\//, "")}$`);
110
+ }
111
+ function isIgnored(project, path, folder = false) {
112
+ const patterns = project.config.ignore ?? [];
113
+ if (!patterns.length) return false;
114
+ const rel = (0, import_node_path.relative)(project.root, (0, import_node_path.resolve)(project.root, path)).split(import_node_path.sep).join("/");
115
+ const candidates = folder ? [rel, `${rel}/`, `${rel}/x`] : [rel];
116
+ return patterns.some((p) => {
117
+ const re = globRegExp(p);
118
+ return candidates.some((c) => re.test(c));
119
+ });
120
+ }
93
121
  function findProject(cwd = process.cwd()) {
94
122
  let dir = (0, import_node_path.resolve)(cwd);
95
123
  for (; ; ) {
@@ -565,6 +593,17 @@ async function renderLocal(project, tpl, data, options) {
565
593
  options.vendor ? { vendor: options.vendor } : {}
566
594
  );
567
595
  }
596
+ function renderedSettings(rendered) {
597
+ const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
598
+ const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
599
+ const { settings } = rendered;
600
+ return {
601
+ ...settings,
602
+ ...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
603
+ ...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
604
+ ...settings.pdf?.metadata?.title !== void 0 ? { pdf: { ...settings.pdf, metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" } } } : {}
605
+ };
606
+ }
568
607
  function htmlKind(tpl) {
569
608
  if ((0, import_engine4.isOfficeKind)(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
570
609
  return tpl.meta.kind;
@@ -1223,6 +1262,7 @@ function generateTypes(sources, language, options = {}) {
1223
1262
  diagnose,
1224
1263
  findProject,
1225
1264
  generateTypes,
1265
+ isIgnored,
1226
1266
  listLocalFiles,
1227
1267
  listTemplateSlugs,
1228
1268
  loadProject,
@@ -1246,6 +1286,7 @@ function generateTypes(sources, language, options = {}) {
1246
1286
  renderContext,
1247
1287
  renderLocal,
1248
1288
  renderOfficeLocal,
1289
+ renderedSettings,
1249
1290
  sha256File,
1250
1291
  sharedPartialNames,
1251
1292
  sharedPartialPath,
package/index.js CHANGED
@@ -9,7 +9,7 @@ var DevkitError = class extends Error {
9
9
 
10
10
  // packages/devkit/src/lib/project-config.ts
11
11
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
12
+ import { dirname, join, relative, resolve, sep } from "node:path";
13
13
  var defaultProjectConfig = {
14
14
  templatesDir: "templates",
15
15
  partialsDir: "partials",
@@ -17,6 +17,32 @@ var defaultProjectConfig = {
17
17
  engine: "jinja2",
18
18
  ignore: ["**/drafts/**"]
19
19
  };
20
+ function globRegExp(pattern) {
21
+ let glob = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
22
+ if (!glob.includes("/")) glob = `**/${glob}`;
23
+ let source = "";
24
+ for (let i = 0; i < glob.length; i++) {
25
+ const ch = glob[i];
26
+ if (ch === "*" && glob[i + 1] === "*") {
27
+ const slash = glob[i + 2] === "/";
28
+ source += slash ? "(?:.*/)?" : ".*";
29
+ i += slash ? 2 : 1;
30
+ } else if (ch === "*") source += "[^/]*";
31
+ else if (ch === "?") source += "[^/]";
32
+ else source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ return new RegExp(`^${source.replace(/^\//, "")}$`);
35
+ }
36
+ function isIgnored(project, path, folder = false) {
37
+ const patterns = project.config.ignore ?? [];
38
+ if (!patterns.length) return false;
39
+ const rel = relative(project.root, resolve(project.root, path)).split(sep).join("/");
40
+ const candidates = folder ? [rel, `${rel}/`, `${rel}/x`] : [rel];
41
+ return patterns.some((p) => {
42
+ const re = globRegExp(p);
43
+ return candidates.some((c) => re.test(c));
44
+ });
45
+ }
20
46
  function findProject(cwd = process.cwd()) {
21
47
  let dir = resolve(cwd);
22
48
  for (; ; ) {
@@ -509,6 +535,17 @@ async function renderLocal(project, tpl, data, options) {
509
535
  options.vendor ? { vendor: options.vendor } : {}
510
536
  );
511
537
  }
538
+ function renderedSettings(rendered) {
539
+ const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
540
+ const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
541
+ const { settings } = rendered;
542
+ return {
543
+ ...settings,
544
+ ...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
545
+ ...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
546
+ ...settings.pdf?.metadata?.title !== void 0 ? { pdf: { ...settings.pdf, metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" } } } : {}
547
+ };
548
+ }
512
549
  function htmlKind(tpl) {
513
550
  if (isOfficeKind2(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
514
551
  return tpl.meta.kind;
@@ -528,7 +565,7 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
528
565
  // packages/devkit/src/lib/files.ts
529
566
  import { createHash as createHash2 } from "node:crypto";
530
567
  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";
568
+ import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
532
569
 
533
570
  // packages/api-types/src/lib/assets.ts
534
571
  var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
@@ -570,7 +607,7 @@ function listLocalFiles(project) {
570
607
  continue;
571
608
  }
572
609
  if (!entry.isFile()) continue;
573
- const name = relative(project.filesDir, path).split(sep).join("/");
610
+ const name = relative2(project.filesDir, path).split(sep2).join("/");
574
611
  const size = statSync2(path).size;
575
612
  const contentType = contentTypeFor(name);
576
613
  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;
@@ -589,7 +626,7 @@ function listLocalFiles(project) {
589
626
  function localFilePath(project, name) {
590
627
  if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
591
628
  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`);
629
+ if (!path.startsWith(resolve2(project.filesDir) + sep2)) throw new DevkitError(`"${name}" is not inside the files folder`);
593
630
  return path;
594
631
  }
595
632
  function writeLocalFile(project, name, bytes) {
@@ -1166,6 +1203,7 @@ export {
1166
1203
  diagnose,
1167
1204
  findProject,
1168
1205
  generateTypes,
1206
+ isIgnored,
1169
1207
  listLocalFiles,
1170
1208
  listTemplateSlugs,
1171
1209
  loadProject,
@@ -1189,6 +1227,7 @@ export {
1189
1227
  renderContext,
1190
1228
  renderLocal,
1191
1229
  renderOfficeLocal,
1230
+ renderedSettings,
1192
1231
  sha256File,
1193
1232
  sharedPartialNames,
1194
1233
  sharedPartialPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/devkit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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.0",
34
- "@formfeed/sdk": "0.3.0"
33
+ "@formfeed/engine": "0.3.1",
34
+ "@formfeed/sdk": "0.3.1"
35
35
  },
36
36
  "keywords": [
37
37
  "formfeed",
package/types/index.d.cts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { DevkitError } from './lib/errors.cjs';
2
- export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.cjs';
2
+ export { defaultProjectConfig, findProject, isIgnored, loadProject, projectAround, writeProjectConfig } from './lib/project-config.cjs';
3
3
  export type { Project, ProjectConfig } from './lib/project-config.cjs';
4
4
  export { contentHash, defaultData, listTemplateSlugs, officeVersionPayload, partialContentHash, partialResolver, readState, readTemplate, recordSharedPartial, recordSync, sharedPartialNames, sharedPartialPath, templateDir, templateFileName, titleFromSlug, versionPayload, writePartials, writeState, writeTemplate, } from './lib/project.cjs';
5
5
  export type { LocalOfficeFile, LocalTemplate, ProjectState, SharedPartialState, TemplateMeta, TemplateState } from './lib/project.cjs';
6
6
  export { brandFromJson, brandPath, readBrand, writeBrand } from './lib/brand.cjs';
7
7
  export { collectPartials } from './lib/partials.cjs';
8
8
  export type { CollectedPartials } from './lib/partials.cjs';
9
- export { diagnose, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal } from './lib/local-render.cjs';
9
+ export { diagnose, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal, renderedSettings } from './lib/local-render.cjs';
10
10
  export type { LocalDiagnostic, OfficeLocalOptions } from './lib/local-render.cjs';
11
11
  export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile } from './lib/files.cjs';
12
12
  export type { LocalFile, LocalFileListing } from './lib/files.cjs';
package/types/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { DevkitError } from './lib/errors.js';
2
- export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.js';
2
+ export { defaultProjectConfig, findProject, isIgnored, loadProject, projectAround, writeProjectConfig } from './lib/project-config.js';
3
3
  export type { Project, ProjectConfig } from './lib/project-config.js';
4
4
  export { contentHash, defaultData, listTemplateSlugs, officeVersionPayload, partialContentHash, partialResolver, readState, readTemplate, recordSharedPartial, recordSync, sharedPartialNames, sharedPartialPath, templateDir, templateFileName, titleFromSlug, versionPayload, writePartials, writeState, writeTemplate, } from './lib/project.js';
5
5
  export type { LocalOfficeFile, LocalTemplate, ProjectState, SharedPartialState, TemplateMeta, TemplateState } from './lib/project.js';
6
6
  export { brandFromJson, brandPath, readBrand, writeBrand } from './lib/brand.js';
7
7
  export { collectPartials } from './lib/partials.js';
8
8
  export type { CollectedPartials } from './lib/partials.js';
9
- export { diagnose, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal } from './lib/local-render.js';
9
+ export { diagnose, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal, renderedSettings } from './lib/local-render.js';
10
10
  export type { LocalDiagnostic, OfficeLocalOptions } from './lib/local-render.js';
11
11
  export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile } from './lib/files.js';
12
12
  export type { LocalFile, LocalFileListing } from './lib/files.js';
@@ -47,5 +47,12 @@ export interface LocalRenderOptions {
47
47
  }
48
48
  /** Assembles the complete document with the shared engine; no browser involved. */
49
49
  export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
50
+ /**
51
+ * The settings to send with a locally rendered document as a raw-HTML render. The API uses
52
+ * `header.html`, `footer.html` and the PDF title of such a render verbatim, so they must be the
53
+ * filled parts, not the template's source; the inline CSS travels with them, because Chromium loads
54
+ * no stylesheet in its header and footer templates.
55
+ */
56
+ export declare function renderedSettings(rendered: RenderedDocument): Record<string, unknown>;
50
57
  /** Wraps a preview render for the screen the way the editor does. */
51
58
  export declare function previewDocument(tpl: LocalTemplate, rendered: RenderedDocument, mode: 'flow' | 'paged', pagedScriptUrl: string): string;
@@ -47,5 +47,12 @@ export interface LocalRenderOptions {
47
47
  }
48
48
  /** Assembles the complete document with the shared engine; no browser involved. */
49
49
  export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
50
+ /**
51
+ * The settings to send with a locally rendered document as a raw-HTML render. The API uses
52
+ * `header.html`, `footer.html` and the PDF title of such a render verbatim, so they must be the
53
+ * filled parts, not the template's source; the inline CSS travels with them, because Chromium loads
54
+ * no stylesheet in its header and footer templates.
55
+ */
56
+ export declare function renderedSettings(rendered: RenderedDocument): Record<string, unknown>;
50
57
  /** Wraps a preview render for the screen the way the editor does. */
51
58
  export declare function previewDocument(tpl: LocalTemplate, rendered: RenderedDocument, mode: 'flow' | 'paged', pagedScriptUrl: string): string;
@@ -33,6 +33,12 @@ export interface Project {
33
33
  filesDir: string;
34
34
  }
35
35
  export declare const defaultProjectConfig: ProjectConfig;
36
+ /**
37
+ * Whether a path of the project matches one of `ignore`. The path is taken relative to the project
38
+ * root; a folder also matches as `<folder>/` and through the files in it, so `**\/drafts/**` and
39
+ * `templates/wip-*` both catch `templates/wip-offer`.
40
+ */
41
+ export declare function isIgnored(project: Project, path: string, folder?: boolean): boolean;
36
42
  /** Walks up from `cwd` to the nearest `formfeed.json`. */
37
43
  export declare function findProject(cwd?: string): Project | null;
38
44
  export declare function loadProject(configPath: string): Project;
@@ -33,6 +33,12 @@ export interface Project {
33
33
  filesDir: string;
34
34
  }
35
35
  export declare const defaultProjectConfig: ProjectConfig;
36
+ /**
37
+ * Whether a path of the project matches one of `ignore`. The path is taken relative to the project
38
+ * root; a folder also matches as `<folder>/` and through the files in it, so `**\/drafts/**` and
39
+ * `templates/wip-*` both catch `templates/wip-offer`.
40
+ */
41
+ export declare function isIgnored(project: Project, path: string, folder?: boolean): boolean;
36
42
  /** Walks up from `cwd` to the nearest `formfeed.json`. */
37
43
  export declare function findProject(cwd?: string): Project | null;
38
44
  export declare function loadProject(configPath: string): Project;