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.
- package/README.md +2 -0
- package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
- package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +21 -3
- package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +26 -4
- package/__tests__/__snapshots__/generateEventDocs.test.js.snap +45 -6
- package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +16 -2
- package/__tests__/components/ConditionalRows.test.js +28 -0
- package/__tests__/components/FoldableRows.test.js +31 -290
- package/__tests__/components/PropertiesTable.test.js +66 -0
- package/__tests__/components/PropertyRow.test.js +297 -0
- package/__tests__/components/SchemaJsonViewer.test.js +194 -10
- package/__tests__/components/SchemaRows.test.js +62 -12
- package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
- package/__tests__/generateEventDocs.test.js +3 -0
- package/__tests__/helpers/example-helper.test.js +12 -0
- package/__tests__/helpers/getConstraints.test.js +16 -0
- package/__tests__/helpers/processSchema.test.js +18 -0
- package/__tests__/helpers/schemaToTableData.test.js +112 -0
- package/__tests__/helpers/schemaTraversal.test.js +110 -0
- package/__tests__/syncGtm.test.js +227 -3
- package/__tests__/validateSchemas.test.js +50 -0
- package/components/ConditionalRows.js +6 -3
- package/components/FoldableRows.js +9 -3
- package/components/PropertiesTable.js +34 -3
- package/components/PropertyRow.js +118 -6
- package/components/SchemaJsonViewer.js +324 -4
- package/components/SchemaRows.css +138 -7
- package/components/SchemaRows.js +11 -1
- package/components/SchemaViewer.js +11 -2
- package/generateEventDocs.js +87 -1
- package/helpers/choice-index-template.js +6 -2
- package/helpers/example-helper.js +2 -2
- package/helpers/file-system.js +28 -0
- package/helpers/getConstraints.js +20 -0
- package/helpers/processSchema.js +32 -1
- package/helpers/schema-doc-template.js +11 -1
- package/helpers/schemaToExamples.js +29 -35
- package/helpers/schemaToTableData.js +68 -7
- package/helpers/schemaTraversal.cjs +148 -0
- package/package.json +1 -1
- package/scripts/sync-gtm.js +41 -28
- package/test-data/payloadContracts.js +35 -0
- 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({
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
199
|
+
shouldCloseConnector && 'is-last',
|
|
131
200
|
hasChildren && 'has-children',
|
|
132
201
|
containerType && `container-${containerType}`,
|
|
133
202
|
)}
|
|
134
203
|
>
|
|
135
|
-
<span
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|