@c-rex/components 0.3.0-build.38 → 0.3.0-build.40

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,14 +1,25 @@
1
1
  import { FilterItem, Tags } from "@c-rex/interfaces";
2
2
  import { EN_LANG } from "@c-rex/constants";
3
- import type { TaxonomyResult } from "@c-rex/services/read-models";
4
-
5
- export type FacetLabelOverride = {
3
+ import {
4
+ type InformationUnitPropertyKey,
5
+ type TaxonomyResult,
6
+ } from "@c-rex/services/read-models";
7
+ import {
8
+ isMetadataGroupDenied,
9
+ isMetadataPropertyRuntimeSuppressed,
10
+ } from "@c-rex/services/metadata-visibility-policy";
11
+
12
+ export type FacetPresentationItem = {
6
13
  label: string;
7
14
  groupLabel?: string;
8
15
  sectionLabel?: string;
16
+ hidden?: boolean;
9
17
  };
10
18
 
11
- export type FacetLabelOverrides = Partial<Record<string, Record<string, FacetLabelOverride>>>;
19
+ export type FacetPresentation = Partial<Record<string, Record<string, FacetPresentationItem>>>;
20
+
21
+ export type FacetLabelOverride = FacetPresentationItem;
22
+ export type FacetLabelOverrides = FacetPresentation;
12
23
 
13
24
  export type ResolvedFilterItem = FilterItem & {
14
25
  groupLabel?: string;
@@ -20,6 +31,20 @@ export type ResolvedFilterItem = FilterItem & {
20
31
 
21
32
  export type FacetTaxonomies = Partial<Record<string, TaxonomyResult>>;
22
33
 
34
+ export const resolveFacetTaxonomy = (
35
+ facetKey: string,
36
+ facetTaxonomies?: FacetTaxonomies
37
+ ): TaxonomyResult | undefined => {
38
+ if (!facetTaxonomies) return undefined;
39
+ if (facetTaxonomies[facetKey]) return facetTaxonomies[facetKey];
40
+
41
+ if (facetKey === "categories") {
42
+ return facetTaxonomies.informationSubjects;
43
+ }
44
+
45
+ return undefined;
46
+ };
47
+
23
48
  export const updateFilterParam = (key: string, item: FilterItem, filter: string | null): Record<string, string | null> => {
24
49
 
25
50
  if (key === "packages") {
@@ -93,24 +118,28 @@ export const clearData = (
93
118
  tags: Tags,
94
119
  options?: {
95
120
  uiLanguage?: string;
96
- labelOverrides?: FacetLabelOverrides;
121
+ facetPresentation?: FacetPresentation;
122
+ labelOverrides?: FacetPresentation;
97
123
  includeZeroHits?: boolean;
98
124
  facetTaxonomies?: FacetTaxonomies;
99
125
  }
100
126
  ): Record<string, ResolvedFilterItem[]> => {
101
127
  const includeZeroHits = options?.includeZeroHits ?? true;
128
+ const facetPresentation = options?.facetPresentation || options?.labelOverrides;
102
129
  const filteredTags: Record<string, ResolvedFilterItem[]> = {}
103
130
  const taxonomyNodesByFacet = Object.entries(options?.facetTaxonomies || {}).reduce<Record<string, Map<string, {
104
131
  id: string;
105
132
  shortId?: string;
106
133
  label: string;
107
134
  parentIds: string[];
135
+ classId?: string;
108
136
  }>>>((acc, [facetKey, taxonomy]) => {
109
137
  const nodes = new Map<string, {
110
138
  id: string;
111
139
  shortId?: string;
112
140
  label: string;
113
141
  parentIds: string[];
142
+ classId?: string;
114
143
  }>();
115
144
 
116
145
  Object.values(taxonomy?.nodesById || {}).forEach((node) => {
@@ -119,6 +148,7 @@ export const clearData = (
119
148
  shortId: node.shortId,
120
149
  label: node.label,
121
150
  parentIds: node.parentIds || [],
151
+ classId: node.classRef?.id,
122
152
  };
123
153
  nodes.set(node.id, normalized);
124
154
  if (node.shortId) {
@@ -130,7 +160,14 @@ export const clearData = (
130
160
  return acc;
131
161
  }, {});
132
162
 
163
+ if (!taxonomyNodesByFacet.categories && taxonomyNodesByFacet.informationSubjects) {
164
+ taxonomyNodesByFacet.categories = taxonomyNodesByFacet.informationSubjects;
165
+ }
166
+
133
167
  for (const [key, value] of Object.entries(tags)) {
168
+ if (isMetadataPropertyRuntimeSuppressed(key as InformationUnitPropertyKey)) {
169
+ continue;
170
+ }
134
171
  if (!value || !value.items || value.items.length === 0) {
135
172
  continue;
136
173
  }
@@ -139,13 +176,16 @@ export const clearData = (
139
176
  if (item?.shortId === undefined) return null
140
177
  if (!includeZeroHits && Number(item.hits) === 0) return null
141
178
 
142
- const override = options?.labelOverrides?.[key]?.[item.shortId];
143
179
  const runtimeItem = item as typeof item & {
144
180
  id?: string;
145
181
  parents?: Array<{ id?: string; shortId?: string }>;
146
182
  };
183
+ const presentationItem = facetPresentation?.[key]?.[item.shortId]
184
+ || (runtimeItem.id ? facetPresentation?.[key]?.[runtimeItem.id] : undefined);
147
185
  const taxonomyNode = taxonomyNodesByFacet[key]?.get(runtimeItem.id || item.shortId);
148
- const label = override?.label || taxonomyNode?.label || resolveLabelByLanguage(item.labels || [], options?.uiLanguage);
186
+ if (presentationItem?.hidden) return null;
187
+ if (isMetadataGroupDenied(key as InformationUnitPropertyKey, { classId: taxonomyNode?.classId })) return null;
188
+ const label = presentationItem?.label || taxonomyNode?.label || resolveLabelByLanguage(item.labels || [], options?.uiLanguage);
149
189
  if (!label) return null;
150
190
  const taxonomyId = taxonomyNode?.id || runtimeItem.id;
151
191
  const parentIds = taxonomyNode?.parentIds || (runtimeItem.parents || [])
@@ -158,8 +198,8 @@ export const clearData = (
158
198
  label,
159
199
  active: false,
160
200
  shortId: item.shortId,
161
- groupLabel: override?.groupLabel,
162
- sectionLabel: override?.sectionLabel,
201
+ groupLabel: presentationItem?.groupLabel,
202
+ sectionLabel: presentationItem?.sectionLabel,
163
203
  taxonomyId,
164
204
  parentIds,
165
205
  }
@@ -182,7 +222,8 @@ export const memoizeFilteredTags = (
182
222
  packages: string | null,
183
223
  options?: {
184
224
  uiLanguage?: string;
185
- labelOverrides?: FacetLabelOverrides;
225
+ facetPresentation?: FacetPresentation;
226
+ labelOverrides?: FacetPresentation;
186
227
  includeZeroHits?: boolean;
187
228
  facetTaxonomies?: FacetTaxonomies;
188
229
  }
@@ -247,9 +288,49 @@ export const applyFacetPropertyVisibility = (
247
288
  ]);
248
289
 
249
290
  return Object.entries(tags).reduce<Record<string, ResolvedFilterItem[]>>((acc, [key, value]) => {
291
+ if (isMetadataPropertyRuntimeSuppressed(key as InformationUnitPropertyKey)) return acc;
250
292
  if (includeSet.size > 0 && !includeSet.has(key)) return acc;
251
293
  if (excludeSet.has(key)) return acc;
252
294
  acc[key] = value;
253
295
  return acc;
254
296
  }, {});
255
297
  }
298
+
299
+ export const pruneHierarchyFacetItemsForMerge = (
300
+ facetKey: string,
301
+ items: ResolvedFilterItem[]
302
+ ): ResolvedFilterItem[] => {
303
+ const isCategoryFacet = facetKey === "categories" || facetKey === "informationSubjects";
304
+ if (!isCategoryFacet) return items;
305
+
306
+ const hasActiveSelection = items.some((item) => item.active);
307
+ if (!hasActiveSelection) return items;
308
+
309
+ return items.filter((item) => item.active || Number(item.hits) > 0 || Number(item.total) > 0);
310
+ };
311
+
312
+ export const resolveHierarchyFacetCountLabel = (
313
+ item: ResolvedFilterItem,
314
+ childItems: ResolvedFilterItem[]
315
+ ): string | undefined => {
316
+ if (item.isStructural && childItems.length > 0) {
317
+ return undefined;
318
+ }
319
+
320
+ return `(${item.hits}/${item.total})`;
321
+ };
322
+
323
+ export const shouldRenderFacetHierarchy = (
324
+ items: ResolvedFilterItem[],
325
+ options?: { hasTaxonomy?: boolean }
326
+ ): boolean => {
327
+ const hasTaxonomy = options?.hasTaxonomy ?? false;
328
+ const hasSectionLabels = items.some((item) => Boolean(item.sectionLabel));
329
+ const hasParentRelations = items.some((item) => (item.parentIds || []).length > 0);
330
+
331
+ if (hasTaxonomy) {
332
+ return hasParentRelations;
333
+ }
334
+
335
+ return !hasSectionLabels && hasParentRelations;
336
+ };
@@ -14,7 +14,7 @@ import { GenericTableResultList } from "./table-result-list";
14
14
  import { Pagination } from "../pagination";
15
15
  import { Skeleton } from "@c-rex/ui/skeleton";
16
16
  import type { CommonItemsModel, ResultContainerModel, Tags } from "@c-rex/interfaces";
17
- import type { FacetLabelOverrides } from "../filter-sidebar/utils";
17
+ import type { FacetPresentation } from "../filter-sidebar/utils";
18
18
 
19
19
  type SerializedError = { message: string; name: string } | null;
20
20
 
@@ -41,7 +41,8 @@ export type GenericSearchResultsClientProps<
41
41
  linkPattern: string;
42
42
  addPackageId: boolean;
43
43
  showFilesColumn?: boolean;
44
- facetLabelOverrides?: FacetLabelOverrides;
44
+ facetPresentation?: FacetPresentation;
45
+ facetLabelOverrides?: FacetPresentation;
45
46
  informationSubjectsTaxonomy?: NonNullable<FilterSidebarProps["facetTaxonomies"]>["informationSubjects"];
46
47
  facetExcludeProperties?: string[];
47
48
  };
@@ -83,7 +84,8 @@ type SearchResultsBodyProps<TItem extends CommonItemsModel> = {
83
84
  data: ResultContainerModel<TItem> | null;
84
85
  error: SerializedError;
85
86
  isLoading: boolean;
86
- facetLabelOverrides?: FacetLabelOverrides;
87
+ facetPresentation?: FacetPresentation;
88
+ facetLabelOverrides?: FacetPresentation;
87
89
  informationSubjectsTaxonomy?: NonNullable<FilterSidebarProps["facetTaxonomies"]>["informationSubjects"];
88
90
  facetExcludeProperties: string[];
89
91
  search: string;
@@ -96,6 +98,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
96
98
  data,
97
99
  error,
98
100
  isLoading,
101
+ facetPresentation,
99
102
  facetLabelOverrides,
100
103
  informationSubjectsTaxonomy,
101
104
  facetExcludeProperties,
@@ -105,6 +108,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
105
108
  showFilesColumn,
106
109
  }: SearchResultsBodyProps<TItem>) => {
107
110
  const [lastFacetTags, setLastFacetTags] = useState<Tags | undefined>(undefined);
111
+ const effectiveFacetPresentation = facetPresentation || facetLabelOverrides;
108
112
 
109
113
  useEffect(() => {
110
114
  if (hasFacetTags(data?.tags)) {
@@ -129,7 +133,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
129
133
  <div className="flex flex-col gap-4 pb-4">
130
134
  <FilterNavbar
131
135
  tags={effectiveTags}
132
- facetLabelOverrides={facetLabelOverrides}
136
+ facetPresentation={effectiveFacetPresentation}
133
137
  excludeProperties={facetExcludeProperties}
134
138
  />
135
139
  <Empty />
@@ -142,7 +146,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
142
146
  <div className="flex flex-col gap-4 pb-4">
143
147
  <FilterNavbar
144
148
  tags={effectiveTags}
145
- facetLabelOverrides={facetLabelOverrides}
149
+ facetPresentation={effectiveFacetPresentation}
146
150
  excludeProperties={facetExcludeProperties}
147
151
  />
148
152
 
@@ -150,7 +154,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
150
154
  <FilterSidebar
151
155
  tags={effectiveTags}
152
156
  totalItemCount={data.pageInfo.totalItemCount}
153
- facetLabelOverrides={facetLabelOverrides}
157
+ facetPresentation={effectiveFacetPresentation}
154
158
  facetTaxonomies={informationSubjectsTaxonomy ? { informationSubjects: informationSubjectsTaxonomy } : undefined}
155
159
  excludeProperties={facetExcludeProperties}
156
160
  />
@@ -167,7 +171,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
167
171
  <div className="flex flex-col gap-4 pb-4">
168
172
  <FilterNavbar
169
173
  tags={effectiveTags}
170
- facetLabelOverrides={facetLabelOverrides}
174
+ facetPresentation={effectiveFacetPresentation}
171
175
  excludeProperties={facetExcludeProperties}
172
176
  />
173
177
 
@@ -175,7 +179,7 @@ const SearchResultsBody = <TItem extends CommonItemsModel>({
175
179
  <FilterSidebar
176
180
  tags={effectiveTags}
177
181
  totalItemCount={data.pageInfo.totalItemCount}
178
- facetLabelOverrides={facetLabelOverrides}
182
+ facetPresentation={effectiveFacetPresentation}
179
183
  facetTaxonomies={informationSubjectsTaxonomy ? { informationSubjects: informationSubjectsTaxonomy } : undefined}
180
184
  excludeProperties={facetExcludeProperties}
181
185
  />
@@ -211,6 +215,7 @@ export const GenericSearchResultsClient = <
211
215
  linkPattern,
212
216
  addPackageId,
213
217
  showFilesColumn = false,
218
+ facetPresentation,
214
219
  facetLabelOverrides,
215
220
  informationSubjectsTaxonomy,
216
221
  facetExcludeProperties = [],
@@ -244,6 +249,7 @@ export const GenericSearchResultsClient = <
244
249
  data={data}
245
250
  error={error}
246
251
  isLoading={isLoading}
252
+ facetPresentation={facetPresentation}
247
253
  facetLabelOverrides={facetLabelOverrides}
248
254
  informationSubjectsTaxonomy={informationSubjectsTaxonomy}
249
255
  facetExcludeProperties={facetExcludeProperties}
@@ -0,0 +1,144 @@
1
+ import { buildTaxonomyHierarchy, type HierarchyItem } from "../hierarchy";
2
+
3
+ describe("taxonomy hierarchy", () => {
4
+ it("merges runtime items into the cached taxonomy hierarchy and materializes missing parents structurally", () => {
5
+ const items: HierarchyItem[] = [
6
+ {
7
+ shortId: "catChild",
8
+ label: "Child",
9
+ active: false,
10
+ hits: 2,
11
+ total: 2,
12
+ taxonomyId: "category-child",
13
+ parentIds: ["category-root"],
14
+ },
15
+ ];
16
+
17
+ const hierarchy = buildTaxonomyHierarchy(items, {
18
+ generatedAt: "2026-04-16T00:00:00.000Z",
19
+ roots: [],
20
+ nodesById: {
21
+ "category-root": {
22
+ id: "category-root",
23
+ shortId: "catRoot",
24
+ label: "Root",
25
+ parentIds: [],
26
+ },
27
+ "category-child": {
28
+ id: "category-child",
29
+ shortId: "catChild",
30
+ label: "Child",
31
+ parentIds: ["category-root"],
32
+ },
33
+ },
34
+ });
35
+
36
+ expect(hierarchy.roots).toEqual([
37
+ expect.objectContaining({
38
+ shortId: "catRoot",
39
+ taxonomyId: "category-root",
40
+ isStructural: true,
41
+ }),
42
+ ]);
43
+ expect(hierarchy.children.get("category-root")).toEqual([
44
+ expect.objectContaining({
45
+ shortId: "catChild",
46
+ taxonomyId: "category-child",
47
+ label: "Child",
48
+ hits: 2,
49
+ total: 2,
50
+ }),
51
+ ]);
52
+ });
53
+
54
+ it("falls back to the local parent links when no taxonomy is provided", () => {
55
+ const items: HierarchyItem[] = [
56
+ {
57
+ shortId: "root",
58
+ label: "Root",
59
+ active: false,
60
+ hits: 1,
61
+ total: 1,
62
+ },
63
+ {
64
+ shortId: "child",
65
+ label: "Child",
66
+ active: false,
67
+ hits: 1,
68
+ total: 1,
69
+ parentIds: ["root"],
70
+ },
71
+ ];
72
+
73
+ const hierarchy = buildTaxonomyHierarchy(items);
74
+
75
+ expect(hierarchy.roots.map((item) => item.shortId)).toEqual(["root"]);
76
+ expect(hierarchy.children.get("root")?.map((item) => item.shortId)).toEqual(["child"]);
77
+ });
78
+
79
+ it("attaches polyhierarchical nodes to all known parents", () => {
80
+ const items: HierarchyItem[] = [
81
+ {
82
+ shortId: "parent-a",
83
+ label: "Parent A",
84
+ active: false,
85
+ hits: 1,
86
+ total: 1,
87
+ },
88
+ {
89
+ shortId: "parent-b",
90
+ label: "Parent B",
91
+ active: false,
92
+ hits: 1,
93
+ total: 1,
94
+ },
95
+ {
96
+ shortId: "child",
97
+ label: "Child",
98
+ active: false,
99
+ hits: 1,
100
+ total: 1,
101
+ parentIds: ["parent-a", "parent-b"],
102
+ },
103
+ ];
104
+
105
+ const hierarchy = buildTaxonomyHierarchy(items);
106
+
107
+ expect(hierarchy.children.get("parent-a")?.map((item) => item.shortId)).toEqual(["child"]);
108
+ expect(hierarchy.children.get("parent-b")?.map((item) => item.shortId)).toEqual(["child"]);
109
+ });
110
+
111
+ it("matches taxonomy nodes by shortId when runtime facet items have no stable taxonomy id", () => {
112
+ const items: HierarchyItem[] = [
113
+ {
114
+ shortId: "catChild",
115
+ label: "Child",
116
+ active: false,
117
+ hits: 2,
118
+ total: 2,
119
+ },
120
+ ];
121
+
122
+ const hierarchy = buildTaxonomyHierarchy(items, {
123
+ generatedAt: "2026-04-24T00:00:00.000Z",
124
+ roots: [],
125
+ nodesById: {
126
+ "category-root": {
127
+ id: "category-root",
128
+ shortId: "catRoot",
129
+ label: "Root",
130
+ parentIds: [],
131
+ },
132
+ "category-child": {
133
+ id: "category-child",
134
+ shortId: "catChild",
135
+ label: "Child",
136
+ parentIds: ["category-root"],
137
+ },
138
+ },
139
+ });
140
+
141
+ expect(hierarchy.roots.map((item) => item.shortId)).toEqual(["catRoot"]);
142
+ expect(hierarchy.children.get("category-root")?.map((item) => item.shortId)).toEqual(["catChild"]);
143
+ });
144
+ });
@@ -0,0 +1,137 @@
1
+ import type { TaxonomyResult } from "@c-rex/services/read-models";
2
+
3
+ export type HierarchyItem = {
4
+ shortId: string;
5
+ label: string;
6
+ active: boolean;
7
+ hits: number;
8
+ total: number;
9
+ taxonomyId?: string;
10
+ parentIds?: string[];
11
+ isStructural?: boolean;
12
+ };
13
+
14
+ export type HierarchyTree<TItem extends HierarchyItem> = {
15
+ roots: TItem[];
16
+ children: Map<string, TItem[]>;
17
+ };
18
+
19
+ const sortItems = <TItem extends HierarchyItem>(items: TItem[]) =>
20
+ items.sort((a, b) => a.label.localeCompare(b.label));
21
+
22
+ export const buildTaxonomyHierarchy = <TItem extends HierarchyItem>(
23
+ items: TItem[],
24
+ taxonomy?: TaxonomyResult
25
+ ): HierarchyTree<TItem> => {
26
+ if (taxonomy) {
27
+ const selectedById = new Map<string, TItem>();
28
+ const includedIds = new Set<string>();
29
+ const resolveTaxonomyNodeId = (item: TItem): string | undefined => {
30
+ const directId = item.taxonomyId || item.shortId;
31
+ if (directId && taxonomy.nodesById[directId]) return directId;
32
+
33
+ return Object.values(taxonomy.nodesById).find((node) => (
34
+ Boolean(item.taxonomyId && node.id === item.taxonomyId) ||
35
+ Boolean(item.shortId && node.shortId === item.shortId)
36
+ ))?.id;
37
+ };
38
+
39
+ const includeAncestorChain = (nodeId: string) => {
40
+ const visited = new Set<string>();
41
+ const stack = [nodeId];
42
+
43
+ while (stack.length > 0) {
44
+ const currentId = stack.pop();
45
+ if (!currentId || visited.has(currentId)) continue;
46
+ visited.add(currentId);
47
+
48
+ const currentNode = taxonomy.nodesById[currentId];
49
+ (currentNode?.parentIds || []).forEach((parentId) => {
50
+ if (taxonomy.nodesById[parentId]) {
51
+ includedIds.add(parentId);
52
+ stack.push(parentId);
53
+ }
54
+ });
55
+ }
56
+ };
57
+
58
+ items.forEach((item) => {
59
+ const itemId = resolveTaxonomyNodeId(item);
60
+ if (!itemId) return;
61
+
62
+ selectedById.set(itemId, item);
63
+ includedIds.add(itemId);
64
+ includeAncestorChain(itemId);
65
+ });
66
+
67
+ const byId = new Map<string, TItem>();
68
+ const children = new Map<string, TItem[]>();
69
+ const parentedIds = new Set<string>();
70
+
71
+ Array.from(includedIds).forEach((nodeId) => {
72
+ const taxonomyNode = taxonomy.nodesById[nodeId];
73
+ if (!taxonomyNode) return;
74
+
75
+ byId.set(nodeId, selectedById.get(nodeId) || {
76
+ shortId: taxonomyNode.shortId || nodeId,
77
+ label: taxonomyNode.label,
78
+ active: false,
79
+ hits: 0,
80
+ total: 0,
81
+ taxonomyId: taxonomyNode.id,
82
+ parentIds: taxonomyNode.parentIds || [],
83
+ isStructural: true,
84
+ } as TItem);
85
+ children.set(nodeId, []);
86
+ });
87
+
88
+ Array.from(includedIds).forEach((nodeId) => {
89
+ const current = byId.get(nodeId);
90
+ const taxonomyNode = taxonomy.nodesById[nodeId];
91
+ if (!current || !taxonomyNode) return;
92
+
93
+ (taxonomyNode.parentIds || [])
94
+ .filter((id) => byId.has(id))
95
+ .forEach((parentKey) => {
96
+ children.get(parentKey)?.push(current);
97
+ parentedIds.add(nodeId);
98
+ });
99
+ });
100
+
101
+ const roots = Array.from(byId.entries())
102
+ .filter(([nodeId]) => !parentedIds.has(nodeId))
103
+ .map(([, node]) => node);
104
+
105
+ sortItems(roots);
106
+ children.forEach((nodes) => sortItems(nodes));
107
+
108
+ return { roots, children };
109
+ }
110
+
111
+ const byId = new Map<string, TItem>();
112
+ const children = new Map<string, TItem[]>();
113
+ const roots: TItem[] = [];
114
+
115
+ items.forEach((item) => {
116
+ const key = item.taxonomyId || item.shortId;
117
+ byId.set(key, item);
118
+ children.set(key, []);
119
+ });
120
+
121
+ items.forEach((item) => {
122
+ const parentKeys = (item.parentIds || []).filter((id) => byId.has(id));
123
+ if (parentKeys.length === 0) {
124
+ roots.push(item);
125
+ return;
126
+ }
127
+
128
+ parentKeys.forEach((parentKey) => {
129
+ children.get(parentKey)?.push(item);
130
+ });
131
+ });
132
+
133
+ sortItems(roots);
134
+ children.forEach((nodes) => sortItems(nodes));
135
+
136
+ return { roots, children };
137
+ };