@asterdoc/render 0.0.0-stage → 0.9.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/LICENSE +25 -0
- package/README.md +264 -2
- package/dist/check-BA5xdYgi.d.ts +69 -0
- package/dist/chunk-4QVR6LUY.js +98 -0
- package/dist/chunk-ICQLU4FL.js +630 -0
- package/dist/chunk-PNYKTSIT.js +1267 -0
- package/dist/chunk-U7LF3GN4.js +34 -0
- package/dist/context-BmjDcc_j.d.ts +32 -0
- package/dist/docx.d.ts +37 -0
- package/dist/docx.js +38 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +69 -0
- package/dist/layer-bindings-CFXyN7SM.d.ts +94 -0
- package/dist/pdf.d.ts +620 -0
- package/dist/pdf.js +4638 -0
- package/dist/signatures-DPaL4gay.d.ts +20 -0
- package/dist/signatures-cS3V0yR3.d.ts +27 -0
- package/dist/text/field-formatter.d.ts +46 -0
- package/dist/text/field-formatter.js +21 -0
- package/dist/text.d.ts +44 -0
- package/dist/text.js +77 -0
- package/package.json +103 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paradoc AI, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
**Trademark Notice**: This license does not grant permission to use the trade names, trademarks, service marks, logos, taglines, or product names of Asterdoc, except as required for reasonable and customary use in describing the origin of the Software.
|
package/README.md
CHANGED
|
@@ -1,3 +1,265 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://asterdoc.com?utm_source=github&utm_medium=render" target="_blank" rel="noopener noreferrer">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://assets.asterdoc.com/asterdoc-logo-400x400-dark.png" type="image/png">
|
|
5
|
+
<img src="https://assets.asterdoc.com/asterdoc-logo-400x400-dark.png" height="64" alt="Asterdoc logo">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
<br />
|
|
9
|
+
</p>
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
<h1 align="center">@asterdoc/render</h1>
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
[](https://docs.asterdoc.com?utm_source=github&utm_medium=render)
|
|
16
|
+
[](https://x.com/asterdoc)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
Dependency-light text, PDF, and DOCX rendering for Asterdoc. It works in modern
|
|
21
|
+
browsers and on the server. Forms use the MIME-selected renderer automatically
|
|
22
|
+
and load only the selected layer engine.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const output = await filled.render({ layer: 'final' })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pass `createLayerRenderer()` explicitly when configuring the built-in renderer or
|
|
29
|
+
provide any compatible custom renderer as an override.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createLayerRenderer } from '@asterdoc/render'
|
|
33
|
+
|
|
34
|
+
const output = await filled.render({
|
|
35
|
+
renderer: createLayerRenderer({ serializers }),
|
|
36
|
+
layer: 'final'
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use a format-specific entry point for inspection, overlays, or other
|
|
41
|
+
format-specific operations so an application only bundles that implementation.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { renderText } from '@asterdoc/render/text'
|
|
45
|
+
import { renderPdf } from '@asterdoc/render/pdf'
|
|
46
|
+
import { renderDocx } from '@asterdoc/render/docx'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`createLayerRenderer()` supports `text/plain`, `text/markdown`, `text/html`,
|
|
50
|
+
`application/pdf`, and DOCX Office MIME types. It fails loudly for an unknown
|
|
51
|
+
MIME type rather than guessing.
|
|
52
|
+
|
|
53
|
+
## Text templates
|
|
54
|
+
|
|
55
|
+
Text templates keep the block markers `{{#if}}`, `{{#unless}}`, `{{else}}`, and
|
|
56
|
+
`{{#each}}`. Everything inside a marker is an artifact expression from
|
|
57
|
+
`@asterdoc/expr`, the language field logic uses, so a condition means the same in
|
|
58
|
+
a template as in a rule. Templates do not execute arbitrary JavaScript.
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
{{#if fields.approved and fields.total.amount > 1000}}
|
|
62
|
+
Approved for {{parties.owner.name}}: {{fields.total}}
|
|
63
|
+
{{else}}
|
|
64
|
+
Pending
|
|
65
|
+
{{/if}}
|
|
66
|
+
|
|
67
|
+
{{#each fields.items}}
|
|
68
|
+
- {{index(item) + 1}}. {{item.name}}{{#unless last(item)}},{{/unless}}
|
|
69
|
+
{{/each}}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Inside `{{#each}}`, `item` is the row and `parent` the enclosing row. A
|
|
73
|
+
condition must be boolean and a loop source a list. `checkTextTemplate()` and
|
|
74
|
+
`checkDocxTemplate()` check every expression against a type environment, and a
|
|
75
|
+
failed render throws `TemplateError` with the layer, position, and expression.
|
|
76
|
+
|
|
77
|
+
## PDF forms and overlays
|
|
78
|
+
|
|
79
|
+
A PDF render can fill AcroForm fields, add coordinate-based text or images, or
|
|
80
|
+
combine all three.
|
|
81
|
+
Overlay page numbers are one-based and coordinates use PDF points measured from
|
|
82
|
+
the bottom-left corner.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const output = await renderPdf({
|
|
86
|
+
template,
|
|
87
|
+
form,
|
|
88
|
+
data: { name: 'Ada Lovelace' },
|
|
89
|
+
bindings: { legal_name: 'name' },
|
|
90
|
+
overlays: [
|
|
91
|
+
{ page: 1, x: 48, y: 72, text: 'Prepared for' },
|
|
92
|
+
{ page: 1, x: 48, y: 54, field: 'name', fontSize: 11 },
|
|
93
|
+
{
|
|
94
|
+
page: 1,
|
|
95
|
+
x: 48,
|
|
96
|
+
y: 20,
|
|
97
|
+
width: 120,
|
|
98
|
+
height: 30,
|
|
99
|
+
image: signaturePng,
|
|
100
|
+
mediaType: 'image/png',
|
|
101
|
+
fit: 'contain',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`inspectAcroFormFields()` discovers fields without modifying a PDF.
|
|
108
|
+
`inspectPdf()` returns its page count and page dimensions. PNG overlays support
|
|
109
|
+
non-interlaced 8-bit grayscale, RGB, grayscale-alpha, and RGBA images; JPEG
|
|
110
|
+
overlays are also supported.
|
|
111
|
+
|
|
112
|
+
A filled field follows its own settings: declared font size or auto-size,
|
|
113
|
+
alignment, color, comb boxes, and multiline wrapping; a list box shows every
|
|
114
|
+
selected value. A value that does not fit shrinks to `MIN_FONT_SIZE` (6 points).
|
|
115
|
+
Past that, or with more characters than a comb field has boxes, the render throws
|
|
116
|
+
`PdfFieldFillError` with the field name, the `reason` (`overflow` or
|
|
117
|
+
`comb-length`), and the `limit`.
|
|
118
|
+
|
|
119
|
+
Each value, and each overlay text, uses the first font that can draw all of it:
|
|
120
|
+
the overlay's own `font` when it names one, then `font` (supplied at render
|
|
121
|
+
time), then `layerFont` (the font the artifact's PDF layer declares), then the
|
|
122
|
+
form's own embedded font for the field, then Helvetica for Latin-1 text. Fonts are TrueType programs given as `{ bytes, source }`, and
|
|
123
|
+
every font used is embedded. A character no font can draw fails with reason
|
|
124
|
+
`missing-glyph`; scripts that need shaping, such as Arabic or Devanagari, fail
|
|
125
|
+
with `unsupported-script` whatever font is available. A font that cannot be read,
|
|
126
|
+
is not TrueType, or forbids embedding fails with `PdfFontError` naming its
|
|
127
|
+
`source`.
|
|
128
|
+
|
|
129
|
+
`pdfFontMetrics(font)` measures text in a supplied font before you draw it:
|
|
130
|
+
`canDraw(text)` says whether overlays can draw every character, and
|
|
131
|
+
`width(text, size)` gives its width in points, so you can wrap overlay text
|
|
132
|
+
yourself.
|
|
133
|
+
|
|
134
|
+
## Reading filled forms back
|
|
135
|
+
|
|
136
|
+
`extractPdfData()` reads a filled PDF's AcroForm fields back through a layer's
|
|
137
|
+
bindings into artifact data, with a report for each binding target. Combined
|
|
138
|
+
bindings are reported as not recoverable rather than split. Most callers use
|
|
139
|
+
`form.extract()` from `@asterdoc/core`, which also chooses the PDF layer.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const { data, report } = await extractPdfData({ pdf: filled, form, bindings })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Merging PDFs
|
|
146
|
+
|
|
147
|
+
`mergePdfs()` concatenates PDFs into one document, keeping every page in the
|
|
148
|
+
order given at its own size.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const packet = await mergePdfs([purchaseOrder, filledW9, certificate])
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
It writes a fresh document rather than an incremental update, because two
|
|
155
|
+
sources number their objects from one and an incremental update cannot hold
|
|
156
|
+
both. Every object a kept page can reach is copied with a new number and every
|
|
157
|
+
reference inside it rewritten; stream bytes are copied exactly as stored, so
|
|
158
|
+
nothing is re-encoded. The merge is deterministic: the same sources produce the
|
|
159
|
+
same bytes.
|
|
160
|
+
|
|
161
|
+
**What a merge drops.** Everything the old catalog held rather than the page:
|
|
162
|
+
|
|
163
|
+
| Dropped | Consequence |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `AcroForm` | Field state is gone. Flatten a filled form with `flattenPdf()` first and its values arrive as page content. |
|
|
166
|
+
| `StructTreeRoot` and the role map | The pages are untagged. A copied page keeps its `StructParents` key, which then dangles; readers ignore it, screen readers lose the structure. |
|
|
167
|
+
| `Names` and `Dests` | A named destination no longer resolves, so a link that used one goes nowhere. |
|
|
168
|
+
| `OCProperties` | A page drawn with optional content loses the configuration deciding which layers are visible, and every layer paints. |
|
|
169
|
+
| `Lang` | The document declares no language. |
|
|
170
|
+
| `ViewerPreferences`, `PageMode`, `PageLabels` | Presentation hints are gone. |
|
|
171
|
+
| `Outlines` and `Metadata` | No bookmarks, no document metadata. |
|
|
172
|
+
|
|
173
|
+
None of those survive a print either, which is what makes the trade acceptable
|
|
174
|
+
for a packet assembled from documents that are already final. A caller who needs
|
|
175
|
+
any of them should say so rather than merge.
|
|
176
|
+
|
|
177
|
+
A single source is returned unchanged. Every failure names its source:
|
|
178
|
+
`PdfMergeError` carries the zero-based `source` index for a document with no
|
|
179
|
+
catalog, no pages, or bytes the parser cannot read at all.
|
|
180
|
+
|
|
181
|
+
An encrypted source throws `PdfEncryptedError`, as it does from every function
|
|
182
|
+
that reads or writes a PDF (`renderPdf`, `flattenPdf`, `selectPdfPages`,
|
|
183
|
+
`inspectPdf`, `extractPdfData`, and the rest). One check, in the parser every
|
|
184
|
+
entry point shares, decides: a trailer or cross-reference stream that declares
|
|
185
|
+
`/Encrypt`.
|
|
186
|
+
|
|
187
|
+
## Digital signature fields
|
|
188
|
+
|
|
189
|
+
`preparePdfSignature()` appends an incremental update that adds an invisible
|
|
190
|
+
signature field, an AcroForm with `/SigFlags 3`, and a signature dictionary
|
|
191
|
+
with a reserved `/Contents`. Sign `signedBytes(prepared)` with your own keys and
|
|
192
|
+
write the container with `embedPdfSignature()`. `readPdfSignature()` reads the
|
|
193
|
+
newest signature dictionary back, and its `coversWholeFile` is false when any
|
|
194
|
+
byte outside the container is unsigned.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const prepared = await preparePdfSignature(pdf, { contentsSize: 16384, signingTime: new Date(), name: 'Ada Lovelace' })
|
|
198
|
+
const signed = embedPdfSignature(prepared, createCmsSignature(signedBytes(prepared)))
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Signature placement
|
|
202
|
+
|
|
203
|
+
The `@asterdoc/render/pdf` subpath locates signature positions in converter-
|
|
204
|
+
produced PDFs without a PDF rendering engine. It reads the PDF text layer with
|
|
205
|
+
a built-in content-stream scanner, so it runs anywhere the renderer runs.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { locate, extractFieldsFromPdf, encode, FieldType } from '@asterdoc/render/pdf'
|
|
209
|
+
|
|
210
|
+
// Find invisible markers injected during rendering, and literal anchor text.
|
|
211
|
+
const hits = await locate(pdf, [
|
|
212
|
+
{ id: 'client-sig', kind: 'marker', signerIndex: 0, fieldType: FieldType.SIGNATURE },
|
|
213
|
+
{ id: 'witness-sig', kind: 'anchor', text: 'Witnessed by:' },
|
|
214
|
+
])
|
|
215
|
+
// [{ id, page, x, y, width, height }] in PDF points, y from the top edge
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`locate()` is all-or-nothing: an unresolved or ambiguous query throws a
|
|
219
|
+
`LocateError` naming every failed id, so a seal pipeline can never proceed on
|
|
220
|
+
a silently incomplete signature map. Anchor text must be unique unless the
|
|
221
|
+
query picks an `occurrence`, a one-based integer; any other `occurrence` throws a
|
|
222
|
+
`RangeError`. `extractFieldsFromPdf()` returns every marker in
|
|
223
|
+
a document, and throws `UnknownMarkerError` naming each marker whose field type
|
|
224
|
+
is not signature or initials; `pageTextRuns()` exposes positioned text runs for verification and
|
|
225
|
+
layout tooling.
|
|
226
|
+
|
|
227
|
+
## DOCX templates
|
|
228
|
+
|
|
229
|
+
DOCX rendering supports direct values, commands split across Word runs, custom
|
|
230
|
+
delimiters, line breaks, and structural commands:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
{{FOR line IN fields.items}}
|
|
234
|
+
{{line.name}}
|
|
235
|
+
{{END-FOR line}}
|
|
236
|
+
|
|
237
|
+
{{IF fields.approved}}
|
|
238
|
+
Approved
|
|
239
|
+
{{ELSE}}
|
|
240
|
+
Pending
|
|
241
|
+
{{END-IF}}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Conditions, loop sources, and placeholders are artifact expressions, as in text
|
|
245
|
+
templates. DOCX templates also support the signing directives `signature`,
|
|
246
|
+
`initials`, `signatureDate`, `capacity`, and `printedName`, written
|
|
247
|
+
`{{signature(parties.client, "client-sign")}}`.
|
|
248
|
+
|
|
249
|
+
`capacity` prints the signatory's capacity and `printedName` the signer's name.
|
|
250
|
+
After a `capacity` or `printed_name` capture at that location, each prints the
|
|
251
|
+
captured text. When no value is known, they fall back to a placeholder:
|
|
252
|
+
`[CAPACITY]` and `[PRINTED NAME]` in text templates, underscores in DOCX
|
|
253
|
+
templates. The `placeholder` and `captured` signature options replace the
|
|
254
|
+
defaults, but a known capacity or name wins over `placeholder`.
|
|
255
|
+
|
|
256
|
+
## Benchmarking
|
|
257
|
+
|
|
258
|
+
From a repository checkout, run `pnpm benchmark` from this package to record
|
|
259
|
+
current text, PDF, and DOCX timings. The benchmark is
|
|
260
|
+
intentionally separate from the unit-test gate, so timing variance does not
|
|
261
|
+
make ordinary tests flaky. Set
|
|
262
|
+
`ASTERDOC_BENCHMARK_ITERATIONS` to change the number of measured iterations.
|
|
263
|
+
Set `ASTERDOC_BINARY_BENCHMARK_ITERATIONS` for PDF and DOCX iterations. Use
|
|
264
|
+
`pnpm size-report` to verify browser bundles, server imports, and the exact npm
|
|
265
|
+
tarball contents and sizes without publishing.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TypeEnv } from '@asterdoc/expr';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Template diagnostics: where an expression sits in a layer, and what replaces
|
|
5
|
+
* syntax the single expression language no longer accepts.
|
|
6
|
+
*/
|
|
7
|
+
/** A position inside a text template (1-based line and column). */
|
|
8
|
+
interface TemplatePosition {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}
|
|
12
|
+
/** One problem found in a template, at authoring validation or render. */
|
|
13
|
+
interface TemplateDiagnostic {
|
|
14
|
+
/** Stable category: `syntax`, `removed-syntax`, `markers`, a checker code, or an evaluation code. */
|
|
15
|
+
code: string;
|
|
16
|
+
message: string;
|
|
17
|
+
/** The expression source, when the problem is inside one. */
|
|
18
|
+
expression?: string;
|
|
19
|
+
/** Line and column in a text template, or in the paragraph of a DOCX part. */
|
|
20
|
+
position?: TemplatePosition;
|
|
21
|
+
/** The DOCX part and paragraph, such as `word/document.xml paragraph 12`. */
|
|
22
|
+
location?: string;
|
|
23
|
+
}
|
|
24
|
+
/** A template that cannot render: malformed markers, a failed expression, or removed syntax. */
|
|
25
|
+
declare class TemplateError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly layer?: string;
|
|
28
|
+
readonly expression?: string;
|
|
29
|
+
readonly position?: TemplatePosition;
|
|
30
|
+
readonly location?: string;
|
|
31
|
+
readonly diagnostic: TemplateDiagnostic;
|
|
32
|
+
constructor(diagnostic: TemplateDiagnostic, layer?: string);
|
|
33
|
+
/** The same error, naming the layer it came from. */
|
|
34
|
+
inLayer(layer: string | undefined): TemplateError;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authoring checks for template expressions.
|
|
39
|
+
*
|
|
40
|
+
* Every condition, loop source, placeholder, and directive argument is parsed
|
|
41
|
+
* and type-checked against the artifact's type environment, the one its field
|
|
42
|
+
* logic is checked against. A condition must be boolean and a loop source a
|
|
43
|
+
* list; `item`, `parent`, a DOCX loop's name, and the loop-only functions
|
|
44
|
+
* resolve only inside a loop.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** Check every expression in a text, Markdown, or HTML template. */
|
|
48
|
+
declare function checkTextTemplate(template: string, env: TypeEnv): TemplateDiagnostic[];
|
|
49
|
+
interface DocxCheckOptions {
|
|
50
|
+
/** Command delimiters, when the template does not use `{{` and `}}`. */
|
|
51
|
+
cmdDelimiter?: [string, string];
|
|
52
|
+
}
|
|
53
|
+
/** Check every control command and expression in a Word template. */
|
|
54
|
+
declare function checkDocxTemplate(template: Uint8Array, env: TypeEnv, options?: DocxCheckOptions): TemplateDiagnostic[];
|
|
55
|
+
/** A signing directive a template writes, such as `signature(parties.tenant, "tenant-sign")`. */
|
|
56
|
+
interface SigningDirectiveUse {
|
|
57
|
+
/** The directive name: `signature`, `initials`, `signatureDate`, `capacity`, or `printedName`. */
|
|
58
|
+
directive: string;
|
|
59
|
+
/** The location the directive places, when written as a string literal; undefined when computed. */
|
|
60
|
+
location?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The signing directives a text, Markdown, or HTML template writes, including
|
|
64
|
+
* those inside blocks. Undefined when the template's markers do not parse, so
|
|
65
|
+
* which directives it writes is unknown.
|
|
66
|
+
*/
|
|
67
|
+
declare function textTemplateSigningDirectives(template: string): SigningDirectiveUse[] | undefined;
|
|
68
|
+
|
|
69
|
+
export { type DocxCheckOptions as D, type SigningDirectiveUse as S, type TemplateDiagnostic as T, TemplateError as a, type TemplatePosition as b, checkDocxTemplate as c, checkTextTemplate as d, textTemplateSigningDirectives as t };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/layer-bindings.ts
|
|
2
|
+
function isPdfLayer(layer) {
|
|
3
|
+
return layer.mimeType?.toLowerCase() === "application/pdf";
|
|
4
|
+
}
|
|
5
|
+
function resolveLayerBindings(layers, layer) {
|
|
6
|
+
if (!isPdfLayer(layer)) return void 0;
|
|
7
|
+
if (layer.bindings || !layer.bindingsFrom) return layer.bindings;
|
|
8
|
+
const source = layers[layer.bindingsFrom];
|
|
9
|
+
if (!source) {
|
|
10
|
+
throw new Error(`bindingsFrom "${layer.bindingsFrom}" references unknown layer. Available: ${Object.keys(layers).join(", ")}`);
|
|
11
|
+
}
|
|
12
|
+
return isPdfLayer(source) ? source.bindings : void 0;
|
|
13
|
+
}
|
|
14
|
+
var PdfBindingSyntaxError = class extends Error {
|
|
15
|
+
binding;
|
|
16
|
+
constructor(binding, problem) {
|
|
17
|
+
super(`Binding ${JSON.stringify(binding)} ${problem}.`);
|
|
18
|
+
this.name = "PdfBindingSyntaxError";
|
|
19
|
+
this.binding = binding;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function parseBinding(binding) {
|
|
23
|
+
const parts = binding.split(",").map((text) => {
|
|
24
|
+
const separator = text.indexOf(":");
|
|
25
|
+
const source = (separator === -1 ? text : text.slice(0, separator)).trim();
|
|
26
|
+
if (!source) throw new PdfBindingSyntaxError(binding, "has an empty path");
|
|
27
|
+
if (separator === -1) return { source, path: bindingDataPath(source) };
|
|
28
|
+
const qualifier = text.slice(separator + 1).trim();
|
|
29
|
+
if (!qualifier) throw new PdfBindingSyntaxError(binding, `has an empty qualifier after "${source}:"`);
|
|
30
|
+
return { source, path: bindingDataPath(source), qualifier };
|
|
31
|
+
});
|
|
32
|
+
if (parts.length > 1 && parts.some((part) => part.qualifier !== void 0)) {
|
|
33
|
+
throw new PdfBindingSyntaxError(binding, "qualifies a part of a joined binding; a joined binding reads whole values");
|
|
34
|
+
}
|
|
35
|
+
return parts;
|
|
36
|
+
}
|
|
37
|
+
function parseBindings(bindings) {
|
|
38
|
+
return Object.entries(bindings).map(([pdfName, binding]) => [pdfName, parseBinding(binding)]);
|
|
39
|
+
}
|
|
40
|
+
function splitPartIndex(qualifier) {
|
|
41
|
+
return /^[1-9]\d*$/.test(qualifier) ? Number(qualifier) - 1 : void 0;
|
|
42
|
+
}
|
|
43
|
+
var PdfBindingKeyError = class extends Error {
|
|
44
|
+
/** The binding keys the template has no field for. */
|
|
45
|
+
keys;
|
|
46
|
+
constructor(keys, templateFields) {
|
|
47
|
+
super(
|
|
48
|
+
`PDF bindings name form fields the template does not have: ${keys.map((key) => JSON.stringify(key)).join(", ")}. ` + (templateFields.length > 0 ? `Template fields: ${templateFields.join(", ")}` : "The template has no form fields")
|
|
49
|
+
);
|
|
50
|
+
this.name = "PdfBindingKeyError";
|
|
51
|
+
this.keys = keys;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function bindingDataPath(path) {
|
|
55
|
+
const trimmed = path.trim();
|
|
56
|
+
return trimmed.startsWith("fields.") ? trimmed.slice("fields.".length) : trimmed;
|
|
57
|
+
}
|
|
58
|
+
function renderDataPath(data, path) {
|
|
59
|
+
const trimmed = path.trim();
|
|
60
|
+
const hasFieldsRoot = data.fields !== null && typeof data.fields === "object" && !Array.isArray(data.fields);
|
|
61
|
+
if (!hasFieldsRoot) return bindingDataPath(trimmed);
|
|
62
|
+
if (trimmed.startsWith("fields.")) return trimmed;
|
|
63
|
+
const root = trimmed.split(".", 1)[0];
|
|
64
|
+
return root === "parties" || root === "annexes" || root === "defs" || root === "_signers" || root === "_captures" ? trimmed : `fields.${trimmed}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/path.ts
|
|
68
|
+
var blockedSegments = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
69
|
+
function pathSegments(path) {
|
|
70
|
+
const normalized = path.replace(/\[(\d+)\]/g, ".$1");
|
|
71
|
+
if (normalized.includes("[") || normalized.includes("]")) return [];
|
|
72
|
+
const segments = normalized.split(".");
|
|
73
|
+
return segments.some((segment) => segment.length === 0) ? [] : segments;
|
|
74
|
+
}
|
|
75
|
+
function getPath(value, path) {
|
|
76
|
+
const segments = pathSegments(path);
|
|
77
|
+
if (segments.length === 0) return void 0;
|
|
78
|
+
let current = value;
|
|
79
|
+
for (const segment of segments) {
|
|
80
|
+
if (blockedSegments.has(segment) || current === null || current === void 0) return void 0;
|
|
81
|
+
if (typeof current !== "object" && typeof current !== "function") return void 0;
|
|
82
|
+
current = current[segment];
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
resolveLayerBindings,
|
|
89
|
+
PdfBindingSyntaxError,
|
|
90
|
+
parseBinding,
|
|
91
|
+
parseBindings,
|
|
92
|
+
splitPartIndex,
|
|
93
|
+
PdfBindingKeyError,
|
|
94
|
+
bindingDataPath,
|
|
95
|
+
renderDataPath,
|
|
96
|
+
pathSegments,
|
|
97
|
+
getPath
|
|
98
|
+
};
|