docusaurus-plugin-generate-schema-docs 1.8.1 → 1.8.3

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 (37) hide show
  1. package/README.md +17 -7
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-constraints-ref.json +20 -0
  3. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +15 -3
  4. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +20 -4
  5. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +30 -6
  6. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +10 -2
  7. package/__tests__/components/ConditionalRows.test.js +28 -0
  8. package/__tests__/components/FoldableRows.test.js +31 -290
  9. package/__tests__/components/PropertyRow.test.js +216 -0
  10. package/__tests__/components/SchemaJsonViewer.test.js +76 -10
  11. package/__tests__/components/SchemaRows.test.js +62 -12
  12. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  13. package/__tests__/generateEventDocs.partials.test.js +95 -0
  14. package/__tests__/generateEventDocs.test.js +3 -0
  15. package/__tests__/helpers/processSchema.test.js +29 -0
  16. package/__tests__/helpers/schemaToTableData.test.js +112 -0
  17. package/__tests__/helpers/validator.test.js +32 -0
  18. package/components/ConditionalRows.js +6 -3
  19. package/components/FoldableRows.js +9 -3
  20. package/components/PropertiesTable.js +2 -1
  21. package/components/PropertyRow.js +90 -5
  22. package/components/SchemaJsonViewer.js +221 -4
  23. package/components/SchemaRows.css +98 -7
  24. package/components/SchemaRows.js +11 -1
  25. package/generateEventDocs.js +184 -18
  26. package/helpers/buildExampleFromSchema.js +3 -3
  27. package/helpers/choice-index-template.js +6 -2
  28. package/helpers/constraintSchemaPaths.js +46 -0
  29. package/helpers/file-system.js +28 -0
  30. package/helpers/mergeSchema.js +16 -0
  31. package/helpers/processSchema.js +19 -6
  32. package/helpers/schema-doc-template.js +7 -1
  33. package/helpers/schema-processing.js +4 -11
  34. package/helpers/schemaToExamples.js +4 -4
  35. package/helpers/schemaToTableData.js +68 -7
  36. package/helpers/validator.js +7 -0
  37. package/package.json +1 -1
@@ -26,12 +26,36 @@ const getContainerSymbol = (containerType) => {
26
26
  return '';
27
27
  };
28
28
 
29
+ const KEYWORD_HELP_TEXT = {
30
+ additionalProperties:
31
+ 'Controls properties not listed in properties and not matched by patternProperties.',
32
+ patternProperties:
33
+ 'Applies the subschema to property names that match the given regular expression.',
34
+ };
35
+
36
+ function splitKeywordLabel(name) {
37
+ const match = /^patternProperties (\/.+\/)$/.exec(name);
38
+ if (!match) {
39
+ return null;
40
+ }
41
+
42
+ return {
43
+ keyword: 'patternProperties',
44
+ pattern: match[1],
45
+ };
46
+ }
47
+
29
48
  /**
30
49
  * Renders a single property row in the schema table.
31
50
  * All data is passed in via the `row` prop, which comes from `tableData`.
32
51
  * This component handles multi-row constraints using `rowSpan`.
33
52
  */
34
- export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
53
+ export default function PropertyRow({
54
+ row,
55
+ stripeIndex,
56
+ isLastInGroup,
57
+ bracketEnds,
58
+ }) {
35
59
  const {
36
60
  name,
37
61
  level,
@@ -44,6 +68,8 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
44
68
  containerType,
45
69
  continuingLevels = [],
46
70
  groupBrackets = [],
71
+ isSchemaKeywordRow = false,
72
+ keepConnectorOpen = false,
47
73
  } = row;
48
74
 
49
75
  const indentStyle = {
@@ -112,11 +138,27 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
112
138
  const bracketStyle = getBracketLinesStyle(groupBrackets, bracketCaps);
113
139
 
114
140
  const containerSymbol = getContainerSymbol(containerType);
141
+ const shouldCloseConnector = isLastInGroup && !keepConnectorOpen;
142
+ const splitKeyword = splitKeywordLabel(name);
143
+ const keywordHelpKey = name.startsWith('patternProperties ')
144
+ ? 'patternProperties'
145
+ : name;
146
+ const keywordHelpText = KEYWORD_HELP_TEXT[keywordHelpKey];
147
+ const keywordHelpId = keywordHelpText
148
+ ? `schema-keyword-help-${name}`
149
+ : undefined;
150
+ const zebraClassName =
151
+ stripeIndex === undefined
152
+ ? undefined
153
+ : stripeIndex % 2 === 0
154
+ ? 'schema-row--zebra-even'
155
+ : 'schema-row--zebra-odd';
115
156
 
116
157
  return (
117
158
  <>
118
159
  <tr
119
160
  className={clsx(
161
+ zebraClassName,
120
162
  required && 'required-row',
121
163
  row.isCondition && 'conditional-condition-row',
122
164
  )}
@@ -126,17 +168,57 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
126
168
  style={{ ...indentStyle, ...continuingLinesStyle }}
127
169
  className={clsx(
128
170
  'property-cell',
171
+ isSchemaKeywordRow && 'property-cell--keyword',
172
+ required && 'property-cell--required',
173
+ level > 0 && 'property-cell--tree',
129
174
  level > 0 && `level-${level}`,
130
- isLastInGroup && 'is-last',
175
+ shouldCloseConnector && 'is-last',
131
176
  hasChildren && 'has-children',
132
177
  containerType && `container-${containerType}`,
133
178
  )}
134
179
  >
135
- <span className="property-name">
180
+ <span
181
+ className={clsx(
182
+ 'property-name',
183
+ isSchemaKeywordRow && 'property-name--keyword',
184
+ )}
185
+ >
136
186
  {containerSymbol && (
137
187
  <span className="container-symbol">{containerSymbol}</span>
138
188
  )}
139
- <strong>{name}</strong>
189
+ {isSchemaKeywordRow ? (
190
+ <span className="property-keyword-wrapper">
191
+ {splitKeyword ? (
192
+ <span className="property-keyword-stack">
193
+ <code
194
+ className="property-keyword"
195
+ aria-describedby={keywordHelpId}
196
+ >
197
+ {splitKeyword.keyword}
198
+ </code>
199
+ <code className="property-keyword-pattern">
200
+ {splitKeyword.pattern}
201
+ </code>
202
+ </span>
203
+ ) : (
204
+ <code
205
+ className="property-keyword"
206
+ aria-describedby={keywordHelpId}
207
+ >
208
+ {name}
209
+ </code>
210
+ )}
211
+ <span
212
+ id={keywordHelpId}
213
+ className="property-keyword-tooltip"
214
+ role="tooltip"
215
+ >
216
+ {keywordHelpText}
217
+ </span>
218
+ </span>
219
+ ) : (
220
+ <strong>{name}</strong>
221
+ )}
140
222
  </span>
141
223
  </td>
142
224
  <td rowSpan={rowSpan}>
@@ -181,7 +263,10 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
181
263
 
182
264
  {/* Render subsequent constraints in their own rows */}
183
265
  {remainingConstraints.map((constraint) => (
184
- <tr className={clsx(required && 'required-row')} key={constraint}>
266
+ <tr
267
+ className={clsx(zebraClassName, required && 'required-row')}
268
+ key={constraint}
269
+ >
185
270
  <td className="constraint-cell">
186
271
  <code
187
272
  className={clsx(
@@ -1,11 +1,228 @@
1
- import React from 'react';
2
- import CodeBlock from '@theme/CodeBlock';
1
+ import React, { Fragment, useState } from 'react';
2
+ import Link from '@docusaurus/Link';
3
+
4
+ function isPlainObject(value) {
5
+ return (
6
+ value !== null &&
7
+ typeof value === 'object' &&
8
+ !Array.isArray(value) &&
9
+ Object.getPrototypeOf(value) === Object.prototype
10
+ );
11
+ }
12
+
13
+ function isExternalRef(value) {
14
+ return typeof value === 'string' && /^https?:\/\//.test(value);
15
+ }
16
+
17
+ function normalizePathSegments(pathValue) {
18
+ const normalized = pathValue.replace(/\\/g, '/');
19
+ const isAbsolute = normalized.startsWith('/');
20
+ const segments = normalized.split('/');
21
+ const resolvedSegments = [];
22
+
23
+ segments.forEach((segment) => {
24
+ if (!segment || segment === '.') return;
25
+ if (segment === '..') {
26
+ resolvedSegments.pop();
27
+ return;
28
+ }
29
+ resolvedSegments.push(segment);
30
+ });
31
+
32
+ return `${isAbsolute ? '/' : ''}${resolvedSegments.join('/')}`;
33
+ }
34
+
35
+ function dirname(pathValue) {
36
+ const normalized = normalizePathSegments(pathValue);
37
+ const segments = normalized.split('/');
38
+
39
+ if (segments.length <= 1) {
40
+ return normalized.startsWith('/') ? '/' : '.';
41
+ }
42
+
43
+ segments.pop();
44
+ const joined = segments.join('/');
45
+ return joined || '/';
46
+ }
47
+
48
+ function resolveLocalRef(currentPath, refValue) {
49
+ if (!currentPath || typeof refValue !== 'string') return null;
50
+ if (refValue.startsWith('#')) return null;
51
+ return normalizePathSegments(`${dirname(currentPath)}/${refValue}`);
52
+ }
53
+
54
+ function JsonIndent({ depth }) {
55
+ return <span>{' '.repeat(depth)}</span>;
56
+ }
57
+
58
+ function JsonPrimitive({
59
+ value,
60
+ propertyKey,
61
+ currentPath,
62
+ schemaSources,
63
+ onNavigate,
64
+ }) {
65
+ const interactiveRefClassName = 'schema-json-viewer__link';
66
+ const quotedValue = `${JSON.stringify(value)}`;
67
+
68
+ if (typeof value === 'string') {
69
+ if (propertyKey === '$ref') {
70
+ if (isExternalRef(value)) {
71
+ return (
72
+ <Link
73
+ className={interactiveRefClassName}
74
+ href={value}
75
+ target="_blank"
76
+ rel="noreferrer"
77
+ >
78
+ {quotedValue}
79
+ </Link>
80
+ );
81
+ }
82
+
83
+ const resolvedRef = resolveLocalRef(currentPath, value);
84
+ if (resolvedRef && schemaSources?.[resolvedRef]) {
85
+ return (
86
+ <button
87
+ type="button"
88
+ className={interactiveRefClassName}
89
+ onClick={() => onNavigate(resolvedRef)}
90
+ >
91
+ {quotedValue}
92
+ </button>
93
+ );
94
+ }
95
+ }
96
+
97
+ return <span className="token string">{quotedValue}</span>;
98
+ }
99
+
100
+ if (typeof value === 'number') {
101
+ return <span className="token number">{value}</span>;
102
+ }
103
+
104
+ if (typeof value === 'boolean') {
105
+ return <span className="token boolean">{String(value)}</span>;
106
+ }
107
+
108
+ return <span className="token null keyword">null</span>;
109
+ }
110
+
111
+ function JsonNode({
112
+ value,
113
+ depth,
114
+ currentPath,
115
+ schemaSources,
116
+ onNavigate,
117
+ propertyKey = null,
118
+ }) {
119
+ if (Array.isArray(value)) {
120
+ return (
121
+ <>
122
+ <span className="token punctuation">[</span>
123
+ {value.length > 0 && <br />}
124
+ {value.map((item, index) => (
125
+ <Fragment key={`${depth}-${index}`}>
126
+ <JsonIndent depth={depth + 1} />
127
+ <JsonNode
128
+ value={item}
129
+ depth={depth + 1}
130
+ currentPath={currentPath}
131
+ schemaSources={schemaSources}
132
+ onNavigate={onNavigate}
133
+ />
134
+ {index < value.length - 1 && (
135
+ <span className="token punctuation">,</span>
136
+ )}
137
+ <br />
138
+ </Fragment>
139
+ ))}
140
+ {value.length > 0 && <JsonIndent depth={depth} />}
141
+ <span className="token punctuation">]</span>
142
+ </>
143
+ );
144
+ }
145
+
146
+ if (isPlainObject(value)) {
147
+ const entries = Object.entries(value);
148
+ return (
149
+ <>
150
+ <span className="token punctuation">{'{'}</span>
151
+ {entries.length > 0 && <br />}
152
+ {entries.map(([key, child], index) => (
153
+ <Fragment key={`${depth}-${key}`}>
154
+ <JsonIndent depth={depth + 1} />
155
+ <span className="token property">{JSON.stringify(key)}</span>
156
+ <span className="token punctuation">: </span>
157
+ <JsonNode
158
+ value={child}
159
+ depth={depth + 1}
160
+ currentPath={currentPath}
161
+ schemaSources={schemaSources}
162
+ onNavigate={onNavigate}
163
+ propertyKey={key}
164
+ />
165
+ {index < entries.length - 1 && (
166
+ <span className="token punctuation">,</span>
167
+ )}
168
+ <br />
169
+ </Fragment>
170
+ ))}
171
+ {entries.length > 0 && <JsonIndent depth={depth} />}
172
+ <span className="token punctuation">{'}'}</span>
173
+ </>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <JsonPrimitive
179
+ value={value}
180
+ propertyKey={propertyKey}
181
+ currentPath={currentPath}
182
+ schemaSources={schemaSources}
183
+ onNavigate={onNavigate}
184
+ />
185
+ );
186
+ }
187
+
188
+ export default function SchemaJsonViewer({
189
+ schema,
190
+ sourcePath = null,
191
+ schemaSources = null,
192
+ }) {
193
+ const resolvedSchemaSources =
194
+ schemaSources || (sourcePath ? { [sourcePath]: schema } : {});
195
+ const rootPath = sourcePath;
196
+ const [currentPath, setCurrentPath] = useState(rootPath);
197
+
198
+ const currentSchema =
199
+ (currentPath && resolvedSchemaSources?.[currentPath]) || schema;
3
200
 
4
- export default function SchemaJsonViewer({ schema }) {
5
201
  return (
6
202
  <details className="schema-json-viewer">
7
203
  <summary>View Raw JSON Schema</summary>
8
- <CodeBlock language="json">{JSON.stringify(schema, null, 2)}</CodeBlock>
204
+ {rootPath && currentPath !== rootPath ? (
205
+ <div className="schema-json-viewer__controls">
206
+ <button
207
+ type="button"
208
+ className="schema-json-viewer__link"
209
+ onClick={() => setCurrentPath(rootPath)}
210
+ >
211
+ Back to root
212
+ </button>
213
+ </div>
214
+ ) : null}
215
+ <pre data-language="json">
216
+ <code className="language-json">
217
+ <JsonNode
218
+ value={currentSchema}
219
+ depth={0}
220
+ currentPath={currentPath}
221
+ schemaSources={resolvedSchemaSources}
222
+ onNavigate={setCurrentPath}
223
+ />
224
+ </code>
225
+ </pre>
9
226
  </details>
10
227
  );
11
228
  }
@@ -1,4 +1,4 @@
1
- .required-row {
1
+ .property-cell--required {
2
2
  background-color: rgba(var(--ifm-color-danger-rgb), 0.05);
3
3
  }
4
4
 
@@ -26,6 +26,26 @@
26
26
  margin-bottom: 1em;
27
27
  }
28
28
 
29
+ .schema-json-viewer__controls {
30
+ margin: 0.5rem 0;
31
+ }
32
+
33
+ .schema-json-viewer__link {
34
+ appearance: none;
35
+ background: none;
36
+ border: none;
37
+ color: var(--ifm-link-color);
38
+ cursor: pointer;
39
+ font: inherit;
40
+ padding: 0;
41
+ text-decoration: none;
42
+ }
43
+
44
+ .schema-json-viewer__link:hover {
45
+ color: var(--ifm-link-hover-color);
46
+ text-decoration: underline;
47
+ }
48
+
29
49
  /* --- Property Name and Container Symbol Styles --- */
30
50
 
31
51
  .property-name {
@@ -34,6 +54,61 @@
34
54
  gap: 4px;
35
55
  }
36
56
 
57
+ .property-name--keyword {
58
+ align-items: center;
59
+ }
60
+
61
+ .property-keyword-wrapper {
62
+ position: relative;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ }
66
+
67
+ .property-keyword-stack {
68
+ display: inline-flex;
69
+ flex-direction: column;
70
+ align-items: flex-start;
71
+ gap: 0.2rem;
72
+ }
73
+
74
+ .property-keyword {
75
+ font-size: 0.95em;
76
+ font-weight: 600;
77
+ }
78
+
79
+ .property-keyword-pattern {
80
+ font-size: 0.85em;
81
+ color: var(--ifm-color-emphasis-700);
82
+ }
83
+
84
+ .property-keyword-tooltip {
85
+ position: absolute;
86
+ left: 0;
87
+ bottom: calc(100% + 0.4rem);
88
+ z-index: 3;
89
+ min-width: 14rem;
90
+ max-width: 18rem;
91
+ padding: 0.5rem 0.625rem;
92
+ border-radius: 0.375rem;
93
+ background: var(--ifm-background-surface-color);
94
+ border: 1px solid var(--ifm-table-border-color);
95
+ box-shadow: var(--ifm-global-shadow-lw);
96
+ color: var(--ifm-font-color-base);
97
+ font-size: 0.8rem;
98
+ line-height: 1.35;
99
+ opacity: 0;
100
+ pointer-events: none;
101
+ transform: translateY(0.125rem);
102
+ transition:
103
+ opacity 120ms ease,
104
+ transform 120ms ease;
105
+ }
106
+
107
+ .property-keyword-wrapper:hover .property-keyword-tooltip {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+
37
112
  .container-symbol {
38
113
  font-family: var(--ifm-font-family-monospace);
39
114
  font-size: 0.9em;
@@ -67,6 +142,10 @@
67
142
  border-left: none;
68
143
  }
69
144
 
145
+ .schema-table td[colspan='5'] {
146
+ border-left: none;
147
+ }
148
+
70
149
  /*
71
150
  * Constraint-only continuation rows render a single cell; in those rows that
72
151
  * cell is also :first-child, but it still needs the separator before the
@@ -85,6 +164,18 @@
85
164
  box-shadow: inset 0 1px 0 var(--ifm-table-border-color);
86
165
  }
87
166
 
167
+ .schema-table tbody tr.schema-row--zebra-even {
168
+ background-color: var(--ifm-table-stripe-background);
169
+ }
170
+
171
+ .schema-table tbody tr.schema-row--zebra-odd {
172
+ background-color: transparent;
173
+ }
174
+
175
+ .schema-table tbody tr.schema-row--control {
176
+ background-color: transparent;
177
+ }
178
+
88
179
  /* --- Organigram Connector Line Styles --- */
89
180
 
90
181
  /*
@@ -108,10 +199,12 @@ td.has-children {
108
199
  td[class*='level-']::before {
109
200
  content: '';
110
201
  position: absolute;
111
- top: 0;
112
- bottom: 0;
202
+ top: -1px;
203
+ bottom: -1px;
113
204
  width: 0;
114
205
  border-left: 1px solid var(--ifm-table-border-color);
206
+ z-index: 1;
207
+ pointer-events: none;
115
208
  }
116
209
 
117
210
  /* Last items: stop vertical line at middle */
@@ -130,6 +223,8 @@ td[class*='level-']::after {
130
223
  height: 0;
131
224
  width: 0.75rem;
132
225
  border-bottom: 1px solid var(--ifm-table-border-color);
226
+ z-index: 1;
227
+ pointer-events: none;
133
228
  }
134
229
 
135
230
  /* --- Level-based positioning --- */
@@ -172,10 +267,6 @@ td.level-6::after {
172
267
 
173
268
  /* --- Choice Row Styles --- */
174
269
 
175
- .choice-row {
176
- background-color: var(--ifm-table-stripe-background);
177
- }
178
-
179
270
  .choice-row:hover {
180
271
  background-color: var(--ifm-hover-overlay);
181
272
  }
@@ -12,7 +12,11 @@ import ConditionalRows from './ConditionalRows';
12
12
  * @param {Array} props.tableData - Flat array of row objects
13
13
  * @param {Array} [props.bracketEnds] - Bracket descriptors that end on the last row
14
14
  */
15
- export default function SchemaRows({ tableData, bracketEnds }) {
15
+ export default function SchemaRows({
16
+ tableData,
17
+ bracketEnds,
18
+ stripeState = { current: 0 },
19
+ }) {
16
20
  if (!tableData) {
17
21
  return null;
18
22
  }
@@ -20,12 +24,15 @@ export default function SchemaRows({ tableData, bracketEnds }) {
20
24
  return tableData.map((row, index) => {
21
25
  const key = row.path.join('.');
22
26
  const isLast = index === tableData.length - 1;
27
+ const stripeIndex = stripeState.current++;
23
28
 
24
29
  if (row.type === 'choice') {
25
30
  return (
26
31
  <FoldableRows
27
32
  key={key}
28
33
  row={row}
34
+ stripeIndex={stripeIndex}
35
+ stripeState={stripeState}
29
36
  bracketEnds={isLast ? bracketEnds : undefined}
30
37
  />
31
38
  );
@@ -36,6 +43,8 @@ export default function SchemaRows({ tableData, bracketEnds }) {
36
43
  <ConditionalRows
37
44
  key={key}
38
45
  row={row}
46
+ stripeIndex={stripeIndex}
47
+ stripeState={stripeState}
39
48
  bracketEnds={isLast ? bracketEnds : undefined}
40
49
  />
41
50
  );
@@ -46,6 +55,7 @@ export default function SchemaRows({ tableData, bracketEnds }) {
46
55
  <PropertyRow
47
56
  key={key}
48
57
  row={row}
58
+ stripeIndex={stripeIndex}
49
59
  isLastInGroup={row.isLastInGroup}
50
60
  bracketEnds={isLast ? bracketEnds : undefined}
51
61
  />