docusaurus-plugin-generate-schema-docs 1.8.2 → 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.
@@ -1,17 +1,21 @@
1
+ /* eslint-disable @docusaurus/no-html-links */
1
2
  import '@testing-library/jest-dom';
2
3
  import React from 'react';
3
- import { render } from '@testing-library/react';
4
+ import { fireEvent, render, screen } from '@testing-library/react';
4
5
  import SchemaJsonViewer from '../../components/SchemaJsonViewer';
5
6
 
6
- // Mock CodeBlock as it's an external theme component
7
- jest.mock('@theme/CodeBlock', () => {
8
- return function DummyCodeBlock({ children, language }) {
9
- return <pre data-language={language}>{children}</pre>;
10
- };
11
- });
7
+ jest.mock(
8
+ '@docusaurus/Link',
9
+ () => {
10
+ return function DocusaurusLink({ children, ...props }) {
11
+ return <a {...props}>{children}</a>;
12
+ };
13
+ },
14
+ { virtual: true },
15
+ );
12
16
 
13
17
  describe('SchemaJsonViewer', () => {
14
- it('renders the schema in a CodeBlock', () => {
18
+ it('renders the schema in a syntax-highlighted pre block', () => {
15
19
  const schema = {
16
20
  type: 'object',
17
21
  properties: {
@@ -29,8 +33,70 @@ describe('SchemaJsonViewer', () => {
29
33
  const codeBlockElement = container.querySelector('pre');
30
34
  expect(codeBlockElement).toBeInTheDocument();
31
35
  expect(codeBlockElement).toHaveAttribute('data-language', 'json');
32
- expect(codeBlockElement.textContent).toEqual(
33
- JSON.stringify(schema, null, 2),
36
+ expect(codeBlockElement.textContent.replace(/\s+/g, '')).toEqual(
37
+ JSON.stringify(schema, null, 2).replace(/\s+/g, ''),
38
+ );
39
+ });
40
+
41
+ it('navigates local $ref values inside the viewer and resets to root', () => {
42
+ const rootSchema = {
43
+ type: 'object',
44
+ properties: {
45
+ component: { $ref: './components/referenced.json' },
46
+ },
47
+ };
48
+ const referencedSchema = {
49
+ title: 'Referenced Component',
50
+ type: 'object',
51
+ properties: {
52
+ prop: { type: 'string' },
53
+ },
54
+ };
55
+
56
+ render(
57
+ <SchemaJsonViewer
58
+ schema={rootSchema}
59
+ sourcePath="main-schema.json"
60
+ schemaSources={{
61
+ 'main-schema.json': rootSchema,
62
+ 'components/referenced.json': referencedSchema,
63
+ }}
64
+ />,
34
65
  );
66
+
67
+ fireEvent.click(screen.getByText('View Raw JSON Schema'));
68
+ fireEvent.click(
69
+ screen.getByRole('button', { name: '"./components/referenced.json"' }),
70
+ );
71
+
72
+ expect(screen.getAllByText(/Referenced Component/).length).toBeGreaterThan(
73
+ 0,
74
+ );
75
+ expect(
76
+ screen.getByRole('button', { name: 'Back to root' }),
77
+ ).toBeInTheDocument();
78
+
79
+ fireEvent.click(screen.getByRole('button', { name: 'Back to root' }));
80
+
81
+ expect(
82
+ screen.getByText('"./components/referenced.json"'),
83
+ ).toBeInTheDocument();
84
+ });
85
+
86
+ it('renders external $ref values as new-tab links', () => {
87
+ const schema = {
88
+ $ref: 'https://example.com/schema.json',
89
+ };
90
+
91
+ render(<SchemaJsonViewer schema={schema} sourcePath="root.json" />);
92
+
93
+ fireEvent.click(screen.getByText('View Raw JSON Schema'));
94
+ const refLink = screen.getByRole('link', {
95
+ name: '"https://example.com/schema.json"',
96
+ });
97
+
98
+ expect(refLink).toHaveAttribute('href', 'https://example.com/schema.json');
99
+ expect(refLink).toHaveAttribute('target', '_blank');
100
+ expect(refLink).toHaveAttribute('rel', 'noreferrer');
35
101
  });
36
102
  });
@@ -7,7 +7,9 @@ import SchemaRows from '../../components/SchemaRows';
7
7
  jest.mock('../../components/PropertyRow', () => {
8
8
  const MockPropertyRow = (props) => (
9
9
  <tr>
10
- <td>Mocked PropertyRow: {props.row.name}</td>
10
+ <td>
11
+ Mocked PropertyRow: {props.row.name} ({props.stripeIndex})
12
+ </td>
11
13
  </tr>
12
14
  );
13
15
  MockPropertyRow.displayName = 'MockPropertyRow';
@@ -17,7 +19,10 @@ jest.mock('../../components/PropertyRow', () => {
17
19
  jest.mock('../../components/FoldableRows', () => {
18
20
  const MockFoldableRows = (props) => (
19
21
  <tr>
20
- <td>Mocked FoldableRows: {props.row.choiceType}</td>
22
+ <td>
23
+ Mocked FoldableRows: {props.row.choiceType} ({props.stripeIndex},{' '}
24
+ {String(!!props.stripeState)})
25
+ </td>
21
26
  </tr>
22
27
  );
23
28
  MockFoldableRows.displayName = 'MockFoldableRows';
@@ -27,7 +32,10 @@ jest.mock('../../components/FoldableRows', () => {
27
32
  jest.mock('../../components/ConditionalRows', () => {
28
33
  const MockConditionalRows = (props) => (
29
34
  <tr>
30
- <td>Mocked ConditionalRows: {props.row.condition.title}</td>
35
+ <td>
36
+ Mocked ConditionalRows: {props.row.condition.title} ({props.stripeIndex}
37
+ , {String(!!props.stripeState)})
38
+ </td>
31
39
  </tr>
32
40
  );
33
41
  MockConditionalRows.displayName = 'MockConditionalRows';
@@ -49,8 +57,8 @@ describe('SchemaRows', () => {
49
57
  </table>,
50
58
  );
51
59
 
52
- expect(getByText('Mocked PropertyRow: name')).toBeInTheDocument();
53
- expect(getByText('Mocked PropertyRow: age')).toBeInTheDocument();
60
+ expect(getByText('Mocked PropertyRow: name (0)')).toBeInTheDocument();
61
+ expect(getByText('Mocked PropertyRow: age (1)')).toBeInTheDocument();
54
62
  });
55
63
 
56
64
  it('renders nested properties from a flat list', () => {
@@ -68,8 +76,8 @@ describe('SchemaRows', () => {
68
76
  );
69
77
 
70
78
  // It should render both the parent and child property from the flat list
71
- expect(getByText('Mocked PropertyRow: user')).toBeInTheDocument();
72
- expect(getByText('Mocked PropertyRow: id')).toBeInTheDocument();
79
+ expect(getByText('Mocked PropertyRow: user (0)')).toBeInTheDocument();
80
+ expect(getByText('Mocked PropertyRow: id (1)')).toBeInTheDocument();
73
81
  });
74
82
 
75
83
  it('renders a FoldableRows for choice type items in tableData', () => {
@@ -90,7 +98,9 @@ describe('SchemaRows', () => {
90
98
  </table>,
91
99
  );
92
100
 
93
- expect(getByText('Mocked FoldableRows: oneOf')).toBeInTheDocument();
101
+ expect(
102
+ getByText('Mocked FoldableRows: oneOf (0, true)'),
103
+ ).toBeInTheDocument();
94
104
  });
95
105
 
96
106
  it('renders a mix of properties and choices', () => {
@@ -113,9 +123,11 @@ describe('SchemaRows', () => {
113
123
  </table>,
114
124
  );
115
125
 
116
- expect(getByText('Mocked PropertyRow: prop1')).toBeInTheDocument();
117
- expect(getByText('Mocked FoldableRows: anyOf')).toBeInTheDocument();
118
- expect(getByText('Mocked PropertyRow: prop2')).toBeInTheDocument();
126
+ expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
127
+ expect(
128
+ getByText('Mocked FoldableRows: anyOf (1, true)'),
129
+ ).toBeInTheDocument();
130
+ expect(getByText('Mocked PropertyRow: prop2 (2)')).toBeInTheDocument();
119
131
  });
120
132
 
121
133
  it('renders a ConditionalRows for conditional type items in tableData', () => {
@@ -136,6 +148,44 @@ describe('SchemaRows', () => {
136
148
  </table>,
137
149
  );
138
150
 
139
- expect(getByText('Mocked ConditionalRows: If')).toBeInTheDocument();
151
+ expect(
152
+ getByText('Mocked ConditionalRows: If (0, true)'),
153
+ ).toBeInTheDocument();
154
+ });
155
+
156
+ it('increments stripe indices across logical rows', () => {
157
+ const tableData = [
158
+ { type: 'property', name: 'prop1', path: ['prop1'] },
159
+ {
160
+ type: 'choice',
161
+ choiceType: 'anyOf',
162
+ path: ['choice'],
163
+ options: [],
164
+ },
165
+ {
166
+ type: 'conditional',
167
+ path: ['if/then/else'],
168
+ condition: { title: 'If', rows: [] },
169
+ branches: [],
170
+ },
171
+ { type: 'property', name: 'prop2', path: ['prop2'] },
172
+ ];
173
+
174
+ const { getByText } = render(
175
+ <table>
176
+ <tbody>
177
+ <SchemaRows tableData={tableData} />
178
+ </tbody>
179
+ </table>,
180
+ );
181
+
182
+ expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
183
+ expect(
184
+ getByText('Mocked FoldableRows: anyOf (1, true)'),
185
+ ).toBeInTheDocument();
186
+ expect(
187
+ getByText('Mocked ConditionalRows: If (2, true)'),
188
+ ).toBeInTheDocument();
189
+ expect(getByText('Mocked PropertyRow: prop2 (3)')).toBeInTheDocument();
140
190
  });
141
191
  });
@@ -1,7 +1,7 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
- exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
3
+ exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
4
4
 
5
- exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
5
+ exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
6
6
 
7
- exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
7
+ exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell property-cell--required property-cell--tree level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
@@ -56,6 +56,9 @@ describe('generateEventDocs (non-versioned)', () => {
56
56
  'utf-8',
57
57
  );
58
58
  expect(addToCart).toMatchSnapshot();
59
+ expect(addToCart).toContain('sourcePath={"add-to-cart-event.json"}');
60
+ expect(addToCart).toContain(JSON.stringify('components/product.json'));
61
+ expect(addToCart).toContain('"$ref":"./components/product.json"');
59
62
 
60
63
  const choiceEvent = fs.readFileSync(
61
64
  path.join(outputDir, 'choice-event.mdx'),
@@ -217,6 +217,118 @@ describe('schemaToTableData', () => {
217
217
  expect(tableData[0].examples).toEqual(['default-value']);
218
218
  });
219
219
 
220
+ it('renders schema-valued additionalProperties as a synthetic row', () => {
221
+ const schema = {
222
+ properties: {
223
+ metadata: {
224
+ type: 'object',
225
+ description: 'Free-form metadata.',
226
+ additionalProperties: {
227
+ type: 'string',
228
+ description: 'Metadata value.',
229
+ pattern: '^[a-z]+$',
230
+ },
231
+ },
232
+ },
233
+ };
234
+
235
+ const tableData = schemaToTableData(schema);
236
+
237
+ const metadataRow = tableData.find((row) => row.name === 'metadata');
238
+ expect(metadataRow).toBeDefined();
239
+ expect(metadataRow.containerType).toBe('object');
240
+
241
+ const additionalPropertiesRow = tableData.find(
242
+ (row) =>
243
+ row.type === 'property' &&
244
+ row.name === 'additionalProperties' &&
245
+ row.level === 1,
246
+ );
247
+
248
+ expect(additionalPropertiesRow).toBeDefined();
249
+ expect(additionalPropertiesRow.propertyType).toBe('string');
250
+ expect(additionalPropertiesRow.description).toBe('Metadata value.');
251
+ expect(additionalPropertiesRow.constraints).toContain(
252
+ 'pattern: /^[a-z]+$/',
253
+ );
254
+ expect(additionalPropertiesRow.isSchemaKeywordRow).toBe(true);
255
+ expect(additionalPropertiesRow.keepConnectorOpen).toBe(false);
256
+ expect(additionalPropertiesRow.isLastInGroup).toBe(true);
257
+ });
258
+
259
+ it('treats additionalProperties as the last sibling after declared properties', () => {
260
+ const schema = {
261
+ properties: {
262
+ user_properties: {
263
+ type: 'object',
264
+ properties: {
265
+ allow_ad_personalization_signals: {
266
+ type: ['string', 'null'],
267
+ },
268
+ },
269
+ additionalProperties: {
270
+ type: ['string', 'null'],
271
+ },
272
+ },
273
+ },
274
+ };
275
+
276
+ const tableData = schemaToTableData(schema);
277
+
278
+ const explicitPropertyRow = tableData.find(
279
+ (row) =>
280
+ row.name === 'allow_ad_personalization_signals' && row.level === 1,
281
+ );
282
+ const additionalPropertiesRow = tableData.find(
283
+ (row) => row.name === 'additionalProperties' && row.level === 1,
284
+ );
285
+
286
+ expect(explicitPropertyRow).toBeDefined();
287
+ expect(additionalPropertiesRow).toBeDefined();
288
+ expect(explicitPropertyRow.isLastInGroup).toBe(false);
289
+ expect(additionalPropertiesRow.isLastInGroup).toBe(true);
290
+ });
291
+
292
+ it('renders schema-valued patternProperties as synthetic keyword rows', () => {
293
+ const schema = {
294
+ properties: {
295
+ attributes: {
296
+ type: 'object',
297
+ properties: {
298
+ segment: { type: 'string' },
299
+ },
300
+ patternProperties: {
301
+ '^custom_': {
302
+ type: 'number',
303
+ description: 'Numeric custom attribute values.',
304
+ minimum: 0,
305
+ },
306
+ },
307
+ },
308
+ },
309
+ };
310
+
311
+ const tableData = schemaToTableData(schema);
312
+
313
+ const explicitPropertyRow = tableData.find(
314
+ (row) => row.name === 'segment' && row.level === 1,
315
+ );
316
+ const patternPropertyRow = tableData.find(
317
+ (row) => row.name === 'patternProperties /^custom_/' && row.level === 1,
318
+ );
319
+
320
+ expect(explicitPropertyRow).toBeDefined();
321
+ expect(patternPropertyRow).toBeDefined();
322
+ expect(explicitPropertyRow.isLastInGroup).toBe(false);
323
+ expect(patternPropertyRow.isLastInGroup).toBe(true);
324
+ expect(patternPropertyRow.propertyType).toBe('number');
325
+ expect(patternPropertyRow.description).toBe(
326
+ 'Numeric custom attribute values.',
327
+ );
328
+ expect(patternPropertyRow.constraints).toContain('minimum: 0');
329
+ expect(patternPropertyRow.isSchemaKeywordRow).toBe(true);
330
+ });
331
+
220
332
  describe('if/then/else conditional support', () => {
221
333
  it('creates a conditional row for schema with if/then/else at root level', () => {
222
334
  const tableData = schemaToTableData(conditionalEventSchema);
@@ -17,6 +17,8 @@ import {
17
17
  */
18
18
  export default function ConditionalRows({
19
19
  row,
20
+ stripeIndex = 0,
21
+ stripeState,
20
22
  bracketEnds: parentBracketEnds,
21
23
  }) {
22
24
  const {
@@ -91,7 +93,7 @@ export default function ConditionalRows({
91
93
  return (
92
94
  <>
93
95
  {/* Condition (if) section - always visible */}
94
- <tr className="conditional-condition-header">
96
+ <tr className="conditional-condition-header schema-row--control">
95
97
  <td colSpan={5} style={headerStyle}>
96
98
  <span className="conditional-condition-label">
97
99
  <span className="conditional-info-icon-wrapper">
@@ -111,7 +113,7 @@ export default function ConditionalRows({
111
113
  )}
112
114
  </td>
113
115
  </tr>
114
- <SchemaRows tableData={condition.rows} />
116
+ <SchemaRows tableData={condition.rows} stripeState={stripeState} />
115
117
 
116
118
  {/* Branch toggles (then / else) */}
117
119
  {branches.map((branch, index) => {
@@ -121,7 +123,7 @@ export default function ConditionalRows({
121
123
  isLastBranch && !isActive ? lastToggleStyle : middleStyle;
122
124
  return (
123
125
  <React.Fragment key={branch.title}>
124
- <tr className="choice-row">
126
+ <tr className="choice-row schema-row--control">
125
127
  <td colSpan={5} style={toggleStyle}>
126
128
  <label className="choice-row-header">
127
129
  <input
@@ -141,6 +143,7 @@ export default function ConditionalRows({
141
143
  {isActive && (
142
144
  <SchemaRows
143
145
  tableData={branch.rows}
146
+ stripeState={stripeState}
144
147
  bracketEnds={
145
148
  isLastBranch
146
149
  ? [ownBracket, ...(parentBracketEnds || [])]
@@ -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 || [])]
@@ -8,6 +8,7 @@ import styles from './PropertiesTable.module.css';
8
8
  export default function PropertiesTable({ schema }) {
9
9
  const [isWordWrapOn, setIsWordWrapOn] = useState(true);
10
10
  const tableData = schemaToTableData(schema);
11
+ const stripeState = { current: 0 };
11
12
 
12
13
  return (
13
14
  <div
@@ -24,7 +25,7 @@ export default function PropertiesTable({ schema }) {
24
25
  <table className="schema-table">
25
26
  <TableHeader />
26
27
  <tbody>
27
- <SchemaRows tableData={tableData} />
28
+ <SchemaRows tableData={tableData} stripeState={stripeState} />
28
29
  </tbody>
29
30
  </table>
30
31
  </div>
@@ -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(