@legalplace/prerenderx 3.8.24 → 3.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.
@@ -1,10 +1,23 @@
1
+ import type { OptionV3 } from "@legalplace/models-v3-types";
1
2
  import type { Types } from "@legalplace/referencesparser";
2
3
  import type ConditionsRunner from "@legalplace/conditions-runner";
3
4
  import type InputsType from "@legalplace/types/dist/inputs";
5
+ import {
6
+ isOptionDisplayed,
7
+ buildOutputTableCellStyleAttr,
8
+ buildOutputTableStyleAttr,
9
+ getOutputTableCellStyle,
10
+ replaceOutputTableMarkers,
11
+ stripOutputTableMarkers,
12
+ } from "@legalplace/referencesparser";
4
13
  import {
5
14
  parseOutputWithVariables,
6
15
  getRelatedVariablesValues,
7
16
  } from "./OutputParser";
17
+ import {
18
+ cleanupRenderedOutputHtml,
19
+ expandOutputTableMarkup,
20
+ } from "./tableOutputExpansion";
8
21
  import type NumAuto from "./NumAuto";
9
22
 
10
23
  class OptionRender {
@@ -40,83 +53,225 @@ class OptionRender {
40
53
  this.renderOption();
41
54
  }
42
55
 
56
+ /**
57
+ * Returns rendered HTML outputs for this option subtree.
58
+ */
43
59
  getOutputs() {
44
60
  return this.outputs;
45
61
  }
46
62
 
63
+ /**
64
+ * Returns whether an option is displayed at the current occurrence.
65
+ */
66
+ private isDisplayed(optionId = this.id): boolean {
67
+ return isOptionDisplayed(
68
+ this.references,
69
+ this.ovc,
70
+ (type, entityId, entityIndex) => {
71
+ if (type === "options") {
72
+ const conditionObject = this.references.conditions.options[entityId];
73
+ return conditionObject === undefined
74
+ ? true
75
+ : this.conditions.executeCondition(
76
+ conditionObject,
77
+ entityId,
78
+ entityIndex,
79
+ "options"
80
+ );
81
+ }
82
+ if (type === "sections") {
83
+ const conditionObject =
84
+ this.references.conditions.sections.main[entityId];
85
+ return conditionObject === undefined
86
+ ? true
87
+ : this.conditions.executeCondition(
88
+ conditionObject,
89
+ entityId,
90
+ entityIndex,
91
+ "sections"
92
+ );
93
+ }
94
+ return true;
95
+ },
96
+ optionId,
97
+ this.index
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Renders a table cell option as a td element.
103
+ */
104
+ private renderOutputTableCell(cellId: number): string {
105
+ const cellOption = this.references.options[cellId];
106
+ if (cellOption?.meta.tableRole !== "cell") return "";
107
+
108
+ const styleAttr = buildOutputTableCellStyleAttr(
109
+ getOutputTableCellStyle(cellOption)
110
+ );
111
+
112
+ // Hidden cells are omitted entirely instead of rendering an empty td
113
+ if (!this.isDisplayed(cellId)) {
114
+ return "";
115
+ }
116
+
117
+ const outputReference = this.references.outputs[cellId];
118
+ if (outputReference === undefined) {
119
+ return `<td${styleAttr}></td>`;
120
+ }
121
+
122
+ const autonums: [string, string, number, string | null][] =
123
+ outputReference.autonums.map((currentAn) => {
124
+ const fn =
125
+ currentAn[0] === "numauto-pre"
126
+ ? this.NumAuto.getNumPre
127
+ : this.NumAuto.getNum;
128
+ return [
129
+ currentAn[0],
130
+ currentAn[1],
131
+ fn(currentAn[1], currentAn[2]),
132
+ currentAn[2],
133
+ ];
134
+ });
135
+
136
+ const variables = getRelatedVariablesValues(
137
+ cellId,
138
+ this.index,
139
+ outputReference.variables,
140
+ this.ovc,
141
+ this.references
142
+ );
143
+
144
+ const content = parseOutputWithVariables(
145
+ outputReference.raw,
146
+ this.index,
147
+ variables,
148
+ autonums
149
+ );
150
+
151
+ return `<td${styleAttr}>${content}</td>`;
152
+ }
153
+
154
+ /**
155
+ * Renders a table row option as a tr element.
156
+ */
157
+ private renderOutputTableRow(rowId: number): string {
158
+ const rowOption = this.references.options[rowId];
159
+ if (rowOption?.meta.tableRole !== "row") return "";
160
+ if (!this.isDisplayed(rowId)) return "";
161
+
162
+ const cells = rowOption.options
163
+ .map((cellId) => this.renderOutputTableCell(cellId))
164
+ .join("");
165
+
166
+ if (cells.length === 0) return "";
167
+
168
+ return `<tr>${cells}</tr>`;
169
+ }
170
+
171
+ /**
172
+ * Renders a table option as a table element from its child rows.
173
+ */
174
+ private renderOutputTable(option: OptionV3): string {
175
+ const rows = option.options
176
+ .map((rowId) => this.renderOutputTableRow(rowId))
177
+ .filter((row) => row.length > 0)
178
+ .join("");
179
+
180
+ if (rows.length === 0) return "";
181
+
182
+ const styleAttr = buildOutputTableStyleAttr(option.meta.tableStyle);
183
+ return `<table${styleAttr}><tbody>${rows}</tbody></table>`;
184
+ }
185
+
186
+ /**
187
+ * Renders the current option output when displayed.
188
+ */
47
189
  private renderOption() {
48
190
  const option = this.references.options[this.id];
49
- const conditionObject = this.references.conditions.options[this.id];
50
- const condition =
51
- conditionObject === undefined
52
- ? true
53
- : this.conditions.executeCondition(
54
- conditionObject,
55
- this.id,
56
- this.index,
57
- "options"
58
- );
59
-
60
- // Rendering documents sections
61
- if (condition === true) {
62
- if (
63
- typeof option.meta.output === "string" &&
64
- option.meta.output.trim().length > 0
65
- ) {
66
- // output Reference
67
- const outputReference = this.references.outputs[this.id];
68
-
69
- // Autonums
70
- let autonums: [string, string, number, string | null][] = [];
71
- if (condition !== false) {
72
- autonums = outputReference.autonums.map((currentAn) => {
73
- const fn =
74
- currentAn[0] === "numauto-pre"
75
- ? this.NumAuto.getNumPre
76
- : this.NumAuto.getNum;
77
- return [
78
- currentAn[0],
79
- currentAn[1],
80
- fn(currentAn[1], currentAn[2]),
81
- currentAn[2],
82
- ];
83
- });
84
- }
191
+ const { tableRole } = option.meta;
192
+
193
+ if (tableRole === "row" || tableRole === "cell") {
194
+ return;
195
+ }
196
+
197
+ if (!this.isDisplayed()) return;
85
198
 
86
- // variables
87
- const variables = getRelatedVariablesValues(
88
- this.id,
89
- this.index,
90
- outputReference.variables,
91
- this.ovc,
92
- this.references
93
- );
94
- let output = parseOutputWithVariables(
95
- option.meta.output,
96
- this.index,
97
- variables,
98
- autonums
99
- );
100
-
101
- // Removing spaces between tags
102
- output = output.replace(/<\/([a-z]+)>([\n\s]+)</gi, "</$1><");
103
-
104
- // Replacing \n with line-break
105
- output = output.replace(/\n/g, "<br />");
106
-
107
- // Cleaning \xA0
108
- output = output.replace(/\xA0/g, " ");
109
-
110
- // Pushing output
111
- this.outputs.push(output);
199
+ if (tableRole === "table") {
200
+ const tableHtml = this.renderOutputTable(option);
201
+ if (tableHtml.length > 0) {
202
+ this.outputs.push(cleanupRenderedOutputHtml(tableHtml));
112
203
  }
113
- this.renderChildren();
204
+ return;
114
205
  }
206
+
207
+ if (
208
+ typeof option.meta.output === "string" &&
209
+ option.meta.output.trim().length > 0
210
+ ) {
211
+ const outputReference = this.references.outputs[this.id];
212
+
213
+ const autonums: [string, string, number, string | null][] =
214
+ outputReference.autonums.map((currentAn) => {
215
+ const fn =
216
+ currentAn[0] === "numauto-pre"
217
+ ? this.NumAuto.getNumPre
218
+ : this.NumAuto.getNum;
219
+ return [
220
+ currentAn[0],
221
+ currentAn[1],
222
+ fn(currentAn[1], currentAn[2]),
223
+ currentAn[2],
224
+ ];
225
+ });
226
+
227
+ const variables = getRelatedVariablesValues(
228
+ this.id,
229
+ this.index,
230
+ outputReference.variables,
231
+ this.ovc,
232
+ this.references
233
+ );
234
+
235
+ let output = expandOutputTableMarkup({
236
+ output: option.meta.output,
237
+ references: this.references,
238
+ ovc: this.ovc,
239
+ conditions: this.conditions,
240
+ contextOptionId: this.id,
241
+ defaultOccurrenceIndex: this.index,
242
+ });
243
+
244
+ output = parseOutputWithVariables(
245
+ output,
246
+ this.index,
247
+ variables,
248
+ autonums
249
+ );
250
+
251
+ output = replaceOutputTableMarkers(output, (tableId) => {
252
+ const childOption = this.references.options[tableId];
253
+ if (!childOption || childOption.meta.tableRole !== "table") return "";
254
+ if (!this.isDisplayed(tableId)) return "";
255
+ return this.renderOutputTable(childOption);
256
+ });
257
+
258
+ output = stripOutputTableMarkers(output);
259
+ output = cleanupRenderedOutputHtml(output);
260
+
261
+ this.outputs.push(output);
262
+ }
263
+ this.renderChildren();
115
264
  }
116
265
 
266
+ /**
267
+ * Recursively renders child options.
268
+ */
117
269
  private renderChildren() {
118
270
  const option = this.references.options[this.id];
119
271
  option.options.forEach((optionId) => {
272
+ const childOption = this.references.options[optionId];
273
+ if (childOption?.meta.tableRole === "table") return;
274
+
120
275
  this.outputs = [
121
276
  ...this.outputs,
122
277
  ...new OptionRender({
@@ -199,20 +199,33 @@ export const parseOutputWithVariables: parseOutputT = (
199
199
  };
200
200
 
201
201
  /**
202
- * Parses document name with variables
203
- * @param text Document's name
202
+ * Parses document name with variables.
203
+ * When `index` is provided, each `[var:X]` is resolved using the value
204
+ * at occurrence `index` instead of always using occurrence 0. This is
205
+ * required for document multiples where each occurrence of the document
206
+ * must have its own filename interpolated with the right variable values.
207
+ *
208
+ * @param text Document's name (or filenameTemplate)
209
+ * @param ovc Inputs (parallel arrays)
210
+ * @param references References parsed from the model
211
+ * @param fallback Fallback used when an interpolated variable is empty
212
+ * @param index Occurrence index (defaults to 0)
204
213
  */
205
214
  type TParseDocumentNameWithVariables = (
206
215
  text: string,
207
216
  ovc: InputsType,
208
217
  references: Types.ReferencesType,
209
- fallback?: string
218
+ fallback?: string,
219
+ index?: number,
220
+ contextOptionId?: number
210
221
  ) => string;
211
222
  export const parseDocumentNameWithVariables: TParseDocumentNameWithVariables = (
212
223
  text,
213
224
  ovc,
214
225
  references,
215
- fallback
226
+ fallback,
227
+ index = 0,
228
+ contextOptionId
216
229
  ) => {
217
230
  /**
218
231
  * Variables
@@ -239,12 +252,30 @@ export const parseDocumentNameWithVariables: TParseDocumentNameWithVariables = (
239
252
  (c) => ovc !== undefined || references.variables[c] !== undefined
240
253
  );
241
254
 
242
- // Getting values
243
255
  const variablesValues: Record<string, string> = {};
244
- variables.forEach((variableId) => {
245
- const value = ovc.variables[variableId]?.[0] || "";
246
- variablesValues[variableId] = value.toString();
247
- });
256
+ if (typeof contextOptionId === "number") {
257
+ const relatedValues = getRelatedVariablesValues(
258
+ contextOptionId,
259
+ index,
260
+ variables,
261
+ ovc,
262
+ references
263
+ );
264
+ variables.forEach((variableId) => {
265
+ variablesValues[variableId] = (
266
+ relatedValues[variableId] ?? ""
267
+ ).toString();
268
+ });
269
+ } else {
270
+ variables.forEach((variableId) => {
271
+ const variableValues = ovc.variables[variableId];
272
+ const valueAtIndex =
273
+ variableValues?.[index] !== undefined
274
+ ? variableValues[index]
275
+ : variableValues?.[0] || "";
276
+ variablesValues[variableId] = (valueAtIndex ?? "").toString();
277
+ });
278
+ }
248
279
 
249
280
  // Inserting values
250
281
  let parsedText = `${text}`;
@@ -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,182 @@ 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 documentSourceId =
214
+ isMultipleDocument && typeof docMultiple.repeatOption === "number"
215
+ ? docMultiple.repeatOption
216
+ : 0;
217
+ const condition =
218
+ conditionObject === undefined
219
+ ? true
220
+ : this.conditions.executeCondition(
221
+ conditionObject,
222
+ documentSourceId,
223
+ occurrenceIndex,
224
+ "documents"
225
+ );
226
+
227
+ if (condition !== true) return;
228
+
229
+ const seed = isMultipleDocument ? `${slug}:${occurrenceIndex}` : slug;
230
+ const hash = crypto.createHash("sha1").update(seed).digest("hex");
231
+
232
+ const name =
233
+ isMultipleDocument && docMultiple
234
+ ? this.resolveDocumentOccurrenceName(
235
+ document,
236
+ docMultiple,
237
+ occurrenceIndex
238
+ )
239
+ : parseDocumentNameWithVariables(
240
+ document.name,
241
+ this.ovc,
242
+ this.references,
243
+ document.fallbackName
244
+ );
245
+
119
246
  if (
120
247
  typeof document.pdf === "object" &&
121
248
  typeof document.pdf.id === "string" &&
122
249
  typeof document.pdf.form === "object"
123
250
  ) {
124
251
  this.documentsIndex[hash] = {
125
- name: parseDocumentNameWithVariables(
126
- document.name,
127
- this.ovc,
128
- this.references,
129
- document.fallbackName
130
- ),
252
+ name,
131
253
  slug,
132
254
  form: true,
133
255
  fdf: {
134
256
  id: "",
135
257
  fields: {},
136
258
  },
259
+ ...(isMultipleDocument ? { occurrenceIndex } : {}),
137
260
  };
138
261
  } else {
139
262
  this.documentsIndex[hash] = {
140
- name: parseDocumentNameWithVariables(
141
- document.name,
142
- this.ovc,
143
- this.references,
144
- document.fallbackName
145
- ),
263
+ name,
146
264
  slug,
147
265
  pdf: documentParams?.formats?.pdf !== false,
148
266
  docx: documentParams?.formats?.docx !== false,
267
+ ...(isMultipleDocument ? { occurrenceIndex } : {}),
149
268
  };
150
269
  }
151
- }
270
+ });
152
271
  });
153
272
  }
154
273
 
@@ -164,15 +283,15 @@ class Prerender {
164
283
  private generateAllDocuments() {
165
284
  Object.keys(this.documentsIndex).forEach((documentNameHash) => {
166
285
  const index = this.documentsIndex[documentNameHash];
286
+ const occurrenceIndex = index.occurrenceIndex ?? 0;
167
287
  if (Prerender.isPdfForm(index)) {
168
288
  this.renderedDocuments[documentNameHash] = this.pdfFiller.fillDocument(
169
- this.documentsIndex[documentNameHash].slug
289
+ index.slug,
290
+ occurrenceIndex
170
291
  );
171
292
  } else {
172
293
  this.renderedDocuments[documentNameHash] =
173
- this.documentRender.renderDocument(
174
- this.documentsIndex[documentNameHash].slug
175
- );
294
+ this.documentRender.renderDocument(index.slug, occurrenceIndex);
176
295
  }
177
296
  });
178
297
  }