@legalplace/prerenderx 3.8.17 → 3.8.18-staging.2

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.
@@ -1,8 +1,11 @@
1
1
  import type { ModelV3 } from "@legalplace/models-v3-types";
2
- import type { Types } from "@legalplace/referencesparser";
3
- import { ReferencesParser } from "@legalplace/referencesparser";
4
2
  import ConditionsRunner from "@legalplace/conditions-runner";
5
3
  import OvcConverter from "@legalplace/ovc-converter";
4
+ import {
5
+ ReferencesParser,
6
+ type OutputRegionRole,
7
+ type Types,
8
+ } from "@legalplace/referencesparser";
6
9
  import type OvcType from "@legalplace/types/dist/ovc";
7
10
  import type InputsType from "@legalplace/types/dist/inputs";
8
11
  import crypto from "crypto";
@@ -16,6 +19,7 @@ import type {
16
19
  DocumentIndex,
17
20
  DocumentOutputIndex,
18
21
  DocumentPdfFormIndex,
22
+ DocumentLayout,
19
23
  } from "../types/types";
20
24
 
21
25
  class Prerender {
@@ -88,67 +92,178 @@ class Prerender {
88
92
  }
89
93
 
90
94
  const index = this.documentsIndex[documentNameHash];
95
+ const occurrenceIndex = index.occurrenceIndex ?? 0;
91
96
 
92
97
  this.renderedDocuments[documentNameHash] = Prerender.isPdfForm(index)
93
- ? this.pdfFiller.fillDocument(index.slug)
94
- : this.documentRender.renderDocument(index.slug);
98
+ ? this.pdfFiller.fillDocument(index.slug, occurrenceIndex)
99
+ : this.documentRender.renderDocument(index.slug, occurrenceIndex);
95
100
 
96
101
  return this.renderedDocuments[documentNameHash];
97
102
  }
98
103
 
104
+ /**
105
+ * Returns rendered header/footer regions and page layout settings for DOCX.
106
+ */
107
+ public getDocumentLayout(documentNameHash: string): DocumentLayout {
108
+ const index = this.documentsIndex[documentNameHash];
109
+ const occurrenceIndex = index.occurrenceIndex ?? 0;
110
+ const document = this.references.documents[index.slug];
111
+ const layoutParams = document.params?.layout;
112
+
113
+ const renderRegion = (role: OutputRegionRole): string | undefined => {
114
+ const html = this.documentRender.renderRegion(
115
+ index.slug,
116
+ role,
117
+ occurrenceIndex
118
+ );
119
+ return html.trim().length > 0 ? html : undefined;
120
+ };
121
+
122
+ const header = renderRegion("header");
123
+ const footer = renderRegion("footer");
124
+ const differentFirstPage = layoutParams?.differentFirstPage === true;
125
+
126
+ return {
127
+ header,
128
+ footer,
129
+ firstPageHeader: differentFirstPage
130
+ ? renderRegion("firstPageHeader")
131
+ : undefined,
132
+ firstPageFooter: differentFirstPage
133
+ ? renderRegion("firstPageFooter")
134
+ : undefined,
135
+ differentFirstPage,
136
+ margins: layoutParams?.margins,
137
+ headerDistance: layoutParams?.headerDistance,
138
+ footerDistance: layoutParams?.footerDistance,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Resolves a document occurrence's display name. Interpolates `[var:X]`
144
+ * in `document.name` at the linked occurrence. Optional `filenameTemplate`
145
+ * overrides the title when set and non-empty after interpolation.
146
+ */
147
+ private resolveDocumentOccurrenceName(
148
+ document: Types.ReferencesDocumentType,
149
+ docMultiple: NonNullable<
150
+ NonNullable<Types.ReferencesDocumentType["params"]>["multiple"]
151
+ >,
152
+ occurrenceIndex: number
153
+ ): string {
154
+ const baseName = parseDocumentNameWithVariables(
155
+ document.name,
156
+ this.ovc,
157
+ this.references,
158
+ document.fallbackName,
159
+ occurrenceIndex,
160
+ docMultiple.repeatOption
161
+ );
162
+
163
+ const template = docMultiple.filenameTemplate;
164
+ if (typeof template !== "string" || template.trim().length === 0) {
165
+ return baseName;
166
+ }
167
+
168
+ const resolved = parseDocumentNameWithVariables(
169
+ template,
170
+ this.ovc,
171
+ this.references,
172
+ undefined,
173
+ occurrenceIndex,
174
+ docMultiple.repeatOption
175
+ );
176
+
177
+ if (resolved.trim().length === 0) return baseName;
178
+ return resolved;
179
+ }
180
+
181
+ /**
182
+ * Returns the array of occurrences (true = render this occurrence) for a
183
+ * given document slug. For non-multiple documents, returns `[true]`.
184
+ * For multiple documents, returns the parallel array of the source option
185
+ * (`params.multiple.repeatOption`), defaulting to `[true]` if absent.
186
+ */
187
+ private getDocumentOccurrences(slug: string): boolean[] {
188
+ const document = this.references.documents[slug];
189
+ const docMultiple = document.params?.multiple;
190
+ if (docMultiple?.enabled !== true) return [true];
191
+
192
+ const { repeatOption } = docMultiple;
193
+ if (typeof repeatOption !== "number") return [true];
194
+
195
+ const inputs = this.ovc.options[String(repeatOption)];
196
+ if (!Array.isArray(inputs) || inputs.length === 0) return [true];
197
+ return inputs;
198
+ }
199
+
99
200
  private generateDocumentNamesHash() {
100
201
  Object.keys(this.references.documents).forEach((slug) => {
101
202
  const document = this.references.documents[slug];
102
- const documentParams = this.references.documents[slug].params;
103
- const conditionObject = this.references.conditions.documents[slug];
104
- const condition =
105
- conditionObject === undefined
106
- ? true
107
- : this.conditions.executeCondition(
108
- conditionObject,
109
- 0,
110
- 0,
111
- "documents"
112
- );
113
-
114
- // Rendering documents sections
115
- if (condition === true) {
116
- const hash = crypto.createHash("sha1").update(slug).digest("hex");
117
-
118
- // Referencing in index
203
+ const documentParams = document.params;
204
+ const docMultiple = documentParams?.multiple;
205
+ const isMultipleDocument = docMultiple?.enabled === true;
206
+
207
+ const occurrences = this.getDocumentOccurrences(slug);
208
+
209
+ occurrences.forEach((occurrenceValue, occurrenceIndex) => {
210
+ if (occurrenceValue !== true) return;
211
+
212
+ const conditionObject = this.references.conditions.documents[slug];
213
+ const condition =
214
+ conditionObject === undefined
215
+ ? true
216
+ : this.conditions.executeCondition(
217
+ conditionObject,
218
+ 0,
219
+ occurrenceIndex,
220
+ "documents"
221
+ );
222
+
223
+ if (condition !== true) return;
224
+
225
+ const seed = isMultipleDocument ? `${slug}:${occurrenceIndex}` : slug;
226
+ const hash = crypto.createHash("sha1").update(seed).digest("hex");
227
+
228
+ const name =
229
+ isMultipleDocument && docMultiple
230
+ ? this.resolveDocumentOccurrenceName(
231
+ document,
232
+ docMultiple,
233
+ occurrenceIndex
234
+ )
235
+ : parseDocumentNameWithVariables(
236
+ document.name,
237
+ this.ovc,
238
+ this.references,
239
+ document.fallbackName
240
+ );
241
+
119
242
  if (
120
243
  typeof document.pdf === "object" &&
121
244
  typeof document.pdf.id === "string" &&
122
245
  typeof document.pdf.form === "object"
123
246
  ) {
124
247
  this.documentsIndex[hash] = {
125
- name: parseDocumentNameWithVariables(
126
- document.name,
127
- this.ovc,
128
- this.references,
129
- document.fallbackName
130
- ),
248
+ name,
131
249
  slug,
132
250
  form: true,
133
251
  fdf: {
134
252
  id: "",
135
253
  fields: {},
136
254
  },
255
+ ...(isMultipleDocument ? { occurrenceIndex } : {}),
137
256
  };
138
257
  } else {
139
258
  this.documentsIndex[hash] = {
140
- name: parseDocumentNameWithVariables(
141
- document.name,
142
- this.ovc,
143
- this.references,
144
- document.fallbackName
145
- ),
259
+ name,
146
260
  slug,
147
261
  pdf: documentParams?.formats?.pdf !== false,
148
262
  docx: documentParams?.formats?.docx !== false,
263
+ ...(isMultipleDocument ? { occurrenceIndex } : {}),
149
264
  };
150
265
  }
151
- }
266
+ });
152
267
  });
153
268
  }
154
269
 
@@ -164,15 +279,15 @@ class Prerender {
164
279
  private generateAllDocuments() {
165
280
  Object.keys(this.documentsIndex).forEach((documentNameHash) => {
166
281
  const index = this.documentsIndex[documentNameHash];
282
+ const occurrenceIndex = index.occurrenceIndex ?? 0;
167
283
  if (Prerender.isPdfForm(index)) {
168
284
  this.renderedDocuments[documentNameHash] = this.pdfFiller.fillDocument(
169
- this.documentsIndex[documentNameHash].slug
285
+ index.slug,
286
+ occurrenceIndex
170
287
  );
171
288
  } else {
172
289
  this.renderedDocuments[documentNameHash] =
173
- this.documentRender.renderDocument(
174
- this.documentsIndex[documentNameHash].slug
175
- );
290
+ this.documentRender.renderDocument(index.slug, occurrenceIndex);
176
291
  }
177
292
  });
178
293
  }
@@ -17,6 +17,13 @@ class SectionRender {
17
17
 
18
18
  ovc: InputsType;
19
19
 
20
+ /**
21
+ * Occurrence index injected by the parent DocumentRender when the document
22
+ * is multiple (`params.multiple.enabled === true`). For non-multiple
23
+ * documents this is always 0.
24
+ */
25
+ baseIndex: number;
26
+
20
27
  private NumAuto: NumAuto;
21
28
 
22
29
  constructor(options: {
@@ -26,6 +33,7 @@ class SectionRender {
26
33
  documentName: string;
27
34
  currentSection: number;
28
35
  numauto: NumAuto;
36
+ baseIndex?: number;
29
37
  }) {
30
38
  this.references = options.references;
31
39
  this.conditions = options.conditions;
@@ -33,6 +41,7 @@ class SectionRender {
33
41
  this.currentSection = options.currentSection;
34
42
  this.ovc = options.ovc;
35
43
  this.NumAuto = options.numauto;
44
+ this.baseIndex = options.baseIndex ?? 0;
36
45
 
37
46
  this.renderSection();
38
47
  }
@@ -41,6 +50,19 @@ class SectionRender {
41
50
  return this.outputs;
42
51
  }
43
52
 
53
+ /**
54
+ * Returns the multiple source option id for the current document if any.
55
+ * Returns `undefined` for non-multiple documents.
56
+ */
57
+ private getDocumentMultipleSource(): number | undefined {
58
+ const docMultiple =
59
+ this.references.documents[this.documentName]?.params?.multiple;
60
+ if (docMultiple?.enabled !== true) return undefined;
61
+ return typeof docMultiple.repeatOption === "number"
62
+ ? docMultiple.repeatOption
63
+ : undefined;
64
+ }
65
+
44
66
  private renderSection() {
45
67
  const section =
46
68
  this.references.sections[this.documentName][this.currentSection];
@@ -51,45 +73,106 @@ class SectionRender {
51
73
  const condition =
52
74
  conditionObject === undefined
53
75
  ? true
54
- : this.conditions.executeCondition(conditionObject, 0, 0, "sections");
55
-
56
- // Rendering documents sections
57
- if (condition === true) {
58
- section.options.forEach((optionId) => {
59
- const option = this.references.options[optionId];
60
- let repeatOption = optionId;
61
- if (option.meta.type === "repeated") {
62
- if (option.meta.repeatOption === undefined) return;
63
- repeatOption =
64
- typeof option.meta.repeatOption === "number"
65
- ? option.meta.repeatOption
66
- : parseInt(option.meta.repeatOption, 10);
67
- }
68
-
69
- // Option inputs
70
- const inputs = this.ovc.options[repeatOption] || [
71
- !["radio", "checkbox"].includes(option.meta.type),
72
- ]; // Falling back on a single value array if option doesn't exist in ovc
73
-
74
- // Looping through inputs
75
- inputs.forEach((value, index) => {
76
- // If value is false we stop here
77
- if (value !== true) return;
78
-
76
+ : this.conditions.executeCondition(
77
+ conditionObject,
78
+ 0,
79
+ this.baseIndex,
80
+ "sections"
81
+ );
82
+
83
+ if (condition !== true) return;
84
+
85
+ const documentMultipleSource = this.getDocumentMultipleSource();
86
+
87
+ section.options.forEach((optionId) => {
88
+ const option = this.references.options[optionId];
89
+
90
+ // Detect a `repeated` wrapper that points to the same source as the
91
+ // document multiple. In that case, iterating over `repeatOption` here
92
+ // would double the loop (the document is already rendered N times by
93
+ // Prerender). Instead, render the wrapper's children once at the
94
+ // document's current occurrence index.
95
+ if (
96
+ option.meta.type === "repeated" &&
97
+ documentMultipleSource !== undefined &&
98
+ option.meta.repeatOption === documentMultipleSource
99
+ ) {
100
+ option.options.forEach((childId) => {
79
101
  this.outputs = [
80
102
  ...this.outputs,
81
103
  ...new OptionRender({
82
104
  references: this.references,
83
- id: optionId,
105
+ id: childId,
84
106
  ovc: this.ovc,
85
- index,
107
+ index: this.baseIndex,
86
108
  conditions: this.conditions,
87
109
  numauto: this.NumAuto,
88
110
  }).getOutputs(),
89
111
  ];
90
112
  });
113
+ return;
114
+ }
115
+
116
+ let repeatOption = optionId;
117
+ if (option.meta.type === "repeated") {
118
+ if (option.meta.repeatOption === undefined) return;
119
+ repeatOption =
120
+ typeof option.meta.repeatOption === "number"
121
+ ? option.meta.repeatOption
122
+ : parseInt(option.meta.repeatOption, 10);
123
+ }
124
+
125
+ // Option inputs
126
+ const inputs = this.ovc.options[repeatOption] || [
127
+ !["radio", "checkbox"].includes(option.meta.type),
128
+ ]; // Falling back on a single value array if option doesn't exist in ovc
129
+
130
+ // Inside a multiple document, every non-`repeated` root option is
131
+ // rendered at the document's occurrence index instead of looping
132
+ // through every input slot. We pick the value at `baseIndex` (or
133
+ // fall back to slot 0 for non-multiplied options whose inputs are
134
+ // a single-element array).
135
+ if (
136
+ documentMultipleSource !== undefined &&
137
+ option.meta.type !== "repeated"
138
+ ) {
139
+ const valueAtIndex =
140
+ this.baseIndex < inputs.length ? inputs[this.baseIndex] : inputs[0];
141
+ if (valueAtIndex !== true) return;
142
+
143
+ this.outputs = [
144
+ ...this.outputs,
145
+ ...new OptionRender({
146
+ references: this.references,
147
+ id: optionId,
148
+ ovc: this.ovc,
149
+ index: this.baseIndex,
150
+ conditions: this.conditions,
151
+ numauto: this.NumAuto,
152
+ }).getOutputs(),
153
+ ];
154
+ return;
155
+ }
156
+
157
+ // Looping through inputs (default behaviour, also used for `repeated`
158
+ // wrappers whose source differs from the document multiple source).
159
+ inputs.forEach((value, index) => {
160
+ // If value is false we stop here
161
+ if (value !== true) return;
162
+
163
+ this.outputs = [
164
+ ...this.outputs,
165
+ ...new OptionRender({
166
+ references: this.references,
167
+ id: optionId,
168
+ ovc: this.ovc,
169
+ index,
170
+ conditions: this.conditions,
171
+ numauto: this.NumAuto,
172
+ }).getOutputs(),
173
+ ];
91
174
  });
92
- }
175
+ });
93
176
  }
94
177
  }
95
178
 
@@ -1,6 +1,11 @@
1
1
  import type { Types } from "@legalplace/referencesparser";
2
2
  import type ConditionsRunner from "@legalplace/conditions-runner";
3
3
  import type InputsType from "@legalplace/types/dist/inputs";
4
+ import {
5
+ isOptionDisplayed as sharedIsOptionDisplayed,
6
+ isVariableDisplayed as sharedIsVariableDisplayed,
7
+ type ConditionEvaluator,
8
+ } from "@legalplace/referencesparser";
4
9
 
5
10
  class FillPdfFormBase {
6
11
  references: Types.ReferencesType;
@@ -20,59 +25,46 @@ class FillPdfFormBase {
20
25
  }
21
26
 
22
27
  /**
23
- * Checks whether a variable is displayed at a given index
24
- * @param id Variable's ID
25
- * @param index Index
28
+ * Evaluates a stored condition for options, variables or sections.
29
+ */
30
+ private readonly conditionEvaluator: ConditionEvaluator = (
31
+ type,
32
+ id,
33
+ index
34
+ ) => {
35
+ if (type === "options") return this.getOptionCondition(id, index);
36
+ if (type === "variables") return this.getVariableCondition(id, index);
37
+ return this.getSectionCondition(id);
38
+ };
39
+
40
+ /**
41
+ * Checks whether a variable is displayed at a given index.
26
42
  */
27
43
  isVariableDisplayed(id: number, index: number) {
28
- // Getting variable's conditions & executing it if any
29
- const variableCondition = this.getVariableCondition(id, index);
30
- const variableParents =
31
- this.references.relations.variables[id]?.parents || [];
32
- const parentOptionIsDisplayed = this.isOptionDisplayed(
33
- variableParents[0],
44
+ return sharedIsVariableDisplayed(
45
+ this.references,
46
+ this.ovc,
47
+ this.conditionEvaluator,
48
+ id,
34
49
  index
35
50
  );
36
-
37
- return (
38
- [variableCondition, parentOptionIsDisplayed].filter((c) => c !== true)
39
- .length === 0
40
- );
41
51
  }
42
52
 
43
53
  /**
44
- * Checks whether an option is displayed at a given index
45
- * @param id Option's ID
46
- * @param index Index
54
+ * Checks whether an option is displayed at a given index.
47
55
  */
48
56
  isOptionDisplayed(id: number, index: number) {
49
- // Getting variable's conditions & executing it if any
50
- const optionCondition = this.getOptionCondition(id, index);
51
- const optionParents = this.references.relations.options[id]?.parents || [];
52
- const parentsConditions = optionParents.map(
53
- (optionId) => this.getOptionCondition(optionId, index) !== false
54
- );
55
- const parentsInputs = optionParents.map(
56
- (optionId) => this.ovc.options[optionId][index]
57
- );
58
- const parentSectionId = this.getOptionParentSection(id);
59
- const parentSectionCondition =
60
- this.getSectionCondition(parentSectionId) !== false;
61
-
62
- return (
63
- [
64
- optionCondition,
65
- parentSectionCondition,
66
- ...parentsConditions,
67
- ...parentsInputs,
68
- ].filter((c) => c !== true).length === 0
57
+ return sharedIsOptionDisplayed(
58
+ this.references,
59
+ this.ovc,
60
+ this.conditionEvaluator,
61
+ id,
62
+ index
69
63
  );
70
64
  }
71
65
 
72
66
  /**
73
- * Returns a variable's conditions
74
- * @param id Variable's id
75
- * @param index Variable's index
67
+ * Returns a variable's conditions.
76
68
  */
77
69
  private getVariableCondition(id: number, index: number) {
78
70
  const conditionObject = this.references.conditions.variables[id];
@@ -87,9 +79,7 @@ class FillPdfFormBase {
87
79
  }
88
80
 
89
81
  /**
90
- * Returns an option's conditions
91
- * @param id Option's id
92
- * @param index Option's index
82
+ * Returns an option's conditions.
93
83
  */
94
84
  private getOptionCondition(id: number, index: number) {
95
85
  const conditionObject = this.references.conditions.options[id];
@@ -99,8 +89,7 @@ class FillPdfFormBase {
99
89
  }
100
90
 
101
91
  /**
102
- * Returns a section's conditions
103
- * @param id Section's id
92
+ * Returns a section's conditions.
104
93
  */
105
94
  private getSectionCondition(id: number) {
106
95
  const conditionObject = this.references.conditions.sections.main[id];
@@ -108,28 +97,6 @@ class FillPdfFormBase {
108
97
  ? true
109
98
  : this.conditions.executeCondition(conditionObject, id, 0, "sections");
110
99
  }
111
-
112
- /**
113
- * Returns option's parent section
114
- * @param id Option's id
115
- */
116
- private getOptionParentSection(id: number) {
117
- const { parents } = this.references.relations.options[id];
118
-
119
- // Getting root option id
120
- const rootId = parents.length > 0 ? parents[parents.length - 1] : id;
121
-
122
- // Looking for section
123
- const sections = Object.values(this.references.sections.main);
124
-
125
- for (let i = 0; i < sections.length; i += 1) {
126
- if (sections[i].options.includes(rootId)) return sections[i].id;
127
- }
128
-
129
- throw new Error(
130
- `Cannot find parent section for option ${id} (Root option id: ${rootId})`
131
- );
132
- }
133
100
  }
134
101
 
135
102
  export default FillPdfFormBase;