@formfeed/engine 0.2.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.
Files changed (67) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_LICENSES.md +31 -0
  3. package/index.cjs +4286 -172
  4. package/index.js +4286 -172
  5. package/package.json +1 -1
  6. package/types/index.d.cts +19 -9
  7. package/types/index.d.ts +19 -9
  8. package/types/lib/assemble.d.cts +24 -9
  9. package/types/lib/assemble.d.ts +24 -9
  10. package/types/lib/brand.d.cts +46 -0
  11. package/types/lib/brand.d.ts +46 -0
  12. package/types/lib/helpers/document.d.cts +24 -0
  13. package/types/lib/helpers/document.d.ts +24 -0
  14. package/types/lib/helpers/index.d.cts +1 -0
  15. package/types/lib/helpers/index.d.ts +1 -0
  16. package/types/lib/helpers/office.d.cts +4 -0
  17. package/types/lib/helpers/office.d.ts +4 -0
  18. package/types/lib/import/apitemplate.d.cts +63 -0
  19. package/types/lib/import/apitemplate.d.ts +63 -0
  20. package/types/lib/office/detect.d.cts +36 -0
  21. package/types/lib/office/detect.d.ts +36 -0
  22. package/types/lib/office/drawings.d.cts +120 -0
  23. package/types/lib/office/drawings.d.ts +120 -0
  24. package/types/lib/office/errors.d.cts +21 -0
  25. package/types/lib/office/errors.d.ts +21 -0
  26. package/types/lib/office/fonts.d.cts +21 -0
  27. package/types/lib/office/fonts.d.ts +21 -0
  28. package/types/lib/office/ids.d.cts +1 -0
  29. package/types/lib/office/ids.d.ts +1 -0
  30. package/types/lib/office/index.d.cts +18 -0
  31. package/types/lib/office/index.d.ts +18 -0
  32. package/types/lib/office/render.d.cts +79 -0
  33. package/types/lib/office/render.d.ts +79 -0
  34. package/types/lib/office/sanitise.d.cts +35 -0
  35. package/types/lib/office/sanitise.d.ts +35 -0
  36. package/types/lib/office/slides.d.cts +47 -0
  37. package/types/lib/office/slides.d.ts +47 -0
  38. package/types/lib/office/starter-pptx.d.cts +6 -0
  39. package/types/lib/office/starter-pptx.d.ts +6 -0
  40. package/types/lib/office/starter.d.cts +24 -0
  41. package/types/lib/office/starter.d.ts +24 -0
  42. package/types/lib/office/structure.d.cts +51 -0
  43. package/types/lib/office/structure.d.ts +51 -0
  44. package/types/lib/office/tags.d.cts +51 -0
  45. package/types/lib/office/tags.d.ts +51 -0
  46. package/types/lib/office/template-text.d.cts +59 -0
  47. package/types/lib/office/template-text.d.ts +59 -0
  48. package/types/lib/office/xml.d.cts +44 -0
  49. package/types/lib/office/xml.d.ts +44 -0
  50. package/types/lib/office/zip.d.cts +49 -0
  51. package/types/lib/office/zip.d.ts +49 -0
  52. package/types/lib/output.d.cts +29 -0
  53. package/types/lib/output.d.ts +29 -0
  54. package/types/lib/preview.d.cts +14 -2
  55. package/types/lib/preview.d.ts +14 -2
  56. package/types/lib/schema-diff.d.cts +27 -0
  57. package/types/lib/schema-diff.d.ts +27 -0
  58. package/types/lib/schema.d.cts +8 -0
  59. package/types/lib/schema.d.ts +8 -0
  60. package/types/lib/snippets.d.cts +38 -1
  61. package/types/lib/snippets.d.ts +38 -1
  62. package/types/lib/source-positions.d.cts +27 -0
  63. package/types/lib/source-positions.d.ts +27 -0
  64. package/types/lib/types.d.cts +37 -1
  65. package/types/lib/types.d.ts +37 -1
  66. package/types/types/modules.d.cts +1 -0
  67. package/types/types/modules.d.ts +1 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Output formats of a render and the one rule for the format a request gets when it names none.
3
+ * Imported without the renderers as `@formfeed/engine/output` (the gateway), so it depends on
4
+ * nothing.
5
+ */
6
+ /** The API's `output` values. JPEG is `jpg` here; settings spell it `jpeg`. */
7
+ export declare const outputFormats: readonly ["pdf", "png", "jpg", "webp", "docx", "pptx"];
8
+ export type OutputFormat = (typeof outputFormats)[number];
9
+ export type ImageOutput = 'png' | 'jpg' | 'webp';
10
+ /** Template kinds whose template is a Word or PowerPoint file (spec 22 §4.1). */
11
+ export declare const officeKinds: readonly ["docx", "pptx"];
12
+ export type OfficeKind = (typeof officeKinds)[number];
13
+ export declare function isOutputFormat(value: unknown): value is OutputFormat;
14
+ export declare function isOfficeKind(value: unknown): value is OfficeKind;
15
+ /**
16
+ * The outputs a source may request (spec 22 §4.1): an office template its own format or a PDF;
17
+ * everything else (PDF and image templates, ad-hoc HTML, URLs) PDF or an image.
18
+ */
19
+ export declare function outputsForKind(kind: string | null | undefined): readonly OutputFormat[];
20
+ /** `settings.image.format` as an output; `jpg` is accepted too, anything else is PNG. */
21
+ export declare function imageOutput(settings: unknown): ImageOutput;
22
+ /**
23
+ * The output of a render that names none: a PDF template renders a PDF, an image template the format
24
+ * of its `settings.image.format` (PNG without one), an office template `settings.office.output` (its
25
+ * own format without one). Settings are the template's with the request's merged over them, so a
26
+ * request can pick the format without naming `output`. Sources without a kind (ad-hoc HTML, URLs)
27
+ * render PDFs.
28
+ */
29
+ export declare function defaultOutput(kind: string | null | undefined, ...settings: unknown[]): OutputFormat;
@@ -1,4 +1,4 @@
1
- import type { TemplateKind, TemplateSettings } from './assemble.cjs';
1
+ import { type TemplateKind, type TemplateSettings } from './assemble.cjs';
2
2
  /**
3
3
  * Preview documents shared by the web editor and `formfeed dev` (spec 06 §3, spec 15 §4): the
4
4
  * assembled document of `renderVersion` wrapped for the screen. Flow mode shows one page-wide
@@ -16,8 +16,20 @@ export declare function paperOf(settings: TemplateSettings): {
16
16
  width: string;
17
17
  height: string;
18
18
  };
19
- /** Chromium's header/footer placeholders replaced for the screen. */
19
+ /**
20
+ * Chromium's header/footer placeholders filled for the screen. Like Chromium, any element whose class
21
+ * list names the placeholder counts, whatever else it carries (the editor adds `data-ff-src`), and
22
+ * the element stays so its styling applies.
23
+ */
20
24
  export declare const pageNumberSpans: (html: string, page: string, total: string) => string;
25
+ /**
26
+ * Zoom gestures inside a preview frame: Ctrl+wheel (a touchpad pinch arrives as one too) and
27
+ * Ctrl+plus/minus/0 would zoom the whole app, and the sandboxed frame's events never reach the app,
28
+ * so the frame cancels them and asks the parent by message:
29
+ * `{ type: 'formfeed:zoom', deltaY, deltaMode }` or `{ type: 'formfeed:zoom', step: 'in' | 'out' | 'reset' }`.
30
+ * The office quick preview's frame uses the same script.
31
+ */
32
+ export declare const zoomGestureScript = "<script>\n(function () {\n var say = function (message) { try { parent.postMessage(message, '*'); } catch (e) {} };\n window.addEventListener('wheel', function (event) {\n if (!event.ctrlKey && !event.metaKey) return;\n event.preventDefault();\n say({ type: 'formfeed:zoom', deltaY: event.deltaY, deltaMode: event.deltaMode });\n }, { passive: false });\n window.addEventListener('keydown', function (event) {\n if (!(event.ctrlKey || event.metaKey) || event.altKey) return;\n var step = event.key === '+' || event.key === '=' ? 'in' : event.key === '-' ? 'out' : event.key === '0' ? 'reset' : null;\n if (!step) return;\n event.preventDefault();\n say({ type: 'formfeed:zoom', step: step });\n });\n})();\n</script>";
21
33
  export declare function flowDocument(draft: RenderedDraft): string;
22
34
  export interface PagedOptions {
23
35
  /** URL of the Paged.js polyfill the frame can load. */
@@ -1,4 +1,4 @@
1
- import type { TemplateKind, TemplateSettings } from './assemble.js';
1
+ import { type TemplateKind, type TemplateSettings } from './assemble.js';
2
2
  /**
3
3
  * Preview documents shared by the web editor and `formfeed dev` (spec 06 §3, spec 15 §4): the
4
4
  * assembled document of `renderVersion` wrapped for the screen. Flow mode shows one page-wide
@@ -16,8 +16,20 @@ export declare function paperOf(settings: TemplateSettings): {
16
16
  width: string;
17
17
  height: string;
18
18
  };
19
- /** Chromium's header/footer placeholders replaced for the screen. */
19
+ /**
20
+ * Chromium's header/footer placeholders filled for the screen. Like Chromium, any element whose class
21
+ * list names the placeholder counts, whatever else it carries (the editor adds `data-ff-src`), and
22
+ * the element stays so its styling applies.
23
+ */
20
24
  export declare const pageNumberSpans: (html: string, page: string, total: string) => string;
25
+ /**
26
+ * Zoom gestures inside a preview frame: Ctrl+wheel (a touchpad pinch arrives as one too) and
27
+ * Ctrl+plus/minus/0 would zoom the whole app, and the sandboxed frame's events never reach the app,
28
+ * so the frame cancels them and asks the parent by message:
29
+ * `{ type: 'formfeed:zoom', deltaY, deltaMode }` or `{ type: 'formfeed:zoom', step: 'in' | 'out' | 'reset' }`.
30
+ * The office quick preview's frame uses the same script.
31
+ */
32
+ export declare const zoomGestureScript = "<script>\n(function () {\n var say = function (message) { try { parent.postMessage(message, '*'); } catch (e) {} };\n window.addEventListener('wheel', function (event) {\n if (!event.ctrlKey && !event.metaKey) return;\n event.preventDefault();\n say({ type: 'formfeed:zoom', deltaY: event.deltaY, deltaMode: event.deltaMode });\n }, { passive: false });\n window.addEventListener('keydown', function (event) {\n if (!(event.ctrlKey || event.metaKey) || event.altKey) return;\n var step = event.key === '+' || event.key === '=' ? 'in' : event.key === '-' ? 'out' : event.key === '0' ? 'reset' : null;\n if (!step) return;\n event.preventDefault();\n say({ type: 'formfeed:zoom', step: step });\n });\n})();\n</script>";
21
33
  export declare function flowDocument(draft: RenderedDraft): string;
22
34
  export interface PagedOptions {
23
35
  /** URL of the Paged.js polyfill the frame can load. */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Compares two data schemas from the caller's point of view (spec 19 §3.1): the caller sends data,
3
+ * the template reads it. A change is breaking when data that fitted the previous schema may no
4
+ * longer fit the next one. Keywords this module does not analyse compare by deep equality and count
5
+ * as breaking when they differ, so the check asks rather than waves a change through.
6
+ *
7
+ * Worker-safe (no Node APIs): the gateway runs it before publishing and before moving a channel.
8
+ */
9
+ export type SchemaChangeKind = 'field-added-required' | 'field-added-optional' | 'field-removed' | 'field-became-required' | 'field-became-optional' | 'type-narrowed' | 'type-widened' | 'enum-values-removed' | 'enum-values-added' | 'enum-added' | 'enum-removed' | 'const-changed' | 'constraint-tightened' | 'constraint-relaxed' | 'additional-properties-closed' | 'additional-properties-opened' | 'keyword-changed';
10
+ export interface SchemaChange {
11
+ /** Dotted field path (`customer.email`, `items[]`); empty for the root. */
12
+ path: string;
13
+ /** JSON pointer into the next schema (or the previous one for a removal). */
14
+ pointer: string;
15
+ kind: SchemaChangeKind;
16
+ breaking: boolean;
17
+ message: string;
18
+ }
19
+ export interface SchemaDiff {
20
+ breaking: SchemaChange[];
21
+ safe: SchemaChange[];
22
+ }
23
+ /**
24
+ * The changes from `previous` to `next`, split into breaking and safe. `null` on either side means
25
+ * "no schema": nothing to compare, so the result is empty.
26
+ */
27
+ export declare function diffSchemas(previous: unknown, next: unknown): SchemaDiff;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Compares two data schemas from the caller's point of view (spec 19 §3.1): the caller sends data,
3
+ * the template reads it. A change is breaking when data that fitted the previous schema may no
4
+ * longer fit the next one. Keywords this module does not analyse compare by deep equality and count
5
+ * as breaking when they differ, so the check asks rather than waves a change through.
6
+ *
7
+ * Worker-safe (no Node APIs): the gateway runs it before publishing and before moving a channel.
8
+ */
9
+ export type SchemaChangeKind = 'field-added-required' | 'field-added-optional' | 'field-removed' | 'field-became-required' | 'field-became-optional' | 'type-narrowed' | 'type-widened' | 'enum-values-removed' | 'enum-values-added' | 'enum-added' | 'enum-removed' | 'const-changed' | 'constraint-tightened' | 'constraint-relaxed' | 'additional-properties-closed' | 'additional-properties-opened' | 'keyword-changed';
10
+ export interface SchemaChange {
11
+ /** Dotted field path (`customer.email`, `items[]`); empty for the root. */
12
+ path: string;
13
+ /** JSON pointer into the next schema (or the previous one for a removal). */
14
+ pointer: string;
15
+ kind: SchemaChangeKind;
16
+ breaking: boolean;
17
+ message: string;
18
+ }
19
+ export interface SchemaDiff {
20
+ breaking: SchemaChange[];
21
+ safe: SchemaChange[];
22
+ }
23
+ /**
24
+ * The changes from `previous` to `next`, split into breaking and safe. `null` on either side means
25
+ * "no schema": nothing to compare, so the result is empty.
26
+ */
27
+ export declare function diffSchemas(previous: unknown, next: unknown): SchemaDiff;
@@ -13,6 +13,12 @@ export interface JsonSchema {
13
13
  additionalProperties?: boolean;
14
14
  }
15
15
  export declare function inferSchema(sample: unknown): JsonSchema;
16
+ /**
17
+ * One schema over several data sets (spec 19 §2.5): a field is required only when every set has
18
+ * it, the way array elements are merged. The editor's "Store as contract" uses it, so a field that
19
+ * only one data set carries becomes optional.
20
+ */
21
+ export declare function inferSchemaFromDataSets(samples: unknown[]): JsonSchema;
16
22
  /** Flat list of dotted paths with their types, for completion items in the editor. */
17
23
  export declare function schemaPaths(schema: JsonSchema, prefix?: string[]): Array<{
18
24
  path: string[];
@@ -20,3 +26,5 @@ export declare function schemaPaths(schema: JsonSchema, prefix?: string[]): Arra
20
26
  format?: string;
21
27
  example?: unknown;
22
28
  }>;
29
+ export { diffSchemas } from './schema-diff.cjs';
30
+ export type { SchemaChange, SchemaChangeKind, SchemaDiff } from './schema-diff.cjs';
@@ -13,6 +13,12 @@ export interface JsonSchema {
13
13
  additionalProperties?: boolean;
14
14
  }
15
15
  export declare function inferSchema(sample: unknown): JsonSchema;
16
+ /**
17
+ * One schema over several data sets (spec 19 §2.5): a field is required only when every set has
18
+ * it, the way array elements are merged. The editor's "Store as contract" uses it, so a field that
19
+ * only one data set carries becomes optional.
20
+ */
21
+ export declare function inferSchemaFromDataSets(samples: unknown[]): JsonSchema;
16
22
  /** Flat list of dotted paths with their types, for completion items in the editor. */
17
23
  export declare function schemaPaths(schema: JsonSchema, prefix?: string[]): Array<{
18
24
  path: string[];
@@ -20,3 +26,5 @@ export declare function schemaPaths(schema: JsonSchema, prefix?: string[]): Arra
20
26
  format?: string;
21
27
  example?: unknown;
22
28
  }>;
29
+ export { diffSchemas } from './schema-diff.js';
30
+ export type { SchemaChange, SchemaChangeKind, SchemaDiff } from './schema-diff.js';
@@ -3,20 +3,57 @@ import type { EngineId } from './types.cjs';
3
3
  * Snippet library (spec 06 §2): building blocks in every engine's syntax with the sample data keys
4
4
  * they expect, so inserting one also completes the data panel.
5
5
  */
6
+ export type SnippetGroup = 'letter' | 'invoice' | 'page' | 'report';
6
7
  export interface Snippet {
7
8
  id: string;
8
9
  title: string;
9
10
  description: string;
11
+ /** Where the library lists the block. */
12
+ group: SnippetGroup;
13
+ /** Document kinds the block makes sense in; absent means both (page breaks and footers are PDF only). */
14
+ kinds?: Array<'pdf' | 'image'>;
15
+ /**
16
+ * The document the block belongs in; absent means the template body. Header and footer blocks
17
+ * style themselves inline, because Chromium renders those templates without the page's CSS.
18
+ */
19
+ target?: 'header' | 'footer';
10
20
  /** Source per engine; `$0` marks the final cursor position (Monaco snippet syntax). */
11
21
  source: Record<EngineId, string>;
12
22
  /** Sample data merged (deep, without overwriting) into the template's sample data. */
13
23
  sampleData?: Record<string, unknown>;
14
- /** CSS appended to the template's stylesheet if the selector prefix is absent. */
24
+ /**
25
+ * CSS whose rules are appended when the stylesheet lacks their selector (`mergeSnippetCss`).
26
+ * Colours and heading fonts read the brand kit's `--brand-*` variables with a neutral fallback.
27
+ */
15
28
  css?: string;
29
+ /**
30
+ * Label texts per language for the `t` calls in `source`, merged into the template's dictionaries
31
+ * without overwriting (`mergeSnippetI18n`), so a German template gets German labels.
32
+ */
33
+ i18n?: Record<string, Record<string, string>>;
16
34
  }
17
35
  export declare const snippets: Snippet[];
18
36
  export declare function snippetsFor(engine: EngineId): Array<Snippet & {
19
37
  code: string;
20
38
  }>;
39
+ /** Snippet code without Monaco tab stops (`$0`, `$1`, `${1:default}` keeps its default), as it renders. */
40
+ export declare function snippetText(code: string): string;
41
+ /**
42
+ * The line that includes a partial in the engine's syntax, the form the gateway's
43
+ * `sharedPartialsFor` recognises; `undefined` for a name that syntax cannot carry. Liquid uses
44
+ * `include`, because `render` hides the template's data from the partial.
45
+ */
46
+ export declare function partialInclude(engine: EngineId, name: string): string | undefined;
47
+ /**
48
+ * Appends the rules of a snippet's CSS whose selector list the stylesheet does not have yet, so a
49
+ * template that already styles `.ff-lines` itself keeps that rule and still gets the others.
50
+ */
51
+ export declare function mergeSnippetCss(css: string, snippetCss: string | undefined): string;
52
+ /**
53
+ * Adds a snippet's label texts to the template's dictionaries without overwriting a key: English
54
+ * always (the `t` helper's last fallback), plus every language the template renders in or already
55
+ * has a dictionary for. A `de-DE` dictionary gets its keys through `de`, which `t` reads next.
56
+ */
57
+ export declare function mergeSnippetI18n(dictionaries: unknown, extra: Snippet['i18n'], locale: string): unknown;
21
58
  /** Deep merge that keeps existing values; arrays are taken from the base when present. */
22
59
  export declare function mergeSampleData(base: unknown, extra: Record<string, unknown> | undefined): unknown;
@@ -3,20 +3,57 @@ import type { EngineId } from './types.js';
3
3
  * Snippet library (spec 06 §2): building blocks in every engine's syntax with the sample data keys
4
4
  * they expect, so inserting one also completes the data panel.
5
5
  */
6
+ export type SnippetGroup = 'letter' | 'invoice' | 'page' | 'report';
6
7
  export interface Snippet {
7
8
  id: string;
8
9
  title: string;
9
10
  description: string;
11
+ /** Where the library lists the block. */
12
+ group: SnippetGroup;
13
+ /** Document kinds the block makes sense in; absent means both (page breaks and footers are PDF only). */
14
+ kinds?: Array<'pdf' | 'image'>;
15
+ /**
16
+ * The document the block belongs in; absent means the template body. Header and footer blocks
17
+ * style themselves inline, because Chromium renders those templates without the page's CSS.
18
+ */
19
+ target?: 'header' | 'footer';
10
20
  /** Source per engine; `$0` marks the final cursor position (Monaco snippet syntax). */
11
21
  source: Record<EngineId, string>;
12
22
  /** Sample data merged (deep, without overwriting) into the template's sample data. */
13
23
  sampleData?: Record<string, unknown>;
14
- /** CSS appended to the template's stylesheet if the selector prefix is absent. */
24
+ /**
25
+ * CSS whose rules are appended when the stylesheet lacks their selector (`mergeSnippetCss`).
26
+ * Colours and heading fonts read the brand kit's `--brand-*` variables with a neutral fallback.
27
+ */
15
28
  css?: string;
29
+ /**
30
+ * Label texts per language for the `t` calls in `source`, merged into the template's dictionaries
31
+ * without overwriting (`mergeSnippetI18n`), so a German template gets German labels.
32
+ */
33
+ i18n?: Record<string, Record<string, string>>;
16
34
  }
17
35
  export declare const snippets: Snippet[];
18
36
  export declare function snippetsFor(engine: EngineId): Array<Snippet & {
19
37
  code: string;
20
38
  }>;
39
+ /** Snippet code without Monaco tab stops (`$0`, `$1`, `${1:default}` keeps its default), as it renders. */
40
+ export declare function snippetText(code: string): string;
41
+ /**
42
+ * The line that includes a partial in the engine's syntax, the form the gateway's
43
+ * `sharedPartialsFor` recognises; `undefined` for a name that syntax cannot carry. Liquid uses
44
+ * `include`, because `render` hides the template's data from the partial.
45
+ */
46
+ export declare function partialInclude(engine: EngineId, name: string): string | undefined;
47
+ /**
48
+ * Appends the rules of a snippet's CSS whose selector list the stylesheet does not have yet, so a
49
+ * template that already styles `.ff-lines` itself keeps that rule and still gets the others.
50
+ */
51
+ export declare function mergeSnippetCss(css: string, snippetCss: string | undefined): string;
52
+ /**
53
+ * Adds a snippet's label texts to the template's dictionaries without overwriting a key: English
54
+ * always (the `t` helper's last fallback), plus every language the template renders in or already
55
+ * has a dictionary for. A `de-DE` dictionary gets its keys through `de`, which `t` reads next.
56
+ */
57
+ export declare function mergeSnippetI18n(dictionaries: unknown, extra: Snippet['i18n'], locale: string): unknown;
21
58
  /** Deep merge that keeps existing values; arrays are taken from the base when present. */
22
59
  export declare function mergeSampleData(base: unknown, extra: Record<string, unknown> | undefined): unknown;
@@ -0,0 +1,27 @@
1
+ import type { EngineId } from './types.cjs';
2
+ /**
3
+ * Click-to-source for the editor's previews (spec 06 §3): every HTML start tag written literally in
4
+ * the template source gets `data-ff-src="<file>:<line>:<column>"` right after its tag name, so the
5
+ * rendered element says where it came from instead of the editor searching the template for it.
6
+ * Elements from a loop all point at the loop body, which is the line that wrote them.
7
+ *
8
+ * The annotation must never change what the template means: tags inside template syntax, in
9
+ * blocks whose output is captured or filtered as a string (`{% set %}…{% endset %}`, `{% filter %}`,
10
+ * `{% capture %}`), in raw and comment blocks, in `<script>`/`<style>`/`<textarea>`/`<title>` and
11
+ * HTML comments, closing tags, declarations, tags whose name is built by template syntax and tags
12
+ * that already carry the attribute stay untouched. Removing the attribute from the rendered output
13
+ * gives the document of the original source. Preview only: saved versions, API renders and the
14
+ * render-worker never see it.
15
+ */
16
+ export type SourceFile = 'body' | 'header' | 'footer';
17
+ export declare const sourceAttribute = "data-ff-src";
18
+ export interface SourceLocation {
19
+ file: SourceFile;
20
+ /** 1-based, as Monaco counts. */
21
+ line: number;
22
+ /** 1-based UTF-16 column of the tag's `<`. */
23
+ column: number;
24
+ }
25
+ /** Parses a `data-ff-src` value; null when it is not one. */
26
+ export declare function parseSourceAttribute(value: string | null | undefined): SourceLocation | null;
27
+ export declare function annotateSourcePositions(source: string, engine: EngineId, file: SourceFile): string;
@@ -0,0 +1,27 @@
1
+ import type { EngineId } from './types.js';
2
+ /**
3
+ * Click-to-source for the editor's previews (spec 06 §3): every HTML start tag written literally in
4
+ * the template source gets `data-ff-src="<file>:<line>:<column>"` right after its tag name, so the
5
+ * rendered element says where it came from instead of the editor searching the template for it.
6
+ * Elements from a loop all point at the loop body, which is the line that wrote them.
7
+ *
8
+ * The annotation must never change what the template means: tags inside template syntax, in
9
+ * blocks whose output is captured or filtered as a string (`{% set %}…{% endset %}`, `{% filter %}`,
10
+ * `{% capture %}`), in raw and comment blocks, in `<script>`/`<style>`/`<textarea>`/`<title>` and
11
+ * HTML comments, closing tags, declarations, tags whose name is built by template syntax and tags
12
+ * that already carry the attribute stay untouched. Removing the attribute from the rendered output
13
+ * gives the document of the original source. Preview only: saved versions, API renders and the
14
+ * render-worker never see it.
15
+ */
16
+ export type SourceFile = 'body' | 'header' | 'footer';
17
+ export declare const sourceAttribute = "data-ff-src";
18
+ export interface SourceLocation {
19
+ file: SourceFile;
20
+ /** 1-based, as Monaco counts. */
21
+ line: number;
22
+ /** 1-based UTF-16 column of the tag's `<`. */
23
+ column: number;
24
+ }
25
+ /** Parses a `data-ff-src` value; null when it is not one. */
26
+ export declare function parseSourceAttribute(value: string | null | undefined): SourceLocation | null;
27
+ export declare function annotateSourcePositions(source: string, engine: EngineId, file: SourceFile): string;
@@ -1,3 +1,4 @@
1
+ import type { BrandContext } from './brand.cjs';
1
2
  /**
2
3
  * Shared contracts of `@formfeed/engine` (spec 05). Used by the Angular editor (browser) and the
3
4
  * Fly.io render-worker (Node); nothing here may touch Node-only APIs.
@@ -84,6 +85,18 @@ export interface RenderContext {
84
85
  i18n?: Record<string, Record<string, string>>;
85
86
  /** Base URL for `asset()` (workspace asset root on the CDN). */
86
87
  assetBaseUrl?: string;
88
+ /**
89
+ * The organisation's brand kit (spec 18): the template global `brand` (request data of the same
90
+ * name wins) and the `--brand-*` variables the assembler injects.
91
+ */
92
+ brand?: BrandContext;
93
+ /**
94
+ * `office` renders the text of an office part (spec 22 §4.3): the engines do not escape, because the
95
+ * text is escaped as a whole when the markup is restored (`office/template-text.ts`). HTML by default.
96
+ */
97
+ mode?: 'html' | 'office';
98
+ /** Office mode, set by `renderOffice`: see `HelperContext.drawing`. */
99
+ drawing?: (request: OfficeDrawingRequest) => string;
87
100
  }
88
101
  export interface CompileOptions {
89
102
  /** Name used in error messages and as the root include name. */
@@ -128,7 +141,30 @@ export interface HelperContext {
128
141
  currency: string;
129
142
  i18n?: Record<string, Record<string, string>>;
130
143
  assetBaseUrl?: string;
131
- }
144
+ /** `office` while an office template is filled (spec 22 §4.4); helpers that emit markup behave differently. */
145
+ mode?: 'html' | 'office';
146
+ /**
147
+ * Office mode: registers a drawing (a code, an image or a page break) and returns the placeholder
148
+ * the post pass turns into it. Set by `renderOffice`.
149
+ */
150
+ drawing?: (request: OfficeDrawingRequest) => string;
151
+ }
152
+ /** What a helper asks the office post pass to place where its placeholder stands. */
153
+ export type OfficeDrawingRequest = {
154
+ kind: 'svg';
155
+ svg: string;
156
+ width?: number | string;
157
+ height?: number | string;
158
+ alt?: string;
159
+ } | {
160
+ kind: 'url';
161
+ url: string;
162
+ width?: number | string;
163
+ height?: number | string;
164
+ alt?: string;
165
+ } | {
166
+ kind: 'page-break';
167
+ };
132
168
  export interface HelperRegistry {
133
169
  /** Registered helpers keyed by canonical name. */
134
170
  readonly definitions: ReadonlyMap<string, HelperDefinition>;
@@ -1,3 +1,4 @@
1
+ import type { BrandContext } from './brand.js';
1
2
  /**
2
3
  * Shared contracts of `@formfeed/engine` (spec 05). Used by the Angular editor (browser) and the
3
4
  * Fly.io render-worker (Node); nothing here may touch Node-only APIs.
@@ -84,6 +85,18 @@ export interface RenderContext {
84
85
  i18n?: Record<string, Record<string, string>>;
85
86
  /** Base URL for `asset()` (workspace asset root on the CDN). */
86
87
  assetBaseUrl?: string;
88
+ /**
89
+ * The organisation's brand kit (spec 18): the template global `brand` (request data of the same
90
+ * name wins) and the `--brand-*` variables the assembler injects.
91
+ */
92
+ brand?: BrandContext;
93
+ /**
94
+ * `office` renders the text of an office part (spec 22 §4.3): the engines do not escape, because the
95
+ * text is escaped as a whole when the markup is restored (`office/template-text.ts`). HTML by default.
96
+ */
97
+ mode?: 'html' | 'office';
98
+ /** Office mode, set by `renderOffice`: see `HelperContext.drawing`. */
99
+ drawing?: (request: OfficeDrawingRequest) => string;
87
100
  }
88
101
  export interface CompileOptions {
89
102
  /** Name used in error messages and as the root include name. */
@@ -128,7 +141,30 @@ export interface HelperContext {
128
141
  currency: string;
129
142
  i18n?: Record<string, Record<string, string>>;
130
143
  assetBaseUrl?: string;
131
- }
144
+ /** `office` while an office template is filled (spec 22 §4.4); helpers that emit markup behave differently. */
145
+ mode?: 'html' | 'office';
146
+ /**
147
+ * Office mode: registers a drawing (a code, an image or a page break) and returns the placeholder
148
+ * the post pass turns into it. Set by `renderOffice`.
149
+ */
150
+ drawing?: (request: OfficeDrawingRequest) => string;
151
+ }
152
+ /** What a helper asks the office post pass to place where its placeholder stands. */
153
+ export type OfficeDrawingRequest = {
154
+ kind: 'svg';
155
+ svg: string;
156
+ width?: number | string;
157
+ height?: number | string;
158
+ alt?: string;
159
+ } | {
160
+ kind: 'url';
161
+ url: string;
162
+ width?: number | string;
163
+ height?: number | string;
164
+ alt?: string;
165
+ } | {
166
+ kind: 'page-break';
167
+ };
132
168
  export interface HelperRegistry {
133
169
  /** Registered helpers keyed by canonical name. */
134
170
  readonly definitions: ReadonlyMap<string, HelperDefinition>;
@@ -33,6 +33,7 @@ declare module 'nunjucks/browser/nunjucks.js' {
33
33
  Loader: typeof N.Loader;
34
34
  runtime: typeof N.runtime & {
35
35
  memberLookup: (obj: unknown, val: unknown, ...rest: unknown[]) => unknown;
36
+ suppressValue: (val: unknown, autoescape: boolean) => unknown;
36
37
  };
37
38
  /** The code generator; patched for `not` precedence and namespace attributes (engines/jinja2.ts). */
38
39
  compiler: {
@@ -33,6 +33,7 @@ declare module 'nunjucks/browser/nunjucks.js' {
33
33
  Loader: typeof N.Loader;
34
34
  runtime: typeof N.runtime & {
35
35
  memberLookup: (obj: unknown, val: unknown, ...rest: unknown[]) => unknown;
36
+ suppressValue: (val: unknown, autoescape: boolean) => unknown;
36
37
  };
37
38
  /** The code generator; patched for `not` precedence and namespace attributes (engines/jinja2.ts). */
38
39
  compiler: {