@witchcraft/editor 0.0.6 → 0.0.7

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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witchcraftEditor",
3
3
  "configKey": "witchcraftEditor",
4
- "version": "0.0.6",
4
+ "version": "0.0.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -99,9 +99,16 @@
99
99
  </div>
100
100
  </template>
101
101
 
102
+ <script>
103
+ export default {};
104
+ </script>
105
+
102
106
  <script setup>
103
107
  import { twMerge } from "@witchcraft/ui/utils/twMerge";
104
108
  import { onMounted, onUnmounted, ref, useAttrs } from "vue";
109
+ defineOptions({
110
+ name: "DragTreeHandle"
111
+ });
105
112
  const $attrs = useAttrs();
106
113
  defineProps({
107
114
  hasChildren: { type: Boolean, required: true },
@@ -0,0 +1,82 @@
1
+ import type { Mark, Node } from "@tiptap/pm/model";
2
+ export declare const pm: {
3
+ schema: import("@tiptap/pm/model").Schema<"paragraph" | "item" | "codeBlock" | "heading" | "iframe" | "image" | "table" | "text" | "blockquote" | "cite" | "list" | "tableCell" | "doc" | "embeddedDoc" | "tableRow" | "tableHeader", "bold" | "code" | "italic" | "strike" | "subscript" | "superscript" | "underline" | "hardBreak" | "highlight">;
4
+ } & {
5
+ readonly [x: string]: import("prosemirror-test-builder").NodeBuilder;
6
+ readonly paragraph: import("prosemirror-test-builder").NodeBuilder;
7
+ readonly item: import("prosemirror-test-builder").NodeBuilder;
8
+ readonly codeBlock: import("prosemirror-test-builder").NodeBuilder;
9
+ readonly heading: import("prosemirror-test-builder").NodeBuilder;
10
+ readonly iframe: import("prosemirror-test-builder").NodeBuilder;
11
+ readonly image: import("prosemirror-test-builder").NodeBuilder;
12
+ readonly table: import("prosemirror-test-builder").NodeBuilder;
13
+ readonly text: import("prosemirror-test-builder").NodeBuilder;
14
+ readonly blockquote: import("prosemirror-test-builder").NodeBuilder;
15
+ readonly cite: import("prosemirror-test-builder").NodeBuilder;
16
+ readonly list: import("prosemirror-test-builder").NodeBuilder;
17
+ readonly tableCell: import("prosemirror-test-builder").NodeBuilder;
18
+ readonly doc: import("prosemirror-test-builder").NodeBuilder;
19
+ readonly embeddedDoc: import("prosemirror-test-builder").NodeBuilder;
20
+ readonly tableRow: import("prosemirror-test-builder").NodeBuilder;
21
+ readonly tableHeader: import("prosemirror-test-builder").NodeBuilder;
22
+ } & {
23
+ readonly [x: string]: import("prosemirror-test-builder").MarkBuilder;
24
+ readonly bold: import("prosemirror-test-builder").MarkBuilder;
25
+ readonly code: import("prosemirror-test-builder").MarkBuilder;
26
+ readonly italic: import("prosemirror-test-builder").MarkBuilder;
27
+ readonly strike: import("prosemirror-test-builder").MarkBuilder;
28
+ readonly subscript: import("prosemirror-test-builder").MarkBuilder;
29
+ readonly superscript: import("prosemirror-test-builder").MarkBuilder;
30
+ readonly underline: import("prosemirror-test-builder").MarkBuilder;
31
+ readonly hardBreak: import("prosemirror-test-builder").MarkBuilder;
32
+ readonly highlight: import("prosemirror-test-builder").MarkBuilder;
33
+ } & {
34
+ [name: string]: import("prosemirror-test-builder").NodeBuilder | import("prosemirror-test-builder").MarkBuilder;
35
+ };
36
+ export type GeneratorConfigEntry<TChildrenType extends "node" | "text" = "node" | "text", TParentType extends "node" | "text" = "node" | "text", TChildren extends TChildrenType extends "node" ? Node : Mark | string = TChildrenType extends "node" ? Node : Mark | string, TParent extends TParentType extends "node" ? Node : Mark | string = TParentType extends "node" ? Node : Mark | string> = {
37
+ parents: {
38
+ /**
39
+ * The type of the parent (node or "text") where "text" is a string or mark.
40
+ * Determines the call signature of the create function.
41
+ */
42
+ type: TParentType;
43
+ /** Possible parent nodes that can have the given children. The children need not be direct descendants, see `ignoreValidation`. */
44
+ names: string[];
45
+ };
46
+ children: {
47
+ /**
48
+ * The type of children (node or "text") where "text" is a string or mark.
49
+ * Determines the call signature of the create function.
50
+ */
51
+ type: TChildrenType;
52
+ names?: string[];
53
+ };
54
+ /**
55
+ * Set to true to ignore all children mismatches, or set an array of children names to ignore.
56
+ *
57
+ * 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()`).
58
+ *
59
+ * 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.
60
+ */
61
+ ignoreValidation?: boolean | string[];
62
+ isRoot?: boolean;
63
+ /**
64
+ * 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.
65
+ *
66
+ * If not set, a default `builder[parentType]({}, ...children)` will be used.
67
+ *
68
+ * 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).
69
+ */
70
+ create?: (parent: string, children: TChildren[]) => TParent | undefined;
71
+ };
72
+ export declare function getTextOrMarkLength(textOrMark: string | Mark): number;
73
+ export declare function createGeneratorConfig<TChildrenType extends "node" | "text" = "node" | "text", TParentType extends "node" | "text" = "node" | "text", TChildren extends TChildrenType extends "node" ? Node : Mark | string = TChildrenType extends "node" ? Node : Mark | string, TParent extends TParentType extends "node" ? Node : Mark | string = TParentType extends "node" ? Node : Mark | string>(config: GeneratorConfigEntry<TChildrenType, TParentType, TChildren, TParent>): GeneratorConfigEntry<TChildrenType, TParentType, TChildren, TParent>;
74
+ export declare const generatorConfig: (GeneratorConfigEntry<"node", "node", Node, Node & {
75
+ tag: {
76
+ [tag: string]: number;
77
+ };
78
+ }> | GeneratorConfigEntry<"text", "node", string | Mark, Node & {
79
+ tag: {
80
+ [tag: string]: number;
81
+ };
82
+ }> | GeneratorConfigEntry<"text", "text", string | Mark, string | Mark>)[];
@@ -0,0 +1,205 @@
1
+ import { faker } from "@faker-js/faker";
2
+ import { nanoid } from "nanoid";
3
+ import { builders } from "prosemirror-test-builder";
4
+ import { schema } from "./schema.js";
5
+ export const pm = builders(schema);
6
+ export function getTextOrMarkLength(textOrMark) {
7
+ if (typeof textOrMark === "string") {
8
+ return textOrMark.length;
9
+ }
10
+ const textNodes = textOrMark.flat.map((_) => "text" in _ ? _.text.length : getTextOrMarkLength(_));
11
+ return textNodes.reduce((a, b) => a + b, 0);
12
+ }
13
+ export function createGeneratorConfig(config) {
14
+ return config;
15
+ }
16
+ function createPsuedoSentence() {
17
+ const sentenceLength = faker.number.int({ min: 0, max: 1e3 });
18
+ const sentence = Array.from(
19
+ { length: sentenceLength },
20
+ () => faker.string.sample({ min: 0, max: 1e3 })
21
+ ).join(" ");
22
+ return sentence;
23
+ }
24
+ export const generatorConfig = [
25
+ createGeneratorConfig({
26
+ isRoot: true,
27
+ parents: { type: "node", names: ["doc"] },
28
+ ignoreValidation: true,
29
+ children: { type: "node", names: ["item"] },
30
+ create: (_parent, children) => {
31
+ if (!children || children.length === 0) {
32
+ return pm.doc(pm.list(pm.item({ blockId: nanoid(10) }, pm.paragraph({}, ""))));
33
+ }
34
+ return pm.doc(pm.list(...children));
35
+ }
36
+ }),
37
+ createGeneratorConfig({
38
+ parents: { type: "node", names: ["list"] },
39
+ children: { type: "node", names: ["item"] },
40
+ create: (_parent, children) => {
41
+ if (!children || children.length === 0) {
42
+ return pm.list(pm.item({ blockId: nanoid(10) }, pm.paragraph("")));
43
+ }
44
+ return pm.list(...children);
45
+ }
46
+ }),
47
+ createGeneratorConfig({
48
+ parents: { type: "node", names: ["item"] },
49
+ children: { type: "node", names: [
50
+ "paragraph",
51
+ "heading",
52
+ "codeBlock",
53
+ "embeddedDoc",
54
+ "iframe",
55
+ "table",
56
+ "image",
57
+ "blockquote"
58
+ ] },
59
+ create: (_parent, children) => {
60
+ if (!children || children.length === 0) {
61
+ return pm.item({ blockId: nanoid(10) }, pm.paragraph(""));
62
+ }
63
+ return pm.item(
64
+ { blockId: nanoid(10) },
65
+ children[0],
66
+ ...children.length > 1 ? [pm.list(
67
+ {},
68
+ ...children.slice(1).map(
69
+ (_) => pm.item({ blockId: nanoid(10) }, _)
70
+ )
71
+ )] : []
72
+ );
73
+ }
74
+ }),
75
+ createGeneratorConfig({
76
+ parents: { type: "node", names: ["blockquote"] },
77
+ children: { type: "node", names: ["paragraph", "cite"] },
78
+ create: (_parent, children) => {
79
+ const parTypes = [];
80
+ const citeTypes = [];
81
+ for (const child of children) {
82
+ if (child.type.name === "cite") {
83
+ citeTypes.push(child);
84
+ } else {
85
+ parTypes.push(child);
86
+ }
87
+ }
88
+ if (parTypes.length === 0) {
89
+ return pm.blockquote({}, pm.paragraph(""));
90
+ } else {
91
+ const someCite = citeTypes.find((_) => _.textContent.length > 0) ?? pm.cite({}, createPsuedoSentence());
92
+ return pm.blockquote({}, ...parTypes, someCite);
93
+ }
94
+ }
95
+ }),
96
+ createGeneratorConfig({
97
+ parents: { type: "node", names: ["table"] },
98
+ children: { type: "node", names: [
99
+ "tableHeader",
100
+ "tableCell"
101
+ ] },
102
+ ignoreValidation: true,
103
+ // we're handling the in-between tableRow nodes
104
+ create: (_parent, children) => {
105
+ const headerType = [];
106
+ const cellType = [];
107
+ for (const child of children) {
108
+ if (child.type.name === "tableHeader") {
109
+ headerType.push(child);
110
+ } else if (child.type.name === "tableCell") {
111
+ cellType.push(child);
112
+ }
113
+ }
114
+ const colNum = headerType.length;
115
+ if (colNum === 0) {
116
+ return pm.table({}, pm.tableRow({}, pm.tableCell(pm.paragraph(""))));
117
+ }
118
+ const rowCount = Math.ceil(cellType.length / colNum);
119
+ const rows = [];
120
+ for (let i = 0; i < rowCount; i++) {
121
+ rows.push(pm.tableRow({}, ...cellType.slice(i * colNum, (i + 1) * colNum)));
122
+ }
123
+ return pm.table({}, pm.tableRow({}, ...headerType), ...rows);
124
+ }
125
+ }),
126
+ createGeneratorConfig({
127
+ parents: { type: "node", names: ["tableHeader", "tableCell"] },
128
+ children: { type: "node", names: ["paragraph"] },
129
+ create: (_parent, children) => {
130
+ if (!children || children.length === 0) {
131
+ return pm.tableCell(pm.paragraph(""));
132
+ }
133
+ return pm.tableCell(...children.slice(0, 1));
134
+ }
135
+ }),
136
+ createGeneratorConfig({
137
+ parents: { type: "node", names: ["paragraph", "heading", "codeBlock", "cite", "heading"] },
138
+ children: { type: "text", names: [
139
+ "bold",
140
+ "italic",
141
+ "underline",
142
+ "strike",
143
+ "subscript",
144
+ "superscript",
145
+ "code",
146
+ "link"
147
+ ] },
148
+ create(parent, children) {
149
+ if (parent === "heading") {
150
+ return pm[parent]({ level: 1 }, ...children);
151
+ }
152
+ if (parent === "cite") {
153
+ return pm[parent]({}, faker.lorem.sentence());
154
+ }
155
+ if (parent === "paragraph") {
156
+ const someIndex = faker.number.int({ min: 0, max: children.length });
157
+ children.splice(someIndex, 0, pm.hardBreak());
158
+ }
159
+ return pm[parent]({}, ...children);
160
+ }
161
+ }),
162
+ createGeneratorConfig({
163
+ parents: {
164
+ type: "text",
165
+ names: [
166
+ "bold",
167
+ "italic",
168
+ "underline",
169
+ "strike",
170
+ "subscript",
171
+ "superscript",
172
+ "link"
173
+ ]
174
+ },
175
+ // note the addition of text
176
+ children: { type: "text", names: [
177
+ "bold",
178
+ "italic",
179
+ "underline",
180
+ "strike",
181
+ "subscript",
182
+ "superscript",
183
+ "link",
184
+ "text"
185
+ ] },
186
+ ignoreValidation: ["text"]
187
+ // text is not a real node
188
+ // create: (parent, children) => {
189
+ // return pm[parent]({}, ...children as any) as any
190
+ // }
191
+ }),
192
+ createGeneratorConfig({
193
+ ignoreValidation: true,
194
+ // text is not a real node
195
+ parents: { type: "text", names: ["code"] },
196
+ children: { type: "text", names: ["text"] }
197
+ }),
198
+ createGeneratorConfig({
199
+ parents: { type: "text", names: ["text"] },
200
+ children: { type: "text" },
201
+ create: (_parent) => {
202
+ return createPsuedoSentence();
203
+ }
204
+ })
205
+ ];
@@ -0,0 +1,23 @@
1
+ import type { Node } from "@tiptap/pm/model";
2
+ import type { builders } from "prosemirror-test-builder";
3
+ import { generateRandomTree } from "./generateRandomTree.js";
4
+ import type { GeneratorConfigEntry } from "../generator.js";
5
+ /**
6
+ * Generates a random doc using faker js.
7
+ *
8
+ * A config describing how to create the doc is required. One is available under `pm/generator`.
9
+ *
10
+ * @experimental
11
+ */
12
+ export declare function generateRandomDoc<TBuilder extends ReturnType<typeof builders>, TData extends string>(builder: TBuilder, config?: GeneratorConfigEntry<any, any, any, any>[], treeOptions?: Parameters<typeof generateRandomTree>[1], { checkAfterNodeCreation }?: {
13
+ /**
14
+ * Checks if every returned node is valid for debugging invalid generator configs.
15
+ *
16
+ * A check is always done on the root node at the end regardless of this option.
17
+ *
18
+ * It is very slow (each check, rechecks children).
19
+ *
20
+ * @default false
21
+ */
22
+ checkAfterNodeCreation?: boolean;
23
+ }): Node;
@@ -0,0 +1,83 @@
1
+ import { unreachable } from "@alanscodelog/utils/unreachable";
2
+ import { faker } from "@faker-js/faker";
3
+ import { generateRandomTree } from "./generateRandomTree.js";
4
+ import { generatorConfig } from "../generator.js";
5
+ export function generateRandomDoc(builder, config = generatorConfig, treeOptions, {
6
+ checkAfterNodeCreation = false
7
+ } = {}) {
8
+ const schema = builder.schema;
9
+ const map = {};
10
+ function schemaFilter(name) {
11
+ return name !== "text" && name !== schema.topNodeType.name;
12
+ }
13
+ for (const entry of config) {
14
+ for (const node of entry.parents.names) {
15
+ if (node === "") throw new Error(`Empty node name ""`);
16
+ const type = entry.parents.type === "node" ? schema.nodes[node] : schema.marks[node];
17
+ if (type === void 0 && node !== "text") {
18
+ 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)}`);
19
+ }
20
+ for (const node2 of entry.parents.names) {
21
+ const subEntry = { ...entry };
22
+ if (entry.ignoreValidation === void 0 || entry.ignoreValidation !== true) {
23
+ const childrenToValidate = (entry.children.names ?? []).filter((_) => !Array.isArray(entry.ignoreValidation) || !entry.ignoreValidation.includes(_));
24
+ for (const child of childrenToValidate) {
25
+ if (child === "") throw new Error(`Empty node name ""`);
26
+ if (entry.children.type === "node") {
27
+ const childType = schema.nodes[child];
28
+ const possibleNames = [childType.name, ...(childType.spec.group ?? "").split(" ").filter((e) => e !== "")];
29
+ const isAllowedChild = type.spec.content && possibleNames.some((n) => type.spec.content.includes(n));
30
+ if (!(isAllowedChild && schemaFilter(childType.name))) {
31
+ throw new Error(`Node ${node2} cannot contain child of type ${child}`);
32
+ }
33
+ } else {
34
+ const childType = schema.marks[child];
35
+ if (entry.parents.type === "node") {
36
+ } else {
37
+ const possibleNames = [childType.name, ...(childType.spec.group ?? "").split(" ").filter((e) => e !== "")];
38
+ const t = type;
39
+ const isExcluded = t.spec.excludes !== void 0 && possibleNames.some((n) => {
40
+ return t.spec.excludes.includes(n) || t.spec.excludes.includes("_");
41
+ });
42
+ if (isExcluded) {
43
+ throw new Error(`Mark ${node2} cannot contain mark child of type ${child}`);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ map[node2] = { ...subEntry };
50
+ }
51
+ }
52
+ }
53
+ const children = generateRandomTree({
54
+ parentData: (data) => {
55
+ if (data === "") return "";
56
+ if (!data) unreachable();
57
+ if (!map[data] || !map[data].children.names || map[data].children.names.length === 0) {
58
+ return "";
59
+ } else {
60
+ const picked = faker.helpers.arrayElement(map[data].children.names);
61
+ return picked;
62
+ }
63
+ },
64
+ createNode: (children2, _isLeaf, data) => {
65
+ if (data === "") return void 0;
66
+ if (!data) unreachable();
67
+ const type = schema.nodes[data];
68
+ if (!type) return void 0;
69
+ const parent = map[data]?.create?.(data, children2) ?? builder[data]({}, ...children2);
70
+ if (checkAfterNodeCreation) {
71
+ ;
72
+ parent?.check?.();
73
+ }
74
+ return parent;
75
+ }
76
+ }, {
77
+ ...treeOptions,
78
+ initialData: schema.topNodeType.name
79
+ });
80
+ const doc = map[schema.topNodeType.name]?.create?.(schema.topNodeType.name, children) ?? builder[schema.topNodeType.name]({}, ...children);
81
+ doc.check?.();
82
+ return doc;
83
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generates a random tree structure where the probability of generating children
3
+ * decreases linearly with depth, resulting in a tree that is bushy at the top
4
+ * and sparse towards the maximum depth.
5
+ *
6
+ * This function is agnostic to the actual node structure, relying on the caller-provided
7
+ * callbacks to create and link nodes.
8
+ *
9
+ * It uses faker.js for randomness (even on the option parameters).
10
+ *
11
+ * See {@link generateRandomDoc} for an example of how to use it.
12
+ *
13
+ * @experimental
14
+ */
15
+ export declare function generateRandomTree<T, TData>({ createNode, parentData }: {
16
+ /** A function that creates a new node (type T) given its depth. */
17
+ createNode: (children: T[], isLeaf: boolean, parentData?: TData) => T | undefined;
18
+ /**
19
+ * 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).
20
+ *
21
+ * 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.
22
+ */
23
+ parentData?: (parentData?: TData) => TData;
24
+ }, { rootNodes: rootNodes, depth, minChildren, maxInitialChildren, initialData }?: {
25
+ /**
26
+ * The exact number of nodes at depth 0.
27
+ *
28
+ * @default faker.number.int({ min: 0, max: 5 })
29
+ */
30
+ rootNodes?: number;
31
+ /**
32
+ * The maximum depth of the tree (0-indexed). Nodes at this depth will have 0 children.
33
+ *
34
+ * @default faker.number.int({ min: 0, max: 5 })
35
+ */
36
+ depth?: number;
37
+ /**
38
+ * The absolute minimum number of children any node can have.
39
+ *
40
+ * @default 0
41
+ */
42
+ minChildren?: number;
43
+ /**
44
+ * The maximum children a node can have at depth 0. This value scales down as depth increases.
45
+ *
46
+ * @default 10
47
+ */
48
+ maxInitialChildren?: number;
49
+ initialData?: TData;
50
+ }): T[];
@@ -0,0 +1,38 @@
1
+ import { faker } from "@faker-js/faker";
2
+ export function generateRandomTree({
3
+ createNode,
4
+ parentData
5
+ }, {
6
+ rootNodes = faker.number.int({ min: 0, max: 5 }),
7
+ depth = faker.number.int({ min: 0, max: 5 }),
8
+ minChildren = 0,
9
+ maxInitialChildren = 10,
10
+ initialData
11
+ } = {}) {
12
+ const generateChildren = (currentDepth, childCount, pData) => {
13
+ const res = [];
14
+ for (let i = 0; i < childCount; i++) {
15
+ const numChildren = calculateNumChildren(depth, currentDepth, minChildren, maxInitialChildren);
16
+ const data = parentData?.(pData) ?? void 0;
17
+ const parent = createNode(
18
+ generateChildren(currentDepth + 1, numChildren, data),
19
+ numChildren === 0,
20
+ data
21
+ );
22
+ if (parent === void 0) continue;
23
+ res.push(parent);
24
+ }
25
+ return res;
26
+ };
27
+ return generateChildren(0, rootNodes, initialData);
28
+ }
29
+ function calculateNumChildren(depth, currentDepth, minChildren, maxInitialChildren) {
30
+ const decayFactor = depth > 0 ? (depth - currentDepth) / depth : 0;
31
+ const maxAtDepth = minChildren + (maxInitialChildren - minChildren) * decayFactor;
32
+ const maxChildrenAtDepth = Math.max(minChildren, Math.floor(maxAtDepth));
33
+ let numChildren = 0;
34
+ if (currentDepth < depth) {
35
+ numChildren = faker.number.int({ min: minChildren, max: maxChildrenAtDepth });
36
+ }
37
+ return numChildren;
38
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/editor",
3
3
  "description": "Block base prosemirror editor with partial/full editable document embeds, infinite embeds, and document uploads.",
4
- "version": "0.0.6",
4
+ "version": "0.0.7",
5
5
  "main": "./dist/runtime/main.lib.js",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -110,11 +110,13 @@
110
110
  "@alanscodelog/eslint-config": "^6.3.1",
111
111
  "@alanscodelog/utils": "^6.0.2",
112
112
  "@commitlint/cli": "^19.8.1",
113
+ "@faker-js/faker": "^10.0.0",
113
114
  "@fortawesome/fontawesome-svg-core": "^7.0.1",
114
115
  "@fortawesome/free-brands-svg-icons": "^7.0.1",
115
116
  "@fortawesome/free-regular-svg-icons": "^7.0.1",
116
117
  "@fortawesome/free-solid-svg-icons": "^7.0.1",
117
118
  "@nuxt/eslint-config": "^1.9.0",
119
+ "@tiptap/html": "^3.4.2",
118
120
  "@witchcraft/nuxt-utils": "^0.3.6",
119
121
  "@witchcraft/ui": "^0.3.7",
120
122
  "colord": "^2.9.3",
@@ -141,7 +143,7 @@
141
143
  "@nuxt/module-builder": "^1.0.2",
142
144
  "@nuxt/schema": "^4.1.2",
143
145
  "@nuxt/types": "^2.18.1",
144
- "@playwright/test": "^1.54.0",
146
+ "@playwright/test": "=1.56.0",
145
147
  "@rollup/plugin-dynamic-import-vars": "^2.1.5",
146
148
  "@tailwindcss/cli": "^4.1.13",
147
149
  "@tailwindcss/vite": "^4.1.13",
@@ -153,15 +155,15 @@
153
155
  "@witchcraft/ui": "^0.3.7",
154
156
  "concurrently": "^9.2.1",
155
157
  "cross-env": "^10.0.0",
156
- "eslint": "^9.35.0",
158
+ "eslint": "^9.38.0",
157
159
  "fast-glob": "^3.3.3",
158
160
  "http-server": "^14.1.1",
159
161
  "husky": "^9.1.7",
160
162
  "madge": "^8.0.0",
161
163
  "nuxt": "^4.1.2",
162
164
  "onchange": "^7.1.0",
163
- "playwright": "^1.54.0",
164
- "playwright-core": "^1.54.0",
165
+ "playwright": "=1.56.0",
166
+ "playwright-core": "=1.56.0",
165
167
  "prosemirror-test-builder": "^1.1.1",
166
168
  "radix-vue": "^1.9.17",
167
169
  "semantic-release": "^24.2.8",
@@ -9,7 +9,7 @@ declare module "@tiptap/core" {
9
9
  changeAttrs: {
10
10
  changeAttrs: (
11
11
  nodeType: string | undefined,
12
- attrs: Record<string, unknown>,
12
+ attrs: Record<string, unknown>
13
13
  ) => ReturnType
14
14
  }
15
15
  }
@@ -32,7 +32,6 @@ declare module "@tiptap/core" {
32
32
  export const Item = Node.create<ItemNodeOptions>({
33
33
  name: "item" satisfies NodeItemName,
34
34
  content: "block list? | list",
35
-
36
35
  addOptions() {
37
36
  return {
38
37
  HTMLAttributes: {},
@@ -174,3 +173,4 @@ export const Item = Node.create<ItemNodeOptions>({
174
173
  }
175
174
  })
176
175
  export type NodeItemName = "item"
176
+
@@ -17,7 +17,7 @@ declare module "@tiptap/core" {
17
17
  */
18
18
  moveItem: (
19
19
  dir: "down" | "up",
20
- pos?: number,
20
+ pos?: number
21
21
  ) => ReturnType
22
22
  }
23
23
  }
@@ -97,22 +97,27 @@
97
97
  </div>
98
98
  </template>
99
99
 
100
- <!--
101
- Multipurpose drag handle + collapse indicator.
102
-
103
- This is incredibly useful for making a compact draggable tree view.
104
-
105
- The collapse indicator has a default height, but it should be set manually. For example `[&>.collapse-indicator]:h-[...]`
106
-
107
- The component only emits a few events, it does not handle the dragging itself or what actually happens on any clicks/input.
108
- -->
109
100
  <script lang="ts">
101
+ /**
102
+ * Multipurpose drag handle + collapse indicator.
103
+ *
104
+ * This is incredibly useful for making a compact draggable tree view.
105
+ *
106
+ * The collapse indicator has a default height, but it should be set manually. For example `[&>.collapse-indicator]:h-[...]`
107
+ *
108
+ * The component only emits a few events, it does not handle the dragging itself or what actually happens on any clicks/input.
109
+ */
110
+ export default { }
110
111
  </script>
111
112
 
112
113
  <script setup lang="ts">
113
114
  import { twMerge } from "@witchcraft/ui/utils/twMerge"
114
115
  import { onMounted, onUnmounted, ref, useAttrs } from "vue"
115
116
 
117
+
118
+ defineOptions({
119
+ name: "DragTreeHandle"
120
+ })
116
121
  interface Props {
117
122
  hasChildren: boolean
118
123
  passedDragThreshold: boolean
@@ -10,7 +10,7 @@ declare module "@tiptap/core" {
10
10
  embeddedCommandRedirect: {
11
11
  embeddedCommandRedirect: (
12
12
  commandName: string,
13
- args: any,
13
+ args: any
14
14
  ) => ReturnType
15
15
  }
16
16
  }
@@ -46,7 +46,7 @@ export type IFileLoaderHandler<
46
46
  editor: Editor,
47
47
  pos: number | undefined,
48
48
  error: Error,
49
- loadingKey: TKey,
49
+ loadingKey: TKey
50
50
  ) => void
51
51
 
52
52
  /**
@@ -77,7 +77,7 @@ export type IFileLoaderHandler<
77
77
  editor: Editor,
78
78
  pos: number,
79
79
  res: T,
80
- loadingKey: TKey,
80
+ loadingKey: TKey
81
81
  ) => void
82
82
  /**
83
83
  * Return the file (or whatever type you'd like) to allow the extension to handle it.
@@ -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
+ }