@formfeed/engine 0.2.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.
Files changed (64) hide show
  1. package/THIRD_PARTY_LICENSES.md +31 -0
  2. package/index.cjs +4204 -166
  3. package/index.js +4204 -166
  4. package/package.json +1 -1
  5. package/types/index.d.cts +19 -9
  6. package/types/index.d.ts +19 -9
  7. package/types/lib/assemble.d.cts +24 -9
  8. package/types/lib/assemble.d.ts +24 -9
  9. package/types/lib/brand.d.cts +46 -0
  10. package/types/lib/brand.d.ts +46 -0
  11. package/types/lib/helpers/document.d.cts +24 -0
  12. package/types/lib/helpers/document.d.ts +24 -0
  13. package/types/lib/helpers/index.d.cts +1 -0
  14. package/types/lib/helpers/index.d.ts +1 -0
  15. package/types/lib/helpers/office.d.cts +4 -0
  16. package/types/lib/helpers/office.d.ts +4 -0
  17. package/types/lib/import/apitemplate.d.cts +60 -0
  18. package/types/lib/import/apitemplate.d.ts +60 -0
  19. package/types/lib/office/detect.d.cts +36 -0
  20. package/types/lib/office/detect.d.ts +36 -0
  21. package/types/lib/office/drawings.d.cts +120 -0
  22. package/types/lib/office/drawings.d.ts +120 -0
  23. package/types/lib/office/errors.d.cts +21 -0
  24. package/types/lib/office/errors.d.ts +21 -0
  25. package/types/lib/office/fonts.d.cts +21 -0
  26. package/types/lib/office/fonts.d.ts +21 -0
  27. package/types/lib/office/ids.d.cts +1 -0
  28. package/types/lib/office/ids.d.ts +1 -0
  29. package/types/lib/office/index.d.cts +18 -0
  30. package/types/lib/office/index.d.ts +18 -0
  31. package/types/lib/office/render.d.cts +79 -0
  32. package/types/lib/office/render.d.ts +79 -0
  33. package/types/lib/office/sanitise.d.cts +35 -0
  34. package/types/lib/office/sanitise.d.ts +35 -0
  35. package/types/lib/office/slides.d.cts +47 -0
  36. package/types/lib/office/slides.d.ts +47 -0
  37. package/types/lib/office/starter-pptx.d.cts +6 -0
  38. package/types/lib/office/starter-pptx.d.ts +6 -0
  39. package/types/lib/office/starter.d.cts +24 -0
  40. package/types/lib/office/starter.d.ts +24 -0
  41. package/types/lib/office/structure.d.cts +51 -0
  42. package/types/lib/office/structure.d.ts +51 -0
  43. package/types/lib/office/tags.d.cts +51 -0
  44. package/types/lib/office/tags.d.ts +51 -0
  45. package/types/lib/office/template-text.d.cts +59 -0
  46. package/types/lib/office/template-text.d.ts +59 -0
  47. package/types/lib/office/xml.d.cts +44 -0
  48. package/types/lib/office/xml.d.ts +44 -0
  49. package/types/lib/office/zip.d.cts +49 -0
  50. package/types/lib/office/zip.d.ts +49 -0
  51. package/types/lib/output.d.cts +29 -0
  52. package/types/lib/output.d.ts +29 -0
  53. package/types/lib/preview.d.cts +9 -1
  54. package/types/lib/preview.d.ts +9 -1
  55. package/types/lib/schema-diff.d.cts +27 -0
  56. package/types/lib/schema-diff.d.ts +27 -0
  57. package/types/lib/schema.d.cts +8 -0
  58. package/types/lib/schema.d.ts +8 -0
  59. package/types/lib/snippets.d.cts +38 -1
  60. package/types/lib/snippets.d.ts +38 -1
  61. package/types/lib/source-positions.d.cts +27 -0
  62. package/types/lib/source-positions.d.ts +27 -0
  63. package/types/lib/types.d.cts +37 -1
  64. package/types/lib/types.d.ts +37 -1
@@ -0,0 +1,47 @@
1
+ import type { ResolvedDrawing } from './drawings.js';
2
+ /**
3
+ * Pictures on slides (spec 22 §10 step 7). A slide cannot hold a picture inside text, so a drawing
4
+ * placeholder in a text box becomes a picture shape (`p:pic`) beside it:
5
+ *
6
+ * - a text box that holds nothing but drawings is replaced by them, placed in its box, so a member
7
+ * draws a box where the code goes and types `{{ qrcode(url) }}` into it;
8
+ * - otherwise the placeholder is removed from the text and the picture is placed over the box, in
9
+ * front of the text;
10
+ * - a picture without a size of its own is fitted into the box, keeping its aspect; several pictures
11
+ * share the box from top to bottom. A box that grows with its text (`a:spAutoFit`, PowerPoint's
12
+ * default for text boxes) is only as high as one line, so there the width alone decides;
13
+ * - a shape whose position comes from the slide layout (no `a:xfrm` of its own) puts the pictures in
14
+ * the middle of the slide.
15
+ */
16
+ export interface SlideBox {
17
+ x: number;
18
+ y: number;
19
+ cx: number;
20
+ cy: number;
21
+ }
22
+ /** `p:sldSz` of `ppt/presentation.xml`; 16:9 at 33.867 cm by 19.05 cm when it names none. */
23
+ export declare function slideSize(presentation: string | null): {
24
+ cx: number;
25
+ cy: number;
26
+ };
27
+ /** A picture shape, with the namespaces it uses declared on itself. */
28
+ export declare function slidePicture(picture: Extract<ResolvedDrawing, {
29
+ kind: 'picture';
30
+ }>, rId: string, id: number, box: SlideBox): string;
31
+ /** Where each picture goes: its own size or fitted into its share of the box, centred in it. */
32
+ export declare function layoutPictures(pictures: ReadonlyArray<Extract<ResolvedDrawing, {
33
+ kind: 'picture';
34
+ }>>, box: SlideBox, growsWithText?: boolean): SlideBox[];
35
+ /**
36
+ * Replaces the drawing placeholders of a filled slide (or notes) part with picture shapes. `picture`
37
+ * returns the placed markup for a resolved drawing, with its relationship and id.
38
+ */
39
+ export declare function placeSlidePictures(xml: string, pattern: RegExp, drawingFor: (index: number) => ResolvedDrawing | undefined, pictureMarkup: (picture: Extract<ResolvedDrawing, {
40
+ kind: 'picture';
41
+ }>, box: SlideBox) => string, slide: {
42
+ cx: number;
43
+ cy: number;
44
+ }): {
45
+ xml: string;
46
+ warnings: string[];
47
+ };
@@ -0,0 +1,6 @@
1
+ import type { EngineId } from '../types.cjs';
2
+ /** The starter presentation for an engine, with the sample data that fills it. */
3
+ export declare function starterPresentation(engine: EngineId): {
4
+ bytes: Uint8Array;
5
+ sampleData: Record<string, unknown>;
6
+ };
@@ -0,0 +1,6 @@
1
+ import type { EngineId } from '../types.js';
2
+ /** The starter presentation for an engine, with the sample data that fills it. */
3
+ export declare function starterPresentation(engine: EngineId): {
4
+ bytes: Uint8Array;
5
+ sampleData: Record<string, unknown>;
6
+ };
@@ -0,0 +1,24 @@
1
+ import type { EngineId } from '../types.cjs';
2
+ /** The tags of one engine: a value, a value through a helper, and a loop over the lines. */
3
+ export declare function starterTags(engine: EngineId): {
4
+ number: string;
5
+ customer: string;
6
+ date: string;
7
+ loopOpen: string;
8
+ line: string;
9
+ lineTotal: string;
10
+ loopClose: string;
11
+ total: string;
12
+ ifOpen: string;
13
+ note: string;
14
+ ifClose: string;
15
+ code: string;
16
+ codeFit: string;
17
+ };
18
+ /** The starter document for an engine, with the sample data that fills it. */
19
+ export declare function starterDocument(engine: EngineId): {
20
+ bytes: Uint8Array;
21
+ sampleData: Record<string, unknown>;
22
+ };
23
+ /** The data the starter files are filled with; a fresh copy each time. */
24
+ export declare function starterSampleData(): Record<string, unknown>;
@@ -0,0 +1,24 @@
1
+ import type { EngineId } from '../types.js';
2
+ /** The tags of one engine: a value, a value through a helper, and a loop over the lines. */
3
+ export declare function starterTags(engine: EngineId): {
4
+ number: string;
5
+ customer: string;
6
+ date: string;
7
+ loopOpen: string;
8
+ line: string;
9
+ lineTotal: string;
10
+ loopClose: string;
11
+ total: string;
12
+ ifOpen: string;
13
+ note: string;
14
+ ifClose: string;
15
+ code: string;
16
+ codeFit: string;
17
+ };
18
+ /** The starter document for an engine, with the sample data that fills it. */
19
+ export declare function starterDocument(engine: EngineId): {
20
+ bytes: Uint8Array;
21
+ sampleData: Record<string, unknown>;
22
+ };
23
+ /** The data the starter files are filled with; a fresh copy each time. */
24
+ export declare function starterSampleData(): Record<string, unknown>;
@@ -0,0 +1,51 @@
1
+ import type { OfficeDiagnostic } from './tags.cjs';
2
+ import { type XmlToken } from './xml.cjs';
3
+ /**
4
+ * Structural tags (spec 22 §4.3 step 5). A block tag alone in its paragraph stands for the paragraph:
5
+ * a loop repeats the paragraphs between its tags, not the text inside one. If that paragraph is also
6
+ * the only content of its table row, the tag stands for the row, so a loop repeats the rows between.
7
+ *
8
+ * The rewrite happens on the part's XML before it becomes template text: the paragraph or row is
9
+ * replaced by a `<ff:tag>` element holding only the tag. `toTemplateSource` reads that element's text
10
+ * like a text element, and `fromTemplateOutput` removes the element again after rendering. Structure
11
+ * the file format needs survives: a paragraph that carries a section break stays (empty), and a cell
12
+ * never ends up without a paragraph.
13
+ */
14
+ export declare const STRUCTURAL_ELEMENT = "ff:tag";
15
+ export interface StructuredPart {
16
+ xml: string;
17
+ diagnostics: OfficeDiagnostic[];
18
+ }
19
+ export interface Span {
20
+ name: string;
21
+ start: number;
22
+ end: number;
23
+ parent: Span | null;
24
+ children: Span[];
25
+ }
26
+ type Role = 'open' | 'close' | 'middle' | 'single';
27
+ /** What a tag does to structure, or null when the text is not exactly one tag. */
28
+ export declare function blockRole(text: string): {
29
+ role: Role;
30
+ keyword: string;
31
+ } | null;
32
+ /**
33
+ * Text boxes carry a VML copy of their text in `mc:Fallback`, which would be filled on its own and
34
+ * loop twice. A fallback holding a tag is removed; Word 2010 and later and LibreOffice read the choice.
35
+ */
36
+ export declare function dropTaggedFallbacks(xml: string, part?: string): string;
37
+ export declare function applyStructure(xml: string, part: string): StructuredPart;
38
+ /**
39
+ * A loop over an empty list can leave a table without rows, which Word and PowerPoint refuse (spec 22
40
+ * §4.3 step 5): such a Word table is removed, and so is the graphic frame of such a slide table.
41
+ */
42
+ export declare function removeEmptyTables(xml: string): string;
43
+ /** Builds the element tree; `start`/`end` are token indexes of the open and close tags. */
44
+ export declare function buildSpans(tokens: XmlToken[]): Span;
45
+ /** Joins the tokens back, with the token ranges replaced; an empty range inserts before `start`. */
46
+ export declare function rewrite(tokens: XmlToken[], replacements: Array<{
47
+ start: number;
48
+ end: number;
49
+ text: string;
50
+ }>): string;
51
+ export {};
@@ -0,0 +1,51 @@
1
+ import type { OfficeDiagnostic } from './tags.js';
2
+ import { type XmlToken } from './xml.js';
3
+ /**
4
+ * Structural tags (spec 22 §4.3 step 5). A block tag alone in its paragraph stands for the paragraph:
5
+ * a loop repeats the paragraphs between its tags, not the text inside one. If that paragraph is also
6
+ * the only content of its table row, the tag stands for the row, so a loop repeats the rows between.
7
+ *
8
+ * The rewrite happens on the part's XML before it becomes template text: the paragraph or row is
9
+ * replaced by a `<ff:tag>` element holding only the tag. `toTemplateSource` reads that element's text
10
+ * like a text element, and `fromTemplateOutput` removes the element again after rendering. Structure
11
+ * the file format needs survives: a paragraph that carries a section break stays (empty), and a cell
12
+ * never ends up without a paragraph.
13
+ */
14
+ export declare const STRUCTURAL_ELEMENT = "ff:tag";
15
+ export interface StructuredPart {
16
+ xml: string;
17
+ diagnostics: OfficeDiagnostic[];
18
+ }
19
+ export interface Span {
20
+ name: string;
21
+ start: number;
22
+ end: number;
23
+ parent: Span | null;
24
+ children: Span[];
25
+ }
26
+ type Role = 'open' | 'close' | 'middle' | 'single';
27
+ /** What a tag does to structure, or null when the text is not exactly one tag. */
28
+ export declare function blockRole(text: string): {
29
+ role: Role;
30
+ keyword: string;
31
+ } | null;
32
+ /**
33
+ * Text boxes carry a VML copy of their text in `mc:Fallback`, which would be filled on its own and
34
+ * loop twice. A fallback holding a tag is removed; Word 2010 and later and LibreOffice read the choice.
35
+ */
36
+ export declare function dropTaggedFallbacks(xml: string, part?: string): string;
37
+ export declare function applyStructure(xml: string, part: string): StructuredPart;
38
+ /**
39
+ * A loop over an empty list can leave a table without rows, which Word and PowerPoint refuse (spec 22
40
+ * §4.3 step 5): such a Word table is removed, and so is the graphic frame of such a slide table.
41
+ */
42
+ export declare function removeEmptyTables(xml: string): string;
43
+ /** Builds the element tree; `start`/`end` are token indexes of the open and close tags. */
44
+ export declare function buildSpans(tokens: XmlToken[]): Span;
45
+ /** Joins the tokens back, with the token ranges replaced; an empty range inserts before `start`. */
46
+ export declare function rewrite(tokens: XmlToken[], replacements: Array<{
47
+ start: number;
48
+ end: number;
49
+ text: string;
50
+ }>): string;
51
+ export {};
@@ -0,0 +1,51 @@
1
+ import { type TemplateSource } from './template-text.cjs';
2
+ /**
3
+ * Normalising the tags Word leaves behind (spec 22 §4.3 step 4).
4
+ *
5
+ * Word splits typed text into several runs — spell-check marks, revision ids, a changed font — so
6
+ * `{{ customer.name }}` often sits in three `<w:r>` elements, and the template source that comes out
7
+ * of `toTemplateSource` has markup placeholders inside the tag. Here a tag's characters move into the
8
+ * run where the tag starts (which keeps that run's formatting) by dropping the run boundaries in
9
+ * between, and Word's autocorrect inside the tag is undone, because `{{ date(x, "short") }}` becomes
10
+ * curly quotes as the customer types and would otherwise be a syntax error nobody can see.
11
+ *
12
+ * A tag that crosses a paragraph, a hyperlink, a content control, a field or a tracked change is not
13
+ * merged: moving the text would move it out of its element. It reports `office-tag-spans-elements`.
14
+ */
15
+ export type OfficeDiagnosticCode = 'office-tag-spans-elements' | 'office-tag-autocorrected' | 'office-tag-in-field' | 'office-block-unbalanced' | 'office-reference-in-loop' | 'office-font-substituted' | 'unsupported-in-office';
16
+ export interface OfficeDiagnostic {
17
+ severity: 'error' | 'warning' | 'info';
18
+ code: OfficeDiagnosticCode;
19
+ message: string;
20
+ /** Part the diagnostic belongs to, `word/document.xml`. */
21
+ part: string;
22
+ /** 1-based paragraph in that part, as the editor lists it. */
23
+ paragraph: number;
24
+ /** The tag as it reads after normalisation, for the message. */
25
+ text: string;
26
+ }
27
+ export interface NormalisedTags {
28
+ template: TemplateSource;
29
+ diagnostics: OfficeDiagnostic[];
30
+ }
31
+ export declare function normaliseTags(template: TemplateSource, part: string): NormalisedTags;
32
+ /** A tag as the office template page lists it (spec 22 §6). */
33
+ export interface OfficeTag {
34
+ part: string;
35
+ paragraph: number;
36
+ /** The tag as the engine reads it, on one line. */
37
+ text: string;
38
+ /** `output` for `{{ }}`, `block` for `{% %}` and Handlebars blocks, `comment` for `{# #}`. */
39
+ kind: 'output' | 'block' | 'comment';
40
+ }
41
+ /** Every tag of a (normalised) part with its paragraph. */
42
+ export declare function listTags(template: TemplateSource, part: string): OfficeTag[];
43
+ /** The 1-based paragraph an offset of the template source sits in (engine diagnostics). */
44
+ export declare function paragraphAtSource(template: TemplateSource, offset: number): number;
45
+ /** Undoes what Word's autocorrect does to a tag while it is typed. */
46
+ export declare function undoAutocorrect(tag: string): string;
47
+ /** Tags in text, delimiters included; unclosed openers are ignored (the engine reports them). */
48
+ export declare function findTags(text: string): Array<{
49
+ start: number;
50
+ end: number;
51
+ }>;
@@ -0,0 +1,51 @@
1
+ import { type TemplateSource } from './template-text.js';
2
+ /**
3
+ * Normalising the tags Word leaves behind (spec 22 §4.3 step 4).
4
+ *
5
+ * Word splits typed text into several runs — spell-check marks, revision ids, a changed font — so
6
+ * `{{ customer.name }}` often sits in three `<w:r>` elements, and the template source that comes out
7
+ * of `toTemplateSource` has markup placeholders inside the tag. Here a tag's characters move into the
8
+ * run where the tag starts (which keeps that run's formatting) by dropping the run boundaries in
9
+ * between, and Word's autocorrect inside the tag is undone, because `{{ date(x, "short") }}` becomes
10
+ * curly quotes as the customer types and would otherwise be a syntax error nobody can see.
11
+ *
12
+ * A tag that crosses a paragraph, a hyperlink, a content control, a field or a tracked change is not
13
+ * merged: moving the text would move it out of its element. It reports `office-tag-spans-elements`.
14
+ */
15
+ export type OfficeDiagnosticCode = 'office-tag-spans-elements' | 'office-tag-autocorrected' | 'office-tag-in-field' | 'office-block-unbalanced' | 'office-reference-in-loop' | 'office-font-substituted' | 'unsupported-in-office';
16
+ export interface OfficeDiagnostic {
17
+ severity: 'error' | 'warning' | 'info';
18
+ code: OfficeDiagnosticCode;
19
+ message: string;
20
+ /** Part the diagnostic belongs to, `word/document.xml`. */
21
+ part: string;
22
+ /** 1-based paragraph in that part, as the editor lists it. */
23
+ paragraph: number;
24
+ /** The tag as it reads after normalisation, for the message. */
25
+ text: string;
26
+ }
27
+ export interface NormalisedTags {
28
+ template: TemplateSource;
29
+ diagnostics: OfficeDiagnostic[];
30
+ }
31
+ export declare function normaliseTags(template: TemplateSource, part: string): NormalisedTags;
32
+ /** A tag as the office template page lists it (spec 22 §6). */
33
+ export interface OfficeTag {
34
+ part: string;
35
+ paragraph: number;
36
+ /** The tag as the engine reads it, on one line. */
37
+ text: string;
38
+ /** `output` for `{{ }}`, `block` for `{% %}` and Handlebars blocks, `comment` for `{# #}`. */
39
+ kind: 'output' | 'block' | 'comment';
40
+ }
41
+ /** Every tag of a (normalised) part with its paragraph. */
42
+ export declare function listTags(template: TemplateSource, part: string): OfficeTag[];
43
+ /** The 1-based paragraph an offset of the template source sits in (engine diagnostics). */
44
+ export declare function paragraphAtSource(template: TemplateSource, offset: number): number;
45
+ /** Undoes what Word's autocorrect does to a tag while it is typed. */
46
+ export declare function undoAutocorrect(tag: string): string;
47
+ /** Tags in text, delimiters included; unclosed openers are ignored (the engine reports them). */
48
+ export declare function findTags(text: string): Array<{
49
+ start: number;
50
+ end: number;
51
+ }>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * How an office part becomes template source and back (spec 22 §4.3 steps 5–7).
3
+ *
4
+ * Only the characters of text elements (`w:t`, `a:t`) are template text, decoded. Every run of markup
5
+ * between them becomes one placeholder, `U+E000 <nonce> <index> U+E001`, so the engine sees plain text
6
+ * with opaque markers and renders with its escaping switched off. On the way back every text segment is
7
+ * escaped and every placeholder restored. Whatever an expression prints therefore lands as text: `safe`,
8
+ * `{{{ }}}`, `raw`, `{% echo %}` and `capture` cannot put markup into the document, and nothing is
9
+ * escaped twice. The nonce is random per render, so data cannot forge a placeholder.
10
+ */
11
+ export type TextFlavour = 'wordprocessing' | 'drawing';
12
+ /**
13
+ * One run of markup between two pieces of text. What it contains decides whether a tag may span it:
14
+ * run boundaries inside a paragraph may be merged away, a paragraph or hyperlink boundary may not
15
+ * (`office/tags.ts`).
16
+ */
17
+ export interface MarkupPiece {
18
+ raw: string;
19
+ /** Paragraphs (`w:p`, `a:p`) that open in this piece; the index of the paragraph a tag sits in. */
20
+ paragraphOpens: number;
21
+ /** A paragraph starts or ends here, so text on both sides is not in the same paragraph. */
22
+ paragraphBoundary: boolean;
23
+ /**
24
+ * Something other than a plain run boundary: a hyperlink, a content control, a tracked change, a
25
+ * field, an alternate-content branch. Merging across it would move text out of its element.
26
+ */
27
+ containerBoundary: boolean;
28
+ /**
29
+ * The piece ends inside a text element, so text the template prints after it is document text.
30
+ * Only the last piece of a part ends outside one.
31
+ */
32
+ endsInText?: boolean;
33
+ }
34
+ export interface TemplateSource {
35
+ /** The part as template text: decoded text of the text elements, placeholders for the markup. */
36
+ source: string;
37
+ markup: MarkupPiece[];
38
+ nonce: string;
39
+ flavour: TextFlavour;
40
+ }
41
+ export interface FilledPart {
42
+ xml: string;
43
+ /** Characters XML does not allow that values contained and that were removed. */
44
+ removedCharacters: number;
45
+ }
46
+ /** The placeholder of markup piece `index`, as it appears in the template source. */
47
+ export declare function placeholderFor(nonce: string, index: number): string;
48
+ /** Matches any placeholder of this template, with the piece index in group 1. */
49
+ export declare function placeholderPattern(nonce: string): RegExp;
50
+ export declare function createNonce(random?: () => number): string;
51
+ export declare function toTemplateSource(xml: string, flavour: TextFlavour, nonce: string, part?: string): TemplateSource;
52
+ /** Restores the markup into rendered output and turns line breaks and tabs from data into elements. */
53
+ export declare function fromTemplateOutput(output: string, template: TemplateSource, part?: string): FilledPart;
54
+ /**
55
+ * Line breaks and tabs inside text elements become elements, and text elements with leading or
56
+ * trailing whitespace get `xml:space="preserve"`. Text of other elements (field codes, a table style
57
+ * id) is markup and stays as it is.
58
+ */
59
+ export declare function layoutText(xml: string, flavour: TextFlavour, part?: string): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * How an office part becomes template source and back (spec 22 §4.3 steps 5–7).
3
+ *
4
+ * Only the characters of text elements (`w:t`, `a:t`) are template text, decoded. Every run of markup
5
+ * between them becomes one placeholder, `U+E000 <nonce> <index> U+E001`, so the engine sees plain text
6
+ * with opaque markers and renders with its escaping switched off. On the way back every text segment is
7
+ * escaped and every placeholder restored. Whatever an expression prints therefore lands as text: `safe`,
8
+ * `{{{ }}}`, `raw`, `{% echo %}` and `capture` cannot put markup into the document, and nothing is
9
+ * escaped twice. The nonce is random per render, so data cannot forge a placeholder.
10
+ */
11
+ export type TextFlavour = 'wordprocessing' | 'drawing';
12
+ /**
13
+ * One run of markup between two pieces of text. What it contains decides whether a tag may span it:
14
+ * run boundaries inside a paragraph may be merged away, a paragraph or hyperlink boundary may not
15
+ * (`office/tags.ts`).
16
+ */
17
+ export interface MarkupPiece {
18
+ raw: string;
19
+ /** Paragraphs (`w:p`, `a:p`) that open in this piece; the index of the paragraph a tag sits in. */
20
+ paragraphOpens: number;
21
+ /** A paragraph starts or ends here, so text on both sides is not in the same paragraph. */
22
+ paragraphBoundary: boolean;
23
+ /**
24
+ * Something other than a plain run boundary: a hyperlink, a content control, a tracked change, a
25
+ * field, an alternate-content branch. Merging across it would move text out of its element.
26
+ */
27
+ containerBoundary: boolean;
28
+ /**
29
+ * The piece ends inside a text element, so text the template prints after it is document text.
30
+ * Only the last piece of a part ends outside one.
31
+ */
32
+ endsInText?: boolean;
33
+ }
34
+ export interface TemplateSource {
35
+ /** The part as template text: decoded text of the text elements, placeholders for the markup. */
36
+ source: string;
37
+ markup: MarkupPiece[];
38
+ nonce: string;
39
+ flavour: TextFlavour;
40
+ }
41
+ export interface FilledPart {
42
+ xml: string;
43
+ /** Characters XML does not allow that values contained and that were removed. */
44
+ removedCharacters: number;
45
+ }
46
+ /** The placeholder of markup piece `index`, as it appears in the template source. */
47
+ export declare function placeholderFor(nonce: string, index: number): string;
48
+ /** Matches any placeholder of this template, with the piece index in group 1. */
49
+ export declare function placeholderPattern(nonce: string): RegExp;
50
+ export declare function createNonce(random?: () => number): string;
51
+ export declare function toTemplateSource(xml: string, flavour: TextFlavour, nonce: string, part?: string): TemplateSource;
52
+ /** Restores the markup into rendered output and turns line breaks and tabs from data into elements. */
53
+ export declare function fromTemplateOutput(output: string, template: TemplateSource, part?: string): FilledPart;
54
+ /**
55
+ * Line breaks and tabs inside text elements become elements, and text elements with leading or
56
+ * trailing whitespace get `xml:space="preserve"`. Text of other elements (field codes, a table style
57
+ * id) is markup and stays as it is.
58
+ */
59
+ export declare function layoutText(xml: string, flavour: TextFlavour, part?: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A small XML tokenizer for office parts (spec 22 §4.3). Not a DOM: the rewrite has to keep every byte
3
+ * it does not change (a DOM round trip reorders namespace declarations and quoting), and the engine runs
4
+ * in browsers and Node without `DOMParser`. OOXML parts are machine-written XML without DTDs, which
5
+ * keeps this small; anything outside that shape is refused rather than guessed at.
6
+ */
7
+ export type XmlToken = {
8
+ kind: 'open';
9
+ raw: string;
10
+ name: string;
11
+ selfClosing: boolean;
12
+ } | {
13
+ kind: 'close';
14
+ raw: string;
15
+ name: string;
16
+ } | {
17
+ kind: 'text';
18
+ raw: string;
19
+ } | {
20
+ kind: 'cdata';
21
+ raw: string;
22
+ } | {
23
+ kind: 'comment';
24
+ raw: string;
25
+ } | {
26
+ kind: 'pi';
27
+ raw: string;
28
+ };
29
+ /** Splits XML into tokens whose `raw` strings concatenate back to the input exactly. */
30
+ export declare function tokenize(xml: string, part?: string): XmlToken[];
31
+ /**
32
+ * Throws unless the tokens form one well-formed element: balanced tags, one root, valid entity
33
+ * references in text and attributes, no characters XML 1.0 forbids.
34
+ */
35
+ export declare function assertWellFormed(tokens: readonly XmlToken[], part?: string): void;
36
+ /** Text content as characters: entity references resolved. */
37
+ export declare function decodeText(raw: string): string;
38
+ /** Characters as text content. `>` is escaped too, so `]]>` can never appear. */
39
+ export declare function escapeText(text: string): string;
40
+ /** Removes the characters XML 1.0 forbids; `removed` counts them for the render's warnings. */
41
+ export declare function stripForbidden(text: string): {
42
+ text: string;
43
+ removed: number;
44
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A small XML tokenizer for office parts (spec 22 §4.3). Not a DOM: the rewrite has to keep every byte
3
+ * it does not change (a DOM round trip reorders namespace declarations and quoting), and the engine runs
4
+ * in browsers and Node without `DOMParser`. OOXML parts are machine-written XML without DTDs, which
5
+ * keeps this small; anything outside that shape is refused rather than guessed at.
6
+ */
7
+ export type XmlToken = {
8
+ kind: 'open';
9
+ raw: string;
10
+ name: string;
11
+ selfClosing: boolean;
12
+ } | {
13
+ kind: 'close';
14
+ raw: string;
15
+ name: string;
16
+ } | {
17
+ kind: 'text';
18
+ raw: string;
19
+ } | {
20
+ kind: 'cdata';
21
+ raw: string;
22
+ } | {
23
+ kind: 'comment';
24
+ raw: string;
25
+ } | {
26
+ kind: 'pi';
27
+ raw: string;
28
+ };
29
+ /** Splits XML into tokens whose `raw` strings concatenate back to the input exactly. */
30
+ export declare function tokenize(xml: string, part?: string): XmlToken[];
31
+ /**
32
+ * Throws unless the tokens form one well-formed element: balanced tags, one root, valid entity
33
+ * references in text and attributes, no characters XML 1.0 forbids.
34
+ */
35
+ export declare function assertWellFormed(tokens: readonly XmlToken[], part?: string): void;
36
+ /** Text content as characters: entity references resolved. */
37
+ export declare function decodeText(raw: string): string;
38
+ /** Characters as text content. `>` is escaped too, so `]]>` can never appear. */
39
+ export declare function escapeText(text: string): string;
40
+ /** Removes the characters XML 1.0 forbids; `removed` counts them for the render's warnings. */
41
+ export declare function stripForbidden(text: string): {
42
+ text: string;
43
+ removed: number;
44
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A ZIP reader and writer for office containers (spec 22 §4.3 step 1). fflate's `unzipSync` inflates
3
+ * every entry, media included, and its `zipSync` compresses them all again; this reader parses the
4
+ * central directory itself, inflates only the parts a caller asks for (capped while inflating), and
5
+ * the writer copies every other entry as its original compressed bytes. Written for untrusted input:
6
+ * every offset is bounds-checked and every rule of §2.1 that concerns the container is applied here.
7
+ */
8
+ export interface ZipEntry {
9
+ readonly name: string;
10
+ /** 0 stored, 8 deflate. */
11
+ readonly method: number;
12
+ readonly crc32: number;
13
+ readonly compressedSize: number;
14
+ /** As declared; `read()` verifies it. */
15
+ readonly size: number;
16
+ readonly dosTime: number;
17
+ readonly dosDate: number;
18
+ /** The compressed bytes, a view into the archive. */
19
+ readonly raw: Uint8Array;
20
+ }
21
+ export interface ZipArchive {
22
+ readonly entries: readonly ZipEntry[];
23
+ get(name: string): ZipEntry | undefined;
24
+ }
25
+ export interface ZipLimits {
26
+ maxEntries: number;
27
+ maxDeclaredBytes: number;
28
+ }
29
+ export declare function isZip(bytes: Uint8Array): boolean;
30
+ /** Name rules: no absolute paths, `..`, backslashes, drive letters or control characters. */
31
+ export declare function isSafeEntryName(name: string): boolean;
32
+ export declare function readZip(bytes: Uint8Array, limits?: ZipLimits): ZipArchive;
33
+ /** The entry's bytes, inflated with a hard cap and checked against its CRC. */
34
+ export declare function readEntry(entry: ZipEntry, maxBytes?: number): Uint8Array;
35
+ export declare function readText(entry: ZipEntry, maxBytes?: number): string;
36
+ /** XML parts may be UTF-8 or UTF-16 (OOXML allows both); a BOM decides. */
37
+ export declare function decodeXml(bytes: Uint8Array): string;
38
+ /** One entry of a new archive: an entry copied as it is, or new bytes to deflate. */
39
+ export type ZipWriteEntry = ZipEntry | {
40
+ name: string;
41
+ data: Uint8Array;
42
+ store?: boolean;
43
+ };
44
+ /**
45
+ * Writes an archive in the given order. Copied entries keep their compressed bytes and timestamps;
46
+ * new entries get 1980-01-01 00:00, so equal inputs produce equal bytes (reproducible renders).
47
+ */
48
+ export declare function writeZip(entries: readonly ZipWriteEntry[]): Uint8Array;
49
+ export declare function crc32(data: Uint8Array): number;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A ZIP reader and writer for office containers (spec 22 §4.3 step 1). fflate's `unzipSync` inflates
3
+ * every entry, media included, and its `zipSync` compresses them all again; this reader parses the
4
+ * central directory itself, inflates only the parts a caller asks for (capped while inflating), and
5
+ * the writer copies every other entry as its original compressed bytes. Written for untrusted input:
6
+ * every offset is bounds-checked and every rule of §2.1 that concerns the container is applied here.
7
+ */
8
+ export interface ZipEntry {
9
+ readonly name: string;
10
+ /** 0 stored, 8 deflate. */
11
+ readonly method: number;
12
+ readonly crc32: number;
13
+ readonly compressedSize: number;
14
+ /** As declared; `read()` verifies it. */
15
+ readonly size: number;
16
+ readonly dosTime: number;
17
+ readonly dosDate: number;
18
+ /** The compressed bytes, a view into the archive. */
19
+ readonly raw: Uint8Array;
20
+ }
21
+ export interface ZipArchive {
22
+ readonly entries: readonly ZipEntry[];
23
+ get(name: string): ZipEntry | undefined;
24
+ }
25
+ export interface ZipLimits {
26
+ maxEntries: number;
27
+ maxDeclaredBytes: number;
28
+ }
29
+ export declare function isZip(bytes: Uint8Array): boolean;
30
+ /** Name rules: no absolute paths, `..`, backslashes, drive letters or control characters. */
31
+ export declare function isSafeEntryName(name: string): boolean;
32
+ export declare function readZip(bytes: Uint8Array, limits?: ZipLimits): ZipArchive;
33
+ /** The entry's bytes, inflated with a hard cap and checked against its CRC. */
34
+ export declare function readEntry(entry: ZipEntry, maxBytes?: number): Uint8Array;
35
+ export declare function readText(entry: ZipEntry, maxBytes?: number): string;
36
+ /** XML parts may be UTF-8 or UTF-16 (OOXML allows both); a BOM decides. */
37
+ export declare function decodeXml(bytes: Uint8Array): string;
38
+ /** One entry of a new archive: an entry copied as it is, or new bytes to deflate. */
39
+ export type ZipWriteEntry = ZipEntry | {
40
+ name: string;
41
+ data: Uint8Array;
42
+ store?: boolean;
43
+ };
44
+ /**
45
+ * Writes an archive in the given order. Copied entries keep their compressed bytes and timestamps;
46
+ * new entries get 1980-01-01 00:00, so equal inputs produce equal bytes (reproducible renders).
47
+ */
48
+ export declare function writeZip(entries: readonly ZipWriteEntry[]): Uint8Array;
49
+ export declare function crc32(data: Uint8Array): number;
@@ -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;