docusaurus-plugin-generate-schema-docs 1.8.2 → 1.8.4

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 (43) hide show
  1. package/README.md +2 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +21 -3
  4. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +26 -4
  5. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +45 -6
  6. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +16 -2
  7. package/__tests__/components/ConditionalRows.test.js +28 -0
  8. package/__tests__/components/FoldableRows.test.js +31 -290
  9. package/__tests__/components/PropertiesTable.test.js +66 -0
  10. package/__tests__/components/PropertyRow.test.js +297 -0
  11. package/__tests__/components/SchemaJsonViewer.test.js +194 -10
  12. package/__tests__/components/SchemaRows.test.js +62 -12
  13. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  14. package/__tests__/generateEventDocs.test.js +3 -0
  15. package/__tests__/helpers/example-helper.test.js +12 -0
  16. package/__tests__/helpers/getConstraints.test.js +16 -0
  17. package/__tests__/helpers/processSchema.test.js +18 -0
  18. package/__tests__/helpers/schemaToTableData.test.js +112 -0
  19. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  20. package/__tests__/syncGtm.test.js +227 -3
  21. package/__tests__/validateSchemas.test.js +50 -0
  22. package/components/ConditionalRows.js +6 -3
  23. package/components/FoldableRows.js +9 -3
  24. package/components/PropertiesTable.js +34 -3
  25. package/components/PropertyRow.js +118 -6
  26. package/components/SchemaJsonViewer.js +324 -4
  27. package/components/SchemaRows.css +138 -7
  28. package/components/SchemaRows.js +11 -1
  29. package/components/SchemaViewer.js +11 -2
  30. package/generateEventDocs.js +87 -1
  31. package/helpers/choice-index-template.js +6 -2
  32. package/helpers/example-helper.js +2 -2
  33. package/helpers/file-system.js +28 -0
  34. package/helpers/getConstraints.js +20 -0
  35. package/helpers/processSchema.js +32 -1
  36. package/helpers/schema-doc-template.js +11 -1
  37. package/helpers/schemaToExamples.js +29 -35
  38. package/helpers/schemaToTableData.js +68 -7
  39. package/helpers/schemaTraversal.cjs +148 -0
  40. package/package.json +1 -1
  41. package/scripts/sync-gtm.js +41 -28
  42. package/test-data/payloadContracts.js +35 -0
  43. package/validateSchemas.js +1 -1
@@ -18,7 +18,7 @@ const ChoiceRow = ({
18
18
  name,
19
19
  continuingLinesStyle,
20
20
  }) => (
21
- <tr className="choice-row">
21
+ <tr className="choice-row schema-row--control">
22
22
  <td colSpan={5} style={continuingLinesStyle}>
23
23
  <label className="choice-row-header">
24
24
  <input
@@ -41,7 +41,12 @@ const ChoiceRow = ({
41
41
  * Renders 'oneOf' and 'anyOf' choices as a set of foldable `<tr>` elements
42
42
  * that integrate directly into the main table body.
43
43
  */
44
- export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
44
+ export default function FoldableRows({
45
+ row,
46
+ stripeIndex = 0,
47
+ stripeState,
48
+ bracketEnds: parentBracketEnds,
49
+ }) {
45
50
  const {
46
51
  choiceType,
47
52
  options,
@@ -136,7 +141,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
136
141
  return (
137
142
  <>
138
143
  {/* A header row for the entire choice block */}
139
- <tr>
144
+ <tr className="schema-row--control">
140
145
  <td colSpan={5} style={headerStyle}>
141
146
  <Heading as="h4" className="choice-row-header-headline">
142
147
  {header}
@@ -176,6 +181,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
176
181
  {isActive && (
177
182
  <SchemaRows
178
183
  tableData={option.rows}
184
+ stripeState={stripeState}
179
185
  bracketEnds={
180
186
  isLastOption
181
187
  ? [ownBracket, ...(parentBracketEnds || [])]
@@ -5,9 +5,40 @@ import WordWrapButton from './WordWrapButton';
5
5
  import { schemaToTableData } from '../helpers/schemaToTableData';
6
6
  import styles from './PropertiesTable.module.css';
7
7
 
8
- export default function PropertiesTable({ schema }) {
8
+ function filterInheritedTopLevelProperties(schema, sourceSchema) {
9
+ if (!schema?.properties || !sourceSchema?.properties) {
10
+ return schema;
11
+ }
12
+
13
+ const sourceKeys = new Set(Object.keys(sourceSchema.properties));
14
+ const filteredEntries = Object.entries(schema.properties).filter(
15
+ ([key, propSchema]) => {
16
+ if (sourceKeys.has(key)) {
17
+ return true;
18
+ }
19
+
20
+ const hasDescription =
21
+ typeof propSchema?.description === 'string' &&
22
+ propSchema.description.trim().length > 0;
23
+ const hasExamples =
24
+ Array.isArray(propSchema?.examples) && propSchema.examples.length > 0;
25
+ const hasExample = propSchema?.example !== undefined;
26
+
27
+ return hasDescription || hasExamples || hasExample;
28
+ },
29
+ );
30
+
31
+ return {
32
+ ...schema,
33
+ properties: Object.fromEntries(filteredEntries),
34
+ };
35
+ }
36
+
37
+ export default function PropertiesTable({ schema, sourceSchema }) {
9
38
  const [isWordWrapOn, setIsWordWrapOn] = useState(true);
10
- const tableData = schemaToTableData(schema);
39
+ const tableSchema = filterInheritedTopLevelProperties(schema, sourceSchema);
40
+ const tableData = schemaToTableData(tableSchema);
41
+ const stripeState = { current: 0 };
11
42
 
12
43
  return (
13
44
  <div
@@ -24,7 +55,7 @@ export default function PropertiesTable({ schema }) {
24
55
  <table className="schema-table">
25
56
  <TableHeader />
26
57
  <tbody>
27
- <SchemaRows tableData={tableData} />
58
+ <SchemaRows tableData={tableData} stripeState={stripeState} />
28
59
  </tbody>
29
60
  </table>
30
61
  </div>
@@ -26,12 +26,60 @@ const getContainerSymbol = (containerType) => {
26
26
  return '';
27
27
  };
28
28
 
29
+ const formatPropertyType = (value) => {
30
+ if (Array.isArray(value)) {
31
+ return value.join(', ');
32
+ }
33
+ if (typeof value === 'string') {
34
+ return value;
35
+ }
36
+ if (value === undefined || value === null) {
37
+ return '';
38
+ }
39
+ return JSON.stringify(value);
40
+ };
41
+
42
+ const KEYWORD_HELP_TEXT = {
43
+ additionalProperties:
44
+ 'Controls properties not listed in properties and not matched by patternProperties.',
45
+ patternProperties:
46
+ 'Applies the subschema to property names that match the given regular expression.',
47
+ };
48
+
49
+ const SCHEMA_KEYWORD_BADGE_TEXT = 'Schema constraint';
50
+
51
+ function splitKeywordLabel(name) {
52
+ const match = /^patternProperties (\/.+\/)$/.exec(name);
53
+ if (!match) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ keyword: 'patternProperties',
59
+ pattern: match[1],
60
+ };
61
+ }
62
+
63
+ function buildKeywordHelpId(name, rowPath) {
64
+ if (!rowPath || rowPath.length === 0) {
65
+ return `schema-keyword-help-${name}`;
66
+ }
67
+
68
+ const normalizedPath = rowPath.join('-').replace(/[^a-zA-Z0-9_-]/g, '_');
69
+ return `schema-keyword-help-${normalizedPath}`;
70
+ }
71
+
29
72
  /**
30
73
  * Renders a single property row in the schema table.
31
74
  * All data is passed in via the `row` prop, which comes from `tableData`.
32
75
  * This component handles multi-row constraints using `rowSpan`.
33
76
  */
34
- export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
77
+ export default function PropertyRow({
78
+ row,
79
+ stripeIndex,
80
+ isLastInGroup,
81
+ bracketEnds,
82
+ }) {
35
83
  const {
36
84
  name,
37
85
  level,
@@ -44,6 +92,8 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
44
92
  containerType,
45
93
  continuingLevels = [],
46
94
  groupBrackets = [],
95
+ isSchemaKeywordRow = false,
96
+ keepConnectorOpen = false,
47
97
  } = row;
48
98
 
49
99
  const indentStyle = {
@@ -112,11 +162,27 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
112
162
  const bracketStyle = getBracketLinesStyle(groupBrackets, bracketCaps);
113
163
 
114
164
  const containerSymbol = getContainerSymbol(containerType);
165
+ const shouldCloseConnector = isLastInGroup && !keepConnectorOpen;
166
+ const splitKeyword = splitKeywordLabel(name);
167
+ const keywordHelpKey = name.startsWith('patternProperties ')
168
+ ? 'patternProperties'
169
+ : name;
170
+ const keywordHelpText = KEYWORD_HELP_TEXT[keywordHelpKey];
171
+ const keywordHelpId = keywordHelpText
172
+ ? buildKeywordHelpId(name, row.path)
173
+ : undefined;
174
+ const zebraClassName =
175
+ stripeIndex === undefined
176
+ ? undefined
177
+ : stripeIndex % 2 === 0
178
+ ? 'schema-row--zebra-even'
179
+ : 'schema-row--zebra-odd';
115
180
 
116
181
  return (
117
182
  <>
118
183
  <tr
119
184
  className={clsx(
185
+ zebraClassName,
120
186
  required && 'required-row',
121
187
  row.isCondition && 'conditional-condition-row',
122
188
  )}
@@ -126,21 +192,64 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
126
192
  style={{ ...indentStyle, ...continuingLinesStyle }}
127
193
  className={clsx(
128
194
  'property-cell',
195
+ isSchemaKeywordRow && 'property-cell--keyword',
196
+ required && 'property-cell--required',
197
+ level > 0 && 'property-cell--tree',
129
198
  level > 0 && `level-${level}`,
130
- isLastInGroup && 'is-last',
199
+ shouldCloseConnector && 'is-last',
131
200
  hasChildren && 'has-children',
132
201
  containerType && `container-${containerType}`,
133
202
  )}
134
203
  >
135
- <span className="property-name">
204
+ <span
205
+ className={clsx(
206
+ 'property-name',
207
+ isSchemaKeywordRow && 'property-name--keyword',
208
+ )}
209
+ >
136
210
  {containerSymbol && (
137
211
  <span className="container-symbol">{containerSymbol}</span>
138
212
  )}
139
- <strong>{name}</strong>
213
+ {isSchemaKeywordRow ? (
214
+ <span className="property-keyword-wrapper">
215
+ {splitKeyword ? (
216
+ <span className="property-keyword-stack">
217
+ <code
218
+ className="property-keyword"
219
+ aria-describedby={keywordHelpId}
220
+ >
221
+ {splitKeyword.keyword}
222
+ </code>
223
+ <code className="property-keyword-pattern">
224
+ {splitKeyword.pattern}
225
+ </code>
226
+ </span>
227
+ ) : (
228
+ <code
229
+ className="property-keyword"
230
+ aria-describedby={keywordHelpId}
231
+ >
232
+ {name}
233
+ </code>
234
+ )}
235
+ <span className="property-keyword-badge">
236
+ {SCHEMA_KEYWORD_BADGE_TEXT}
237
+ </span>
238
+ <span
239
+ id={keywordHelpId}
240
+ className="property-keyword-tooltip"
241
+ role="tooltip"
242
+ >
243
+ {keywordHelpText}
244
+ </span>
245
+ </span>
246
+ ) : (
247
+ <strong>{name}</strong>
248
+ )}
140
249
  </span>
141
250
  </td>
142
251
  <td rowSpan={rowSpan}>
143
- <code>{propertyType}</code>
252
+ <code>{formatPropertyType(propertyType)}</code>
144
253
  </td>
145
254
 
146
255
  {/* The first constraint cell */}
@@ -181,7 +290,10 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
181
290
 
182
291
  {/* Render subsequent constraints in their own rows */}
183
292
  {remainingConstraints.map((constraint) => (
184
- <tr className={clsx(required && 'required-row')} key={constraint}>
293
+ <tr
294
+ className={clsx(zebraClassName, required && 'required-row')}
295
+ key={constraint}
296
+ >
185
297
  <td className="constraint-cell">
186
298
  <code
187
299
  className={clsx(
@@ -1,11 +1,331 @@
1
- import React from 'react';
2
- import CodeBlock from '@theme/CodeBlock';
1
+ import React, { useState } from 'react';
2
+ import Link from '@docusaurus/Link';
3
+ import { usePrismTheme } from '@docusaurus/theme-common';
4
+ import { Highlight } from 'prism-react-renderer';
5
+
6
+ const SCHEMA_META_KEYS = [
7
+ '$schema',
8
+ '$id',
9
+ '$anchor',
10
+ '$dynamicAnchor',
11
+ '$comment',
12
+ '$vocabulary',
13
+ ];
14
+
15
+ const SCHEMA_STRUCTURAL_KEYS = new Set([
16
+ '$ref',
17
+ '$defs',
18
+ 'properties',
19
+ 'required',
20
+ 'allOf',
21
+ 'anyOf',
22
+ 'oneOf',
23
+ 'if',
24
+ 'then',
25
+ 'else',
26
+ 'not',
27
+ 'items',
28
+ 'prefixItems',
29
+ 'contains',
30
+ 'dependentSchemas',
31
+ 'patternProperties',
32
+ 'additionalProperties',
33
+ ]);
34
+
35
+ const SCHEMA_NAME_MAP_KEYS = new Set([
36
+ 'properties',
37
+ 'patternProperties',
38
+ '$defs',
39
+ 'dependentSchemas',
40
+ ]);
41
+
42
+ function isExternalRef(value) {
43
+ return typeof value === 'string' && /^https?:\/\//.test(value);
44
+ }
45
+
46
+ function normalizePathSegments(pathValue) {
47
+ const normalized = pathValue.replace(/\\/g, '/');
48
+ const isAbsolute = normalized.startsWith('/');
49
+ const segments = normalized.split('/');
50
+ const resolvedSegments = [];
51
+
52
+ segments.forEach((segment) => {
53
+ if (!segment || segment === '.') return;
54
+ if (segment === '..') {
55
+ resolvedSegments.pop();
56
+ return;
57
+ }
58
+ resolvedSegments.push(segment);
59
+ });
60
+
61
+ return `${isAbsolute ? '/' : ''}${resolvedSegments.join('/')}`;
62
+ }
63
+
64
+ function dirname(pathValue) {
65
+ const normalized = normalizePathSegments(pathValue);
66
+ const segments = normalized.split('/');
67
+
68
+ if (segments.length <= 1) {
69
+ return normalized.startsWith('/') ? '/' : '.';
70
+ }
71
+
72
+ segments.pop();
73
+ const joined = segments.join('/');
74
+ return joined || '/';
75
+ }
76
+
77
+ function resolveLocalRef(currentPath, refValue) {
78
+ if (!currentPath || typeof refValue !== 'string') return null;
79
+ if (refValue.startsWith('#')) return null;
80
+ return normalizePathSegments(`${dirname(currentPath)}/${refValue}`);
81
+ }
82
+
83
+ function getSchemaKeywordClassName(key, parentKey) {
84
+ if (parentKey && SCHEMA_NAME_MAP_KEYS.has(parentKey)) {
85
+ return '';
86
+ }
87
+
88
+ if (SCHEMA_META_KEYS.includes(key)) {
89
+ return 'schema-json-viewer__keyword schema-json-viewer__keyword--meta';
90
+ }
91
+
92
+ if (SCHEMA_STRUCTURAL_KEYS.has(key)) {
93
+ return 'schema-json-viewer__keyword schema-json-viewer__keyword--structural';
94
+ }
95
+
96
+ return '';
97
+ }
98
+
99
+ function joinClassNames(...classNames) {
100
+ return classNames.filter(Boolean).join(' ');
101
+ }
102
+
103
+ function createParserState() {
104
+ return {
105
+ stack: [],
106
+ };
107
+ }
108
+
109
+ function beginNestedValue(state, tokenContent) {
110
+ const currentContext = state.stack[state.stack.length - 1];
111
+ let parentKey = null;
112
+
113
+ if (currentContext?.type === 'object' && currentContext.afterColon) {
114
+ parentKey = currentContext.currentKey;
115
+ currentContext.currentKey = null;
116
+ currentContext.afterColon = false;
117
+ }
118
+
119
+ state.stack.push({
120
+ type: tokenContent === '{' ? 'object' : 'array',
121
+ parentKey,
122
+ currentKey: null,
123
+ afterColon: false,
124
+ });
125
+ }
126
+
127
+ function classifyRenderedToken(state, token) {
128
+ const currentContext = state.stack[state.stack.length - 1];
129
+ const tokenTypes = new Set(token.types);
130
+ const content = token.content;
131
+ const semantic = {};
132
+
133
+ if (!content) {
134
+ return semantic;
135
+ }
136
+
137
+ if (tokenTypes.has('property')) {
138
+ const key = JSON.parse(content);
139
+ semantic.propertyKey = key;
140
+ semantic.parentKey = currentContext?.parentKey ?? null;
141
+
142
+ if (currentContext?.type === 'object') {
143
+ currentContext.currentKey = key;
144
+ currentContext.afterColon = false;
145
+ }
146
+
147
+ return semantic;
148
+ }
149
+
150
+ if (content === ':') {
151
+ if (
152
+ currentContext?.type === 'object' &&
153
+ currentContext.currentKey !== null
154
+ ) {
155
+ currentContext.afterColon = true;
156
+ }
157
+ return semantic;
158
+ }
159
+
160
+ if (content === '{' || content === '[') {
161
+ beginNestedValue(state, content);
162
+ return semantic;
163
+ }
164
+
165
+ if (content === '}' || content === ']') {
166
+ state.stack.pop();
167
+ return semantic;
168
+ }
169
+
170
+ if (content === ',') {
171
+ if (currentContext?.type === 'object') {
172
+ currentContext.currentKey = null;
173
+ currentContext.afterColon = false;
174
+ }
175
+ return semantic;
176
+ }
177
+
178
+ if (currentContext?.type === 'object' && currentContext.afterColon) {
179
+ if (tokenTypes.has('string') && content.trim().startsWith('"')) {
180
+ semantic.valueKey = currentContext.currentKey;
181
+ semantic.stringValue = JSON.parse(content);
182
+ currentContext.currentKey = null;
183
+ currentContext.afterColon = false;
184
+ return semantic;
185
+ }
186
+
187
+ if (
188
+ tokenTypes.has('number') ||
189
+ tokenTypes.has('boolean') ||
190
+ (tokenTypes.has('keyword') && content === 'null')
191
+ ) {
192
+ currentContext.currentKey = null;
193
+ currentContext.afterColon = false;
194
+ }
195
+ }
196
+
197
+ return semantic;
198
+ }
199
+
200
+ function renderToken({
201
+ token,
202
+ tokenIndex,
203
+ getTokenProps,
204
+ semantic,
205
+ currentPath,
206
+ schemaSources,
207
+ onNavigate,
208
+ }) {
209
+ const tokenProps = getTokenProps({ token, key: tokenIndex });
210
+ const propertyKeyClassName = semantic.propertyKey
211
+ ? getSchemaKeywordClassName(semantic.propertyKey, semantic.parentKey)
212
+ : '';
213
+ const className = joinClassNames(tokenProps.className, propertyKeyClassName);
214
+
215
+ if (
216
+ semantic.valueKey === '$ref' &&
217
+ typeof semantic.stringValue === 'string'
218
+ ) {
219
+ if (isExternalRef(semantic.stringValue)) {
220
+ return (
221
+ <Link
222
+ key={tokenIndex}
223
+ className={joinClassNames(
224
+ className,
225
+ 'schema-json-viewer__link',
226
+ 'schema-json-viewer__ref-link',
227
+ )}
228
+ style={tokenProps.style}
229
+ href={semantic.stringValue}
230
+ target="_blank"
231
+ rel="noreferrer"
232
+ >
233
+ {token.content}
234
+ </Link>
235
+ );
236
+ }
237
+
238
+ const resolvedRef = resolveLocalRef(currentPath, semantic.stringValue);
239
+ if (resolvedRef && schemaSources?.[resolvedRef]) {
240
+ return (
241
+ <button
242
+ key={tokenIndex}
243
+ type="button"
244
+ className={joinClassNames(
245
+ className,
246
+ 'schema-json-viewer__link',
247
+ 'schema-json-viewer__ref-link',
248
+ )}
249
+ style={tokenProps.style}
250
+ onClick={() => onNavigate(resolvedRef)}
251
+ >
252
+ {token.content}
253
+ </button>
254
+ );
255
+ }
256
+ }
257
+
258
+ return (
259
+ <span
260
+ key={tokenIndex}
261
+ className={className || tokenProps.className}
262
+ style={tokenProps.style}
263
+ >
264
+ {token.content}
265
+ </span>
266
+ );
267
+ }
268
+
269
+ export default function SchemaJsonViewer({
270
+ schema,
271
+ sourcePath = null,
272
+ schemaSources = null,
273
+ }) {
274
+ const prismTheme = usePrismTheme();
275
+ const resolvedSchemaSources =
276
+ schemaSources || (sourcePath ? { [sourcePath]: schema } : {});
277
+ const rootPath = sourcePath;
278
+ const [currentPath, setCurrentPath] = useState(rootPath);
279
+
280
+ const currentSchema =
281
+ (currentPath && resolvedSchemaSources?.[currentPath]) || schema;
282
+ const formattedSchema = JSON.stringify(currentSchema, null, 2);
3
283
 
4
- export default function SchemaJsonViewer({ schema }) {
5
284
  return (
6
285
  <details className="schema-json-viewer">
7
286
  <summary>View Raw JSON Schema</summary>
8
- <CodeBlock language="json">{JSON.stringify(schema, null, 2)}</CodeBlock>
287
+ {rootPath && currentPath !== rootPath ? (
288
+ <div className="schema-json-viewer__controls">
289
+ <button
290
+ type="button"
291
+ className="schema-json-viewer__link"
292
+ onClick={() => setCurrentPath(rootPath)}
293
+ >
294
+ Back to root
295
+ </button>
296
+ </div>
297
+ ) : null}
298
+ <Highlight code={formattedSchema} language="json" theme={prismTheme}>
299
+ {({ className, style, tokens, getLineProps, getTokenProps }) => {
300
+ const parserState = createParserState();
301
+
302
+ return (
303
+ <pre className={className} style={style} data-language="json">
304
+ <code className="language-json">
305
+ {tokens.map((line, lineIndex) => (
306
+ <span
307
+ key={lineIndex}
308
+ {...getLineProps({ line, key: lineIndex })}
309
+ >
310
+ {line.map((token, tokenIndex) =>
311
+ renderToken({
312
+ token,
313
+ tokenIndex,
314
+ getTokenProps,
315
+ semantic: classifyRenderedToken(parserState, token),
316
+ currentPath,
317
+ schemaSources: resolvedSchemaSources,
318
+ onNavigate: setCurrentPath,
319
+ }),
320
+ )}
321
+ {'\n'}
322
+ </span>
323
+ ))}
324
+ </code>
325
+ </pre>
326
+ );
327
+ }}
328
+ </Highlight>
9
329
  </details>
10
330
  );
11
331
  }