@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.
@@ -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;
@@ -0,0 +1,145 @@
1
+ import LpLogic from "@legalplace/lplogic";
2
+ import type { ConditionV3 } from "@legalplace/models-v3-types";
3
+ import type ConditionsRunner from "@legalplace/conditions-runner";
4
+ import DataPopulator from "@legalplace/conditions-runner/dist/DataPopulator";
5
+ import type { Types } from "@legalplace/referencesparser";
6
+ import {
7
+ expandTableMarkup,
8
+ extractConditionDataMap,
9
+ hasTableDynamicMarkup,
10
+ stripConditionUiMetadata,
11
+ type ExpandTableMarkupContext,
12
+ } from "@legalplace/referencesparser";
13
+ import type InputsType from "@legalplace/types/dist/inputs";
14
+ import {
15
+ getRelatedVariablesValues,
16
+ parseOutputWithVariables,
17
+ } from "./OutputParser";
18
+
19
+ /**
20
+ * Extracts variable ids referenced as `[var:N]` tags in an HTML fragment.
21
+ */
22
+ const extractVariableIds = (html: string): number[] => {
23
+ const matches = html.match(/\[var:([0-9]+)\]/gi);
24
+ if (matches === null) return [];
25
+
26
+ return Array.from(
27
+ new Set(
28
+ matches.map((tag) => {
29
+ const idMatch = tag.match(/([0-9]+)/);
30
+ return idMatch ? parseInt(idMatch[1], 10) : 0;
31
+ })
32
+ )
33
+ ).filter((id) => id > 0);
34
+ };
35
+
36
+ /**
37
+ * Evaluates an inline table condition at a given occurrence index.
38
+ */
39
+ export const evaluateInlineTableCondition = (
40
+ condition: ConditionV3,
41
+ contextOptionId: number,
42
+ occurrenceIndex: number,
43
+ conditionsRunner: ConditionsRunner,
44
+ references: Types.ReferencesType,
45
+ ovc: InputsType
46
+ ): boolean => {
47
+ const dataMap = extractConditionDataMap(condition);
48
+ const currentData = new DataPopulator(
49
+ conditionsRunner,
50
+ references,
51
+ ovc,
52
+ dataMap,
53
+ contextOptionId,
54
+ occurrenceIndex
55
+ ).getData();
56
+ const result = LpLogic(stripConditionUiMetadata(condition), currentData);
57
+ return result !== false;
58
+ };
59
+
60
+ type CreateTableMarkupContextParams = {
61
+ references: Types.ReferencesType;
62
+ ovc: InputsType;
63
+ conditions: ConditionsRunner;
64
+ contextOptionId: number;
65
+ defaultOccurrenceIndex: number;
66
+ };
67
+
68
+ /**
69
+ * Builds the expandTableMarkup context for prerenderx output rendering.
70
+ */
71
+ export const createTableMarkupContext = ({
72
+ references,
73
+ ovc,
74
+ conditions,
75
+ contextOptionId,
76
+ defaultOccurrenceIndex,
77
+ }: CreateTableMarkupContextParams): ExpandTableMarkupContext => ({
78
+ defaultContextOptionId: contextOptionId,
79
+ defaultOccurrenceIndex,
80
+ getOccurrenceInputs: (multipleOptionId: number) =>
81
+ ovc.options[String(multipleOptionId)] || [],
82
+ evaluateCondition: (condition, multipleOptionId, occurrenceIndex) =>
83
+ evaluateInlineTableCondition(
84
+ condition,
85
+ multipleOptionId,
86
+ occurrenceIndex,
87
+ conditions,
88
+ references,
89
+ ovc
90
+ ),
91
+ substituteVariables: (html, multipleOptionId, occurrenceIndex) => {
92
+ const variableIds = extractVariableIds(html);
93
+ const variables = getRelatedVariablesValues(
94
+ multipleOptionId,
95
+ occurrenceIndex,
96
+ variableIds,
97
+ ovc,
98
+ references
99
+ );
100
+ return parseOutputWithVariables(html, occurrenceIndex, variables, []);
101
+ },
102
+ });
103
+
104
+ type ExpandOutputTableMarkupParams = CreateTableMarkupContextParams & {
105
+ output: string;
106
+ };
107
+
108
+ /**
109
+ * Expands dynamic table markup in an output HTML string when needed.
110
+ */
111
+ export const expandOutputTableMarkup = ({
112
+ output,
113
+ references,
114
+ ovc,
115
+ conditions,
116
+ contextOptionId,
117
+ defaultOccurrenceIndex,
118
+ }: ExpandOutputTableMarkupParams): string => {
119
+ if (!hasTableDynamicMarkup(output)) return output;
120
+
121
+ return expandTableMarkup(
122
+ output,
123
+ createTableMarkupContext({
124
+ references,
125
+ ovc,
126
+ conditions,
127
+ contextOptionId,
128
+ defaultOccurrenceIndex,
129
+ })
130
+ );
131
+ };
132
+
133
+ /**
134
+ * Applies post-processing cleanup to rendered output HTML.
135
+ */
136
+ export const cleanupRenderedOutputHtml = (output: string): string => {
137
+ if (output.includes("<table")) {
138
+ return output.replace(/\xA0/g, " ");
139
+ }
140
+
141
+ return output
142
+ .replace(/<\/([a-z]+)>([\n\s]+)</gi, "</$1><")
143
+ .replace(/\n/g, "<br />")
144
+ .replace(/\xA0/g, " ");
145
+ };
@@ -1,14 +1,27 @@
1
+ import type { DocumentLayoutV3 } from "@legalplace/models-v3-types";
2
+
1
3
  export type DocumentOutputIndex = {
2
4
  name: string;
3
5
  slug: string;
4
6
  pdf: boolean;
5
7
  docx: boolean;
8
+ /**
9
+ * Occurrence index of the document when the document is multiple
10
+ * (`params.multiple.enabled === true`). `undefined` for non-multiple
11
+ * documents (treated as 0 internally).
12
+ */
13
+ occurrenceIndex?: number;
6
14
  };
15
+
7
16
  export type DocumentPdfFormIndex = {
8
17
  name: string;
9
18
  slug: string;
10
19
  form: boolean;
11
20
  fdf: FDFType;
21
+ /**
22
+ * Occurrence index of the document when the document is multiple.
23
+ */
24
+ occurrenceIndex?: number;
12
25
  };
13
26
 
14
27
  export type DocumentIndex = Record<
@@ -20,3 +33,17 @@ export type FDFType = {
20
33
  id: string;
21
34
  fields: Record<string, string>;
22
35
  };
36
+
37
+ /**
38
+ * Rendered header/footer regions and page layout for DOCX generation.
39
+ */
40
+ export type DocumentLayout = {
41
+ header?: string;
42
+ footer?: string;
43
+ firstPageHeader?: string;
44
+ firstPageFooter?: string;
45
+ differentFirstPage: boolean;
46
+ margins?: DocumentLayoutV3["margins"];
47
+ headerDistance?: number;
48
+ footerDistance?: number;
49
+ };