docusaurus-plugin-generate-schema-docs 1.5.4 → 1.7.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.
Files changed (38) hide show
  1. package/__tests__/__fixtures__/static/schemas/battle-test-event.json +771 -0
  2. package/__tests__/__fixtures__/static/schemas/conditional-event.json +52 -0
  3. package/__tests__/__fixtures__/static/schemas/nested-conditional-event.json +50 -0
  4. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +3 -2
  5. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +4 -2
  6. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +3 -2
  7. package/__tests__/components/ConditionalRows.test.js +150 -0
  8. package/__tests__/components/ConnectorLines.visualRegression.test.js +93 -0
  9. package/__tests__/components/FoldableRows.test.js +7 -4
  10. package/__tests__/components/SchemaRows.test.js +31 -0
  11. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +7 -0
  12. package/__tests__/generateEventDocs.anchor.test.js +7 -0
  13. package/__tests__/generateEventDocs.nested.test.js +7 -0
  14. package/__tests__/generateEventDocs.partials.test.js +134 -0
  15. package/__tests__/generateEventDocs.test.js +7 -0
  16. package/__tests__/helpers/buildExampleFromSchema.test.js +49 -0
  17. package/__tests__/helpers/schemaToExamples.test.js +75 -0
  18. package/__tests__/helpers/schemaToTableData.battleTest.test.js +704 -0
  19. package/__tests__/helpers/schemaToTableData.hierarchicalLines.test.js +190 -7
  20. package/__tests__/helpers/schemaToTableData.test.js +263 -2
  21. package/__tests__/helpers/validator.test.js +6 -6
  22. package/components/ConditionalRows.js +156 -0
  23. package/components/FoldableRows.js +88 -61
  24. package/components/PropertiesTable.js +1 -1
  25. package/components/PropertyRow.js +24 -8
  26. package/components/SchemaRows.css +115 -0
  27. package/components/SchemaRows.js +31 -4
  28. package/generateEventDocs.js +55 -37
  29. package/helpers/buildExampleFromSchema.js +11 -0
  30. package/helpers/choice-index-template.js +2 -1
  31. package/helpers/continuingLinesStyle.js +169 -0
  32. package/helpers/schema-doc-template.js +2 -5
  33. package/helpers/schema-processing.js +3 -0
  34. package/helpers/schemaToExamples.js +75 -2
  35. package/helpers/schemaToTableData.js +252 -26
  36. package/helpers/update-schema-ids.js +3 -3
  37. package/helpers/validator.js +7 -19
  38. package/package.json +3 -2
@@ -7,6 +7,20 @@ import SchemaDocTemplate from './helpers/schema-doc-template.js';
7
7
  import ChoiceIndexTemplate from './helpers/choice-index-template.js';
8
8
  import processSchema from './helpers/processSchema.js';
9
9
 
10
+ function buildEditUrl(organizationName, projectName, siteDir, filePath) {
11
+ const baseEditUrl = `https://github.com/${organizationName}/${projectName}/edit/main`;
12
+ return `${baseEditUrl}/${path.relative(path.join(siteDir, '..'), filePath)}`;
13
+ }
14
+
15
+ function resolvePartial(partialPath, relativePartialsDir, componentPrefix) {
16
+ if (!fs.existsSync(partialPath)) return { import: '', component: '' };
17
+ const fileName = path.basename(partialPath);
18
+ return {
19
+ import: `import ${componentPrefix} from '@site/${relativePartialsDir}/${fileName}';`,
20
+ component: `<${componentPrefix} />`,
21
+ };
22
+ }
23
+
10
24
  async function generateAndWriteDoc(
11
25
  filePath,
12
26
  schema,
@@ -14,45 +28,45 @@ async function generateAndWriteDoc(
14
28
  outputDir,
15
29
  options,
16
30
  alreadyMergedSchema = null,
31
+ editFilePath = null,
17
32
  ) {
18
- const { organizationName, projectName, siteDir, dataLayerName } = options;
19
- const baseEditUrl = `https://github.com/${organizationName}/${projectName}/edit/main`;
20
- const PARTIALS_DIR = path.join(siteDir, 'docs/partials');
33
+ const { organizationName, projectName, siteDir, dataLayerName, version } =
34
+ options;
35
+
36
+ const { outputDir: versionOutputDir } = getPathsForVersion(version, siteDir);
37
+ const PARTIALS_DIR = path.join(versionOutputDir, 'partials');
38
+ const relativePartialsDir = path.relative(siteDir, PARTIALS_DIR);
21
39
 
22
40
  const mergedSchema = alreadyMergedSchema || (await processSchema(filePath));
23
41
 
24
42
  // Check for partials
25
- const topPartialPath = path.join(PARTIALS_DIR, `${eventName}.mdx`);
26
- const bottomPartialPath = path.join(PARTIALS_DIR, `${eventName}_bottom.mdx`);
27
-
28
- let topPartialImport = '';
29
- let topPartialComponent = '';
30
- if (fs.existsSync(topPartialPath)) {
31
- topPartialImport = `import TopPartial from '@site/docs/partials/${eventName}.mdx';`;
32
- topPartialComponent = '<TopPartial />';
33
- }
34
-
35
- let bottomPartialImport = '';
36
- let bottomPartialComponent = '';
37
- if (fs.existsSync(bottomPartialPath)) {
38
- bottomPartialImport = `import BottomPartial from '@site/docs/partials/${eventName}_bottom.mdx';`;
39
- bottomPartialComponent = '<BottomPartial />';
40
- }
41
-
42
- const editUrl = `${baseEditUrl}/${path.relative(
43
- path.join(siteDir, '..'),
44
- filePath,
45
- )}`;
43
+ const top = resolvePartial(
44
+ path.join(PARTIALS_DIR, `_${eventName}.mdx`),
45
+ relativePartialsDir,
46
+ 'TopPartial',
47
+ );
48
+ const bottom = resolvePartial(
49
+ path.join(PARTIALS_DIR, `_${eventName}_bottom.mdx`),
50
+ relativePartialsDir,
51
+ 'BottomPartial',
52
+ );
53
+
54
+ const editUrl = buildEditUrl(
55
+ organizationName,
56
+ projectName,
57
+ siteDir,
58
+ editFilePath || filePath,
59
+ );
46
60
 
47
61
  const mdxContent = SchemaDocTemplate({
48
62
  schema,
49
63
  mergedSchema,
50
64
  editUrl,
51
65
  file: path.basename(filePath),
52
- topPartialImport,
53
- bottomPartialImport,
54
- topPartialComponent,
55
- bottomPartialComponent,
66
+ topPartialImport: top.import,
67
+ bottomPartialImport: bottom.import,
68
+ topPartialComponent: top.component,
69
+ bottomPartialComponent: bottom.component,
56
70
  dataLayerName,
57
71
  });
58
72
 
@@ -67,6 +81,14 @@ async function generateOneOfDocs(
67
81
  outputDir,
68
82
  options,
69
83
  ) {
84
+ const { organizationName, projectName, siteDir } = options;
85
+ const editUrl = buildEditUrl(
86
+ organizationName,
87
+ projectName,
88
+ siteDir,
89
+ filePath,
90
+ );
91
+
70
92
  const eventOutputDir = path.join(outputDir, eventName);
71
93
  createDir(eventOutputDir);
72
94
 
@@ -75,39 +97,35 @@ async function generateOneOfDocs(
75
97
  const indexPageContent = ChoiceIndexTemplate({
76
98
  schema,
77
99
  processedOptions: processed,
100
+ editUrl,
78
101
  });
79
102
  writeDoc(eventOutputDir, 'index.mdx', indexPageContent);
80
103
 
81
104
  for (const [
82
105
  index,
83
- { slug, schema: processedSchema },
106
+ { slug, schema: processedSchema, sourceFilePath },
84
107
  ] of processed.entries()) {
85
108
  const subChoiceType = processedSchema.oneOf ? 'oneOf' : null;
86
109
  const prefixedSlug = `${(index + 1).toString().padStart(2, '0')}-${slug}`;
87
110
 
88
111
  if (subChoiceType) {
89
- const tempFilePath = path.join(eventOutputDir, `${slug}.json`);
90
- fs.writeFileSync(tempFilePath, JSON.stringify(processedSchema, null, 2));
91
112
  await generateOneOfDocs(
92
113
  prefixedSlug,
93
114
  processedSchema,
94
- tempFilePath,
115
+ sourceFilePath || filePath,
95
116
  eventOutputDir,
96
117
  options,
97
118
  );
98
- fs.unlinkSync(tempFilePath);
99
119
  } else {
100
- const tempFilePath = path.join(eventOutputDir, `${prefixedSlug}.json`);
101
- fs.writeFileSync(tempFilePath, JSON.stringify(processedSchema, null, 2));
102
120
  await generateAndWriteDoc(
103
- tempFilePath,
121
+ `${prefixedSlug}.json`,
104
122
  processedSchema,
105
123
  slug,
106
124
  eventOutputDir,
107
125
  options,
108
126
  processedSchema,
127
+ sourceFilePath || filePath,
109
128
  );
110
- fs.unlinkSync(tempFilePath);
111
129
  }
112
130
  }
113
131
  }
@@ -30,6 +30,17 @@ const buildExampleFromSchema = (schema) => {
30
30
  return buildExampleFromSchema(merged);
31
31
  }
32
32
 
33
+ // For conditionals, default to the 'then' branch and recurse.
34
+ if (schema.if && schema.then) {
35
+ const newSchema = { ...schema };
36
+ const thenBranch = newSchema.then;
37
+ delete newSchema.if;
38
+ delete newSchema.then;
39
+ delete newSchema.else;
40
+ const merged = mergeJsonSchema({ allOf: [newSchema, thenBranch] });
41
+ return buildExampleFromSchema(merged);
42
+ }
43
+
33
44
  // If there's an explicit example, use it.
34
45
  const exampleValue = getSingleExampleValue(schema);
35
46
  if (typeof exampleValue !== 'undefined') {
@@ -1,9 +1,10 @@
1
1
  export default function ChoiceIndexTemplate(data) {
2
- const { schema, processedOptions } = data;
2
+ const { schema, processedOptions, editUrl } = data;
3
3
 
4
4
  return `---
5
5
  title: ${schema.title}
6
6
  description: "${schema.description}"
7
+ custom_edit_url: ${editUrl}
7
8
  ---
8
9
  import SchemaJsonViewer from '@theme/SchemaJsonViewer';
9
10
 
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Colors for group bracket lines, indexed by bracketIndex.
3
+ * Uses Docusaurus CSS custom properties so they adapt to light/dark themes.
4
+ */
5
+ const BRACKET_COLORS = [
6
+ 'var(--ifm-color-info)',
7
+ 'var(--ifm-color-warning)',
8
+ 'var(--ifm-color-success)',
9
+ ];
10
+
11
+ /**
12
+ * Returns the right-offset (in rem) for a bracket line.
13
+ * Brackets are positioned from the right edge of the cell so they appear
14
+ * on the right side of the table, away from the tree connector lines.
15
+ *
16
+ * B=0 → 0.50 rem from right
17
+ * B=1 → 0.75 rem from right
18
+ * B=2 → 1.00 rem from right
19
+ */
20
+ export const getBracketPosition = (bracketIndex) => 0.5 + bracketIndex * 0.25;
21
+
22
+ export const getBracketColor = (bracketIndex) =>
23
+ BRACKET_COLORS[bracketIndex % BRACKET_COLORS.length];
24
+
25
+ /** Width of horizontal cap lines in pixels */
26
+ const CAP_WIDTH = 10;
27
+
28
+ /** Inset from cell edge for cap lines (so they aren't hidden by table borders) */
29
+ const CAP_INSET = 6;
30
+
31
+ /** Helper to create a bracket key for Set lookups */
32
+ const bracketKey = (b) => `${b.level}:${b.bracketIndex}`;
33
+
34
+ /**
35
+ * Generates inline styles for bracket lines positioned on the right side.
36
+ * Supports optional horizontal "cap" lines at the top and/or bottom of
37
+ * brackets to visually delineate where a bracket group starts and ends.
38
+ *
39
+ * @param {Array<{level: number, bracketIndex: number}>} groupBrackets - Active bracket groups
40
+ * @param {object} [caps] - Optional cap configuration
41
+ * @param {Array<{level: number, bracketIndex: number}>} [caps.starting] - Brackets needing a top cap
42
+ * @param {Array<{level: number, bracketIndex: number}>} [caps.ending] - Brackets needing a bottom cap
43
+ * @returns {object} Style object with background gradients
44
+ */
45
+ export const getBracketLinesStyle = (groupBrackets = [], caps = {}) => {
46
+ if (groupBrackets.length === 0) return {};
47
+
48
+ const startingKeys = new Set((caps.starting || []).map(bracketKey));
49
+ const endingKeys = new Set((caps.ending || []).map(bracketKey));
50
+
51
+ const gradients = [];
52
+ const sizes = [];
53
+ const positions = [];
54
+
55
+ groupBrackets.forEach((bracket) => {
56
+ const { bracketIndex } = bracket;
57
+ const pos = getBracketPosition(bracketIndex);
58
+ const color = getBracketColor(bracketIndex);
59
+ const key = bracketKey(bracket);
60
+ const isStarting = startingKeys.has(key);
61
+ const isEnding = endingKeys.has(key);
62
+
63
+ // Vertical line — shortened when caps are present so it doesn't bleed past them
64
+ const topInset = isStarting ? CAP_INSET : 0;
65
+ const bottomInset = isEnding ? CAP_INSET : 0;
66
+ gradients.push(`linear-gradient(${color}, ${color})`);
67
+ sizes.push(`1px calc(100% - ${topInset + bottomInset}px)`);
68
+ positions.push(`right ${pos}rem top ${topInset}px`);
69
+
70
+ // Horizontal cap at the top (bracket start) — inset from cell edge
71
+ if (isStarting) {
72
+ const capOffset = (CAP_WIDTH - 1) / 2; // center cap on the vertical line
73
+ gradients.push(`linear-gradient(${color}, ${color})`);
74
+ sizes.push(`${CAP_WIDTH}px 1px`);
75
+ positions.push(
76
+ `right calc(${pos}rem - ${capOffset}px) top ${CAP_INSET}px`,
77
+ );
78
+ }
79
+
80
+ // Horizontal cap at the bottom (bracket end) — inset from cell edge
81
+ if (isEnding) {
82
+ const capOffset = (CAP_WIDTH - 1) / 2;
83
+ gradients.push(`linear-gradient(${color}, ${color})`);
84
+ sizes.push(`${CAP_WIDTH}px 1px`);
85
+ positions.push(
86
+ `right calc(${pos}rem - ${capOffset}px) bottom ${CAP_INSET}px`,
87
+ );
88
+ }
89
+ });
90
+
91
+ return {
92
+ backgroundImage: gradients.join(', '),
93
+ backgroundSize: sizes.join(', '),
94
+ backgroundPosition: positions.join(', '),
95
+ backgroundRepeat: 'no-repeat',
96
+ };
97
+ };
98
+
99
+ /**
100
+ * Merges two background-gradient style objects into one.
101
+ * @param {object} style1 - First style object
102
+ * @param {object} style2 - Second style object
103
+ * @returns {object} Merged style object
104
+ */
105
+ export const mergeBackgroundStyles = (style1, style2) => {
106
+ if (!style2.backgroundImage) return style1;
107
+ if (!style1.backgroundImage) return { ...style1, ...style2 };
108
+
109
+ return {
110
+ ...style1,
111
+ backgroundImage: `${style1.backgroundImage}, ${style2.backgroundImage}`,
112
+ backgroundSize: `${style1.backgroundSize}, ${style2.backgroundSize}`,
113
+ backgroundPosition: `${style1.backgroundPosition}, ${style2.backgroundPosition}`,
114
+ backgroundRepeat: 'no-repeat',
115
+ };
116
+ };
117
+
118
+ /**
119
+ * Generates inline styles for continuing hierarchical lines through a row.
120
+ * Only handles tree connector lines (left side). Bracket lines are separate.
121
+ * @param {number[]} continuingLevels - Array of ancestor levels that need lines
122
+ * @param {number} level - Current level of the row
123
+ * @returns {object} Style object with background gradients
124
+ */
125
+ export const getContinuingLinesStyle = (continuingLevels = [], level = 0) => {
126
+ const getLevelPosition = (lvl) => lvl * 1.25 + 0.5;
127
+
128
+ const allGradients = [];
129
+ const allSizes = [];
130
+ const allPositions = [];
131
+
132
+ // Draw continuing lines for all ancestor levels
133
+ continuingLevels.forEach((lvl) => {
134
+ const pos = getLevelPosition(lvl);
135
+ allGradients.push(
136
+ 'linear-gradient(var(--ifm-table-border-color), var(--ifm-table-border-color))',
137
+ );
138
+ allSizes.push('1px 100%');
139
+ allPositions.push(`${pos}rem top`);
140
+ });
141
+
142
+ // Also draw the line for the immediate parent level (level - 1) if level > 0
143
+ // This connects the rows to their parent property
144
+ if (level > 0) {
145
+ const parentPos = getLevelPosition(level - 1);
146
+ if (!continuingLevels.includes(level - 1)) {
147
+ allGradients.push(
148
+ 'linear-gradient(var(--ifm-table-border-color), var(--ifm-table-border-color))',
149
+ );
150
+ allSizes.push('1px 100%');
151
+ allPositions.push(`${parentPos}rem top`);
152
+ }
153
+ }
154
+
155
+ // Calculate indentation based on level
156
+ const paddingLeft = `${level * 1.25 + 0.5}rem`;
157
+
158
+ if (allGradients.length === 0) {
159
+ return { paddingLeft };
160
+ }
161
+
162
+ return {
163
+ paddingLeft,
164
+ backgroundImage: allGradients.join(', '),
165
+ backgroundSize: allSizes.join(', '),
166
+ backgroundPosition: allPositions.join(', '),
167
+ backgroundRepeat: 'no-repeat',
168
+ };
169
+ };
@@ -8,6 +8,7 @@ export default function MdxTemplate(data) {
8
8
  bottomPartialImport,
9
9
  topPartialComponent,
10
10
  bottomPartialComponent,
11
+ dataLayerName,
11
12
  } = data;
12
13
 
13
14
  return `---
@@ -30,11 +31,7 @@ ${topPartialComponent}
30
31
 
31
32
  <SchemaViewer
32
33
  schema={${JSON.stringify(mergedSchema)}}
33
- ${
34
- data.dataLayerName && data.dataLayerName !== 'undefined'
35
- ? ` dataLayerName={'${data.dataLayerName}'}`
36
- : ''
37
- }
34
+ ${dataLayerName ? ` dataLayerName={'${dataLayerName}'}` : ''}
38
35
  />
39
36
  <SchemaJsonViewer schema={${JSON.stringify(schema)}} />
40
37
 
@@ -25,8 +25,10 @@ export async function processOneOfSchema(schema, filePath) {
25
25
 
26
26
  for (const option of schema[choiceType]) {
27
27
  let resolvedOption = option;
28
+ let sourceFilePath = null;
28
29
  if (option.$ref && !option.$ref.startsWith('#')) {
29
30
  const refPath = path.resolve(path.dirname(filePath), option.$ref);
31
+ sourceFilePath = refPath;
30
32
  resolvedOption = await processSchema(refPath);
31
33
  }
32
34
 
@@ -54,6 +56,7 @@ export async function processOneOfSchema(schema, filePath) {
54
56
  processedSchemas.push({
55
57
  slug,
56
58
  schema: newSchema,
59
+ sourceFilePath,
57
60
  });
58
61
  }
59
62
  }
@@ -22,6 +22,25 @@ const findChoicePoints = (subSchema, path = []) => {
22
22
  return [...currentChoice, ...nestedChoices];
23
23
  };
24
24
 
25
+ const findConditionalPoints = (subSchema, path = []) => {
26
+ if (!subSchema) {
27
+ return [];
28
+ }
29
+
30
+ const currentConditional =
31
+ subSchema.if && (subSchema.then || subSchema.else)
32
+ ? [{ path, schema: subSchema }]
33
+ : [];
34
+
35
+ const nestedConditionals = subSchema.properties
36
+ ? Object.entries(subSchema.properties).flatMap(([key, propSchema]) =>
37
+ findConditionalPoints(propSchema, [...path, 'properties', key]),
38
+ )
39
+ : [];
40
+
41
+ return [...currentConditional, ...nestedConditionals];
42
+ };
43
+
25
44
  const generateExampleForChoice = (rootSchema, path, option) => {
26
45
  const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
27
46
 
@@ -44,10 +63,43 @@ const generateExampleForChoice = (rootSchema, path, option) => {
44
63
  }
45
64
  };
46
65
 
66
+ const generateConditionalExample = (rootSchema, path, branch) => {
67
+ const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
68
+
69
+ if (path.length === 0) {
70
+ const branchSchema = schemaVariant[branch];
71
+ delete schemaVariant.if;
72
+ delete schemaVariant.then;
73
+ delete schemaVariant.else;
74
+ if (branchSchema) {
75
+ return buildExampleFromSchema(
76
+ mergeJsonSchema({ allOf: [schemaVariant, branchSchema] }),
77
+ );
78
+ }
79
+ return buildExampleFromSchema(schemaVariant);
80
+ }
81
+
82
+ let target = schemaVariant;
83
+ for (const segment of path) {
84
+ target = target[segment];
85
+ }
86
+ const branchSchema = target[branch];
87
+ delete target.if;
88
+ delete target.then;
89
+ delete target.else;
90
+ if (branchSchema) {
91
+ const merged = mergeJsonSchema({ allOf: [target, branchSchema] });
92
+ Object.keys(target).forEach((k) => delete target[k]);
93
+ Object.assign(target, merged);
94
+ }
95
+ return buildExampleFromSchema(schemaVariant);
96
+ };
97
+
47
98
  export function schemaToExamples(rootSchema) {
48
99
  const choicePoints = findChoicePoints(rootSchema);
100
+ const conditionalPoints = findConditionalPoints(rootSchema);
49
101
 
50
- if (choicePoints.length === 0) {
102
+ if (choicePoints.length === 0 && conditionalPoints.length === 0) {
51
103
  const example = buildExampleFromSchema(rootSchema);
52
104
  if (example && Object.keys(example).length > 0) {
53
105
  return [
@@ -57,7 +109,7 @@ export function schemaToExamples(rootSchema) {
57
109
  return [];
58
110
  }
59
111
 
60
- return choicePoints.map(({ path, schema }) => {
112
+ const choiceExamples = choicePoints.map(({ path, schema }) => {
61
113
  const choiceType = schema.oneOf ? 'oneOf' : 'anyOf';
62
114
  const propertyName = path.length > 0 ? path[path.length - 1] : 'root';
63
115
 
@@ -68,4 +120,25 @@ export function schemaToExamples(rootSchema) {
68
120
 
69
121
  return { property: propertyName, options };
70
122
  });
123
+
124
+ const conditionalExamples = conditionalPoints.map(({ path, schema }) => {
125
+ const options = [];
126
+
127
+ if (schema.then) {
128
+ options.push({
129
+ title: 'When condition is met',
130
+ example: generateConditionalExample(rootSchema, path, 'then'),
131
+ });
132
+ }
133
+ if (schema.else) {
134
+ options.push({
135
+ title: 'When condition is not met',
136
+ example: generateConditionalExample(rootSchema, path, 'else'),
137
+ });
138
+ }
139
+
140
+ return { property: 'conditional', options };
141
+ });
142
+
143
+ return [...choiceExamples, ...conditionalExamples];
71
144
  }