@witchcraft/editor 0.0.6 → 0.0.8

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.
Files changed (59) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/CodeBlockThemePicker.vue +0 -1
  3. package/dist/runtime/components/Commands.vue +1 -1
  4. package/dist/runtime/components/Editor.vue +1 -2
  5. package/dist/runtime/composables/useEditor.d.ts +1 -1
  6. package/dist/runtime/demo/App.vue +0 -1
  7. package/dist/runtime/pm/features/Blockquote/Blockquote.d.ts +1 -1
  8. package/dist/runtime/pm/features/Blocks/components/DragTreeHandle.d.vue.ts +26 -0
  9. package/dist/runtime/pm/features/Blocks/components/DragTreeHandle.vue +7 -0
  10. package/dist/runtime/pm/features/Blocks/components/DragTreeHandle.vue.d.ts +26 -0
  11. package/dist/runtime/pm/features/Blocks/components/ItemMenu.vue +1 -2
  12. package/dist/runtime/pm/features/Blocks/components/ItemNodeView.vue +0 -1
  13. package/dist/runtime/pm/features/Blocks/components/defaultItemMenu.d.ts +2 -2
  14. package/dist/runtime/pm/features/Blocks/composables/useNodeStates.d.ts +2 -2
  15. package/dist/runtime/pm/features/CodeBlock/CodeBlock.d.ts +1 -1
  16. package/dist/runtime/pm/features/CodeBlock/components/CodeBlockView.vue +0 -1
  17. package/dist/runtime/pm/features/CodeBlock/composables/useHighlightJsTheme.d.ts +3 -3
  18. package/dist/runtime/pm/features/CommandsMenus/components/CommandBar.vue +1 -1
  19. package/dist/runtime/pm/features/CommandsMenus/components/CommandBarItem.vue +1 -1
  20. package/dist/runtime/pm/features/CommandsMenus/components/CommandMenuGroup.vue +0 -1
  21. package/dist/runtime/pm/features/CommandsMenus/icons/HighlightIcon.vue +1 -1
  22. package/dist/runtime/pm/features/EmbeddedDocument/components/EmbeddedDocumentPicker.vue +0 -1
  23. package/dist/runtime/pm/features/EmbeddedDocument/components/EmbeddedNodeView.vue +0 -1
  24. package/dist/runtime/pm/features/FileLoader/components/FileLoaderNodeView.vue +0 -1
  25. package/dist/runtime/pm/features/HardBreak/HardBreak.d.ts +1 -1
  26. package/dist/runtime/pm/features/Link/components/BubbleMenuExternalLink.vue +1 -1
  27. package/dist/runtime/pm/features/Link/components/BubbleMenuInternalLink.vue +1 -1
  28. package/dist/runtime/pm/features/Link/components/BubbleMenuLink.vue +1 -1
  29. package/dist/runtime/pm/features/Menus/components/MarkMenuManager.vue +1 -1
  30. package/dist/runtime/pm/features/Tables/index.d.ts +5 -5
  31. package/dist/runtime/pm/generator.d.ts +82 -0
  32. package/dist/runtime/pm/generator.js +205 -0
  33. package/dist/runtime/pm/testSchema.d.ts +1 -1
  34. package/dist/runtime/pm/utils/generateRandomDoc.d.ts +23 -0
  35. package/dist/runtime/pm/utils/generateRandomDoc.js +83 -0
  36. package/dist/runtime/pm/utils/generateRandomTree.d.ts +50 -0
  37. package/dist/runtime/pm/utils/generateRandomTree.js +38 -0
  38. package/package.json +7 -5
  39. package/src/module.ts +1 -0
  40. package/src/runtime/components/CodeBlockThemePicker.vue +0 -1
  41. package/src/runtime/components/Editor.vue +0 -1
  42. package/src/runtime/demo/App.vue +0 -1
  43. package/src/runtime/pm/commands/changeAttrs.ts +1 -1
  44. package/src/runtime/pm/features/Blocks/Item.ts +1 -1
  45. package/src/runtime/pm/features/Blocks/commands/moveItem.ts +1 -1
  46. package/src/runtime/pm/features/Blocks/components/DragTreeHandle.vue +18 -14
  47. package/src/runtime/pm/features/Blocks/components/ItemMenu.vue +0 -1
  48. package/src/runtime/pm/features/Blocks/components/ItemNodeView.vue +0 -1
  49. package/src/runtime/pm/features/CodeBlock/components/CodeBlockView.vue +0 -1
  50. package/src/runtime/pm/features/CommandsMenus/components/CommandMenuGroup.vue +0 -1
  51. package/src/runtime/pm/features/EmbeddedDocument/Embedded.ts +1 -1
  52. package/src/runtime/pm/features/EmbeddedDocument/components/EmbeddedDocumentPicker.vue +0 -1
  53. package/src/runtime/pm/features/EmbeddedDocument/components/EmbeddedNodeView.vue +0 -1
  54. package/src/runtime/pm/features/FileLoader/components/FileLoaderNodeView.vue +0 -1
  55. package/src/runtime/pm/features/FileLoader/types.ts +2 -2
  56. package/src/runtime/pm/generator.ts +266 -0
  57. package/src/runtime/pm/schema.ts +1 -0
  58. package/src/runtime/pm/utils/generateRandomDoc.ts +140 -0
  59. package/src/runtime/pm/utils/generateRandomTree.ts +100 -0
@@ -0,0 +1,266 @@
1
+ import { faker } from "@faker-js/faker"
2
+ import type { Mark, Node } from "@tiptap/pm/model"
3
+ import { nanoid } from "nanoid"
4
+ import { builders } from "prosemirror-test-builder"
5
+
6
+ import { schema } from "./schema.js"
7
+
8
+ export const pm = builders(schema)
9
+
10
+ export type GeneratorConfigEntry<
11
+ TChildrenType extends "node" | "text" = "node" | "text",
12
+ TParentType extends "node" | "text" = "node" | "text",
13
+ TChildren extends TChildrenType extends "node" ? Node : Mark | string = TChildrenType extends "node" ? Node : Mark | string,
14
+ TParent extends TParentType extends "node" ? Node : Mark | string = TParentType extends "node" ? Node : Mark | string
15
+ > = {
16
+ parents: {
17
+ /**
18
+ * The type of the parent (node or "text") where "text" is a string or mark.
19
+ * Determines the call signature of the create function.
20
+ */
21
+ type: TParentType
22
+ /** Possible parent nodes that can have the given children. The children need not be direct descendants, see `ignoreValidation`. */
23
+ names: string[]
24
+ }
25
+ children: {
26
+ /**
27
+ * The type of children (node or "text") where "text" is a string or mark.
28
+ * Determines the call signature of the create function.
29
+ */
30
+ type: TChildrenType
31
+ /* Children types to generate. */
32
+ names?: string[]
33
+ }
34
+ /**
35
+ * Set to true to ignore all children mismatches, or set an array of children names to ignore.
36
+ *
37
+ * This is needed for creating "end" nodes that aren't valid names (e.g. `text` is not technically allowed since you can't do `builder.text()`).
38
+ *
39
+ * Or when the children are not direct children of the parent types as there is some in-between wrapper node that needs to be generated.
40
+ */
41
+ ignoreValidation?: boolean | string[]
42
+ /* Whether the parent listed is the root node. */
43
+ isRoot?: boolean
44
+ /**
45
+ * Creates the node. Can return undefined to "terminate" the branch being created, the node will be filtered out of the nodes passed to it's parent.
46
+ *
47
+ * If not set, a default `builder[parentType]({}, ...children)` will be used.
48
+ *
49
+ * Note also, it is not required to use the children. You can ignore them or use a subset or create a different one if needed (some node types *require* text and if your text not can return "" this can be a problem).
50
+ */
51
+ create?: (parent: string, children: TChildren[]) => TParent | undefined
52
+ }
53
+
54
+ export function getTextOrMarkLength(textOrMark: string | Mark) {
55
+ if (typeof textOrMark === "string") {
56
+ return textOrMark.length
57
+ }
58
+ const textNodes = (textOrMark as any).flat.map((_: any) => "text" in _ ? _.text.length : getTextOrMarkLength(_ as any)) as number[]
59
+ return textNodes.reduce((a, b) => a + b, 0)
60
+ }
61
+
62
+ export function createGeneratorConfig<
63
+ TChildrenType extends "node" | "text" = "node" | "text",
64
+ TParentType extends "node" | "text" = "node" | "text",
65
+ TChildren extends TChildrenType extends "node" ? Node : Mark | string = TChildrenType extends "node" ? Node : Mark | string,
66
+ TParent extends TParentType extends "node" ? Node : Mark | string = TParentType extends "node" ? Node : Mark | string
67
+ >(
68
+ config: GeneratorConfigEntry<TChildrenType, TParentType, TChildren, TParent>
69
+ ): GeneratorConfigEntry<TChildrenType, TParentType, TChildren, TParent> {
70
+ return config
71
+ }
72
+
73
+ function createPsuedoSentence() {
74
+ // sentence generated with string.sample (which contains all possible chars) instead of lorem.sentence
75
+ const sentenceLength = faker.number.int({ min: 0, max: 1000 })
76
+ const sentence = Array.from(
77
+ { length: sentenceLength },
78
+ () => faker.string.sample({ min: 0, max: 1000 })
79
+ ).join(" ")
80
+ return sentence
81
+ }
82
+
83
+ export const generatorConfig = [
84
+ createGeneratorConfig({
85
+ isRoot: true,
86
+ parents: { type: "node", names: ["doc"] },
87
+ ignoreValidation: true,
88
+ children: { type: "node", names: ["item"] },
89
+ create: (_parent, children) => {
90
+ if (!children || children.length === 0) {
91
+ return pm.doc(pm.list(pm.item({ blockId: nanoid(10) }, pm.paragraph({}, ""))))
92
+ }
93
+ return pm.doc(pm.list(...children))
94
+ }
95
+ }),
96
+ createGeneratorConfig({
97
+ parents: { type: "node", names: ["list"] },
98
+ children: { type: "node", names: ["item"] },
99
+ create: (_parent, children) => {
100
+ if (!children || children.length === 0) {
101
+ return pm.list(pm.item({ blockId: nanoid(10) }, pm.paragraph("")))
102
+ }
103
+ return pm.list(...children)
104
+ }
105
+ }),
106
+ createGeneratorConfig({
107
+ parents: { type: "node", names: ["item"] },
108
+ children: { type: "node", names: [
109
+ "paragraph",
110
+ "heading",
111
+ "codeBlock",
112
+ "embeddedDoc",
113
+ "iframe",
114
+ "table",
115
+ "image",
116
+ "blockquote"
117
+ ] },
118
+ create: (_parent, children) => {
119
+ if (!children || children.length === 0) {
120
+ return pm.item({ blockId: nanoid(10) }, pm.paragraph(""))
121
+ }
122
+ return pm.item(
123
+ { blockId: nanoid(10) },
124
+ children[0]!,
125
+ ...(children.length > 1
126
+ ? [pm.list({},
127
+ ...children.slice(1).map(_ =>
128
+ pm.item({ blockId: nanoid(10) }, _)
129
+ )
130
+ )]
131
+ : []
132
+ ) as any
133
+ )
134
+ }
135
+ }),
136
+ createGeneratorConfig({
137
+ parents: { type: "node", names: ["blockquote"] },
138
+ children: { type: "node", names: ["paragraph", "cite"] },
139
+ create: (_parent, children) => {
140
+ const parTypes = []
141
+ const citeTypes = []
142
+ for (const child of children) {
143
+ if (child.type.name === "cite") {
144
+ citeTypes.push(child)
145
+ } else {
146
+ parTypes.push(child)
147
+ }
148
+ }
149
+ if (parTypes.length === 0) {
150
+ return pm.blockquote({}, pm.paragraph(""))
151
+ } else {
152
+ const someCite = citeTypes.find(_ => _.textContent.length > 0) ?? pm.cite({}, createPsuedoSentence())
153
+ return pm.blockquote({}, ...parTypes, someCite)
154
+ }
155
+ }
156
+ }),
157
+ createGeneratorConfig({
158
+ parents: { type: "node", names: ["table"] },
159
+ children: { type: "node", names: [
160
+ "tableHeader",
161
+ "tableCell"
162
+ ] },
163
+ ignoreValidation: true, // we're handling the in-between tableRow nodes
164
+ create: (_parent, children) => {
165
+ const headerType = []
166
+ const cellType = []
167
+
168
+ for (const child of children) {
169
+ if (child.type.name === "tableHeader") {
170
+ headerType.push(child)
171
+ } else if (child.type.name === "tableCell") {
172
+ cellType.push(child)
173
+ }
174
+ }
175
+ const colNum = headerType.length
176
+
177
+ if (colNum === 0) {
178
+ return pm.table({}, pm.tableRow({}, pm.tableCell(pm.paragraph(""))))
179
+ }
180
+ const rowCount = Math.ceil(cellType.length / colNum)
181
+ const rows: Node[] = []
182
+ for (let i = 0; i < rowCount; i++) {
183
+ rows.push(pm.tableRow({}, ...cellType.slice(i * colNum, (i + 1) * colNum)))
184
+ }
185
+
186
+ return pm.table({}, pm.tableRow({}, ...headerType), ...rows)
187
+ }
188
+ }),
189
+ createGeneratorConfig({
190
+ parents: { type: "node", names: ["tableHeader", "tableCell"] },
191
+ children: { type: "node", names: ["paragraph"] },
192
+ create: (_parent, children) => {
193
+ if (!children || children.length === 0) {
194
+ return pm.tableCell(pm.paragraph(""))
195
+ }
196
+ return pm.tableCell(...children.slice(0, 1))
197
+ }
198
+ }),
199
+ createGeneratorConfig({
200
+ parents: { type: "node", names: ["paragraph", "heading", "codeBlock", "cite", "heading"] },
201
+ children: { type: "text", names: [
202
+ "bold",
203
+ "italic",
204
+ "underline",
205
+ "strike",
206
+ "subscript",
207
+ "superscript",
208
+ "code",
209
+ "link"
210
+ ] },
211
+ create(parent, children) {
212
+ if (parent === "heading") {
213
+ return pm[parent]({ level: 1 }, ...children as any)
214
+ }
215
+ if (parent === "cite") {
216
+ // citation must have SOME text
217
+ return pm[parent]({}, faker.lorem.sentence())
218
+ }
219
+ if (parent === "paragraph") {
220
+ const someIndex = faker.number.int({ min: 0, max: children.length })
221
+ children.splice(someIndex, 0, pm.hardBreak() as any)
222
+ }
223
+
224
+ return pm[parent]({}, ...children as any)
225
+ }
226
+ }),
227
+ createGeneratorConfig({
228
+ parents: { type: "text", names: [
229
+ "bold",
230
+ "italic",
231
+ "underline",
232
+ "strike",
233
+ "subscript",
234
+ "superscript",
235
+ "link"
236
+ ]
237
+ },
238
+ // note the addition of text
239
+ children: { type: "text", names: [
240
+ "bold",
241
+ "italic",
242
+ "underline",
243
+ "strike",
244
+ "subscript",
245
+ "superscript",
246
+ "link",
247
+ "text"
248
+ ] },
249
+ ignoreValidation: ["text"] // text is not a real node
250
+ // create: (parent, children) => {
251
+ // return pm[parent]({}, ...children as any) as any
252
+ // }
253
+ }),
254
+ createGeneratorConfig({
255
+ ignoreValidation: true, // text is not a real node
256
+ parents: { type: "text", names: ["code"] },
257
+ children: { type: "text", names: ["text"] }
258
+ }),
259
+ createGeneratorConfig({
260
+ parents: { type: "text", names: ["text"] },
261
+ children: { type: "text" },
262
+ create: _parent => {
263
+ return createPsuedoSentence()
264
+ }
265
+ })
266
+ ]
@@ -126,3 +126,4 @@ const _schema = getSchema(extensions) as Schema<
126
126
  | MarkHighlightName
127
127
  >
128
128
  export const schema = _schema
129
+
@@ -0,0 +1,140 @@
1
+ import { unreachable } from "@alanscodelog/utils/unreachable"
2
+ import { faker } from "@faker-js/faker"
3
+ import type { Mark, MarkType, Node } from "@tiptap/pm/model"
4
+ import type { builders } from "prosemirror-test-builder"
5
+
6
+ import { generateRandomTree } from "./generateRandomTree.js"
7
+
8
+ import type { GeneratorConfigEntry } from "../generator.js"
9
+ import { generatorConfig } from "../generator.js"
10
+
11
+ /**
12
+ * Generates a random doc using faker js.
13
+ *
14
+ * A config describing how to create the doc is required. One is available under `pm/generator`.
15
+ *
16
+ * @experimental
17
+ */
18
+ export function generateRandomDoc<
19
+ TBuilder extends ReturnType<typeof builders>,
20
+ TData extends string
21
+ >(
22
+ builder: TBuilder,
23
+ config: GeneratorConfigEntry<any, any, any, any>[] = generatorConfig,
24
+ treeOptions?: Parameters<typeof generateRandomTree>[1],
25
+ {
26
+ checkAfterNodeCreation = false
27
+ }: {
28
+ /**
29
+ * Checks if every returned node is valid for debugging invalid generator configs.
30
+ *
31
+ * A check is always done on the root node at the end regardless of this option.
32
+ *
33
+ * It is very slow (each check, rechecks children).
34
+ *
35
+ * @default false
36
+ */
37
+ checkAfterNodeCreation?: boolean
38
+ } = {}
39
+ ): Node {
40
+ const schema = builder.schema
41
+
42
+ const map: Record<string, GeneratorConfigEntry<any, any, any, any>> = {}
43
+ function schemaFilter(name: string) {
44
+ return name !== "text" && name !== schema.topNodeType.name
45
+ }
46
+
47
+ for (const entry of config) {
48
+ for (const node of entry.parents.names) {
49
+ if (node === "") throw new Error(`Empty node name ""`)
50
+ const type = entry.parents.type === "node" ? schema.nodes[node] : schema.marks[node]
51
+ if (type === undefined && node !== "text") {
52
+ throw new Error(`${node} (on entry of type ${entry.parents.type}) not found in schema. Valid names are ${Object.keys(entry.parents.type === "node" ? schema.nodes : schema.marks)}`)
53
+ }
54
+
55
+ for (const node of entry.parents.names) {
56
+ const subEntry = { ...entry }
57
+ if (entry.ignoreValidation === undefined || entry.ignoreValidation !== true) {
58
+ const childrenToValidate = (entry.children.names ?? []).filter(_ => !Array.isArray(entry.ignoreValidation) || !entry.ignoreValidation.includes(_))
59
+ for (const child of childrenToValidate) {
60
+ if (child === "") throw new Error(`Empty node name ""`)
61
+ if (entry.children.type === "node") {
62
+ const childType = schema.nodes[child]
63
+ const possibleNames = [childType.name, ...((childType.spec.group ?? "").split(" ").filter(e => e !== ""))]
64
+ const isAllowedChild = type.spec.content && possibleNames.some(n => type.spec.content!.includes(n))
65
+ // we don't use contentMatch.matchType because
66
+ // it can give false negatives when the content expression
67
+ // has multiple types like type1+ type2+,
68
+ // it will only return true for type1 in those cases
69
+ if (!(isAllowedChild && schemaFilter(childType.name))) {
70
+ throw new Error(`Node ${node} cannot contain child of type ${child}`)
71
+ }
72
+ } else {
73
+ const childType = schema.marks[child]
74
+ if (entry.parents.type === "node") {
75
+ // not sure why this is returning false when it shouldn't (e.g. paragraph can't contain bold)
76
+ // if (!(type as NodeType).allowsMarkType(childType)) {
77
+ // throw new Error(`Node ${node} cannot contain mark child of type ${child}`)
78
+ // }
79
+ } else {
80
+ const possibleNames = [childType.name, ...((childType.spec.group ?? "").split(" ").filter(e => e !== ""))]
81
+ const t = type as MarkType
82
+ const isExcluded = t.spec.excludes !== undefined
83
+ && possibleNames.some(n => {
84
+ // _ this means exclude everything in prosemirror
85
+ return t.spec.excludes!.includes(n)
86
+ || t.spec.excludes!.includes("_")
87
+ })
88
+ if (isExcluded) {
89
+ throw new Error(`Mark ${node} cannot contain mark child of type ${child}`)
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ map[node] = { ...subEntry }
96
+ }
97
+ }
98
+ }
99
+
100
+
101
+ const children = generateRandomTree<Node | Mark | string, TData>({
102
+ parentData: (data?: TData): TData => {
103
+ if (data === "") return "" as TData
104
+ if (!data) unreachable()
105
+ if (!map[data] || !map[data].children.names || map[data].children.names.length === 0) {
106
+ // we must prematurely end the branch as the node can contain no children
107
+ return "" as TData
108
+ } else {
109
+ const picked = faker.helpers.arrayElement(map[data].children.names)
110
+ return picked as TData
111
+ }
112
+ },
113
+ createNode: (children, _isLeaf, data): Node | Mark | string | undefined => {
114
+ if (data === "") return undefined
115
+ if (!data) unreachable()
116
+ const type = schema.nodes[data]
117
+ // type allows no children or isn't configured to generate children
118
+ if (!type) return undefined
119
+
120
+ const parent = map[data]?.create?.(data, children as any)
121
+ ?? builder[data]({}, ...children as any)
122
+
123
+ if (checkAfterNodeCreation) {
124
+ ;(parent as any)?.check?.()
125
+ }
126
+
127
+ return parent
128
+ }
129
+ }, {
130
+ ...treeOptions,
131
+ initialData: schema.topNodeType.name as TData
132
+ })
133
+
134
+ const doc = map[schema.topNodeType.name]?.create?.(schema.topNodeType.name as any, children as any)
135
+ ?? builder[schema.topNodeType.name]({}, ...children as any)
136
+
137
+ ;(doc as any).check?.()
138
+ return doc as Node
139
+ }
140
+
@@ -0,0 +1,100 @@
1
+ import { faker } from "@faker-js/faker"
2
+ /**
3
+ * Generates a random tree structure where the probability of generating children
4
+ * decreases linearly with depth, resulting in a tree that is bushy at the top
5
+ * and sparse towards the maximum depth.
6
+ *
7
+ * This function is agnostic to the actual node structure, relying on the caller-provided
8
+ * callbacks to create and link nodes.
9
+ *
10
+ * It uses faker.js for randomness (even on the option parameters).
11
+ *
12
+ * See {@link generateRandomDoc} for an example of how to use it.
13
+ *
14
+ * @experimental
15
+ */
16
+ export function generateRandomTree<T, TData>(
17
+ {
18
+ createNode,
19
+ parentData
20
+ }: {
21
+ /** A function that creates a new node (type T) given its depth. */
22
+ createNode: (children: T[], isLeaf: boolean, parentData?: TData) => T | undefined
23
+ /**
24
+ * A function that is called before creating a a node or it's children whose result is passed to both. This allows creating children that will be compatible with the parent (via this same function, it is passed it's parent).
25
+ *
26
+ * For example, we could randomly generate a parent type. It's then passed both to the children, to limit the types of children nodes, and to the parent so it can actually create it.
27
+ */
28
+ parentData?: (parentData?: TData) => TData
29
+ },
30
+ {
31
+ rootNodes: rootNodes = faker.number.int({ min: 0, max: 5 }),
32
+ depth = faker.number.int({ min: 0, max: 5 }),
33
+ minChildren = 0,
34
+ maxInitialChildren = 10,
35
+ initialData
36
+ }: {
37
+ /**
38
+ * The exact number of nodes at depth 0.
39
+ *
40
+ * @default faker.number.int({ min: 0, max: 5 })
41
+ */
42
+ rootNodes?: number
43
+ /**
44
+ * The maximum depth of the tree (0-indexed). Nodes at this depth will have 0 children.
45
+ *
46
+ * @default faker.number.int({ min: 0, max: 5 })
47
+ */
48
+ depth?: number
49
+ /**
50
+ * The absolute minimum number of children any node can have.
51
+ *
52
+ * @default 0
53
+ */
54
+ minChildren?: number
55
+ /**
56
+ * The maximum children a node can have at depth 0. This value scales down as depth increases.
57
+ *
58
+ * @default 10
59
+ */
60
+ maxInitialChildren?: number
61
+ initialData?: TData
62
+ } = {}
63
+ ): T[] {
64
+ const generateChildren = (currentDepth: number, childCount: number, pData?: TData): T[] => {
65
+ const res: T[] = []
66
+ for (let i = 0; i < childCount; i++) {
67
+ const numChildren = calculateNumChildren(depth, currentDepth, minChildren, maxInitialChildren)
68
+
69
+ const data = parentData?.(pData) ?? undefined
70
+ const parent = createNode(
71
+ generateChildren(currentDepth + 1, numChildren, data),
72
+ numChildren === 0,
73
+ data
74
+ )
75
+ if (parent === undefined) continue
76
+ res.push(parent)
77
+ }
78
+ return res
79
+ }
80
+
81
+ return generateChildren(0, rootNodes, initialData)
82
+ }
83
+
84
+ function calculateNumChildren(
85
+ depth: number,
86
+ currentDepth: number,
87
+ minChildren: number,
88
+ maxInitialChildren: number
89
+ ): number {
90
+ const decayFactor = depth > 0 ? (depth - currentDepth) / depth : 0
91
+ const maxAtDepth = minChildren + (maxInitialChildren - minChildren) * decayFactor
92
+ const maxChildrenAtDepth = Math.max(minChildren, Math.floor(maxAtDepth))
93
+
94
+ let numChildren = 0
95
+
96
+ if (currentDepth < depth) {
97
+ numChildren = faker.number.int({ min: minChildren, max: maxChildrenAtDepth })
98
+ }
99
+ return numChildren
100
+ }