@patternfly/quickstarts 6.3.0-prerelease.6 → 6.3.0-prerelease.8
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/dist/ConsoleShared/src/components/markdown-extensions/accordion-extension.d.ts +1 -1
- package/dist/ConsoleShared/src/components/markdown-extensions/admonition-extension.d.ts +1 -1
- package/dist/index.es.js +226 -136
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +226 -136
- package/dist/index.js.map +1 -1
- package/dist/quickstarts-full.es.js +274 -184
- package/dist/quickstarts-full.es.js.map +1 -1
- package/package.json +2 -2
- package/src/ConsoleInternal/components/markdown-view.tsx +92 -4
- package/src/ConsoleShared/src/components/markdown-extensions/__tests__/accordion-extension.spec.tsx +105 -0
- package/src/ConsoleShared/src/components/markdown-extensions/__tests__/admonition-extension.spec.tsx +121 -0
- package/src/ConsoleShared/src/components/markdown-extensions/accordion-extension.tsx +20 -13
- package/src/ConsoleShared/src/components/markdown-extensions/accordion-render-extension.tsx +19 -5
- package/src/ConsoleShared/src/components/markdown-extensions/admonition-extension.tsx +17 -6
- package/src/QuickStartDrawerContent.tsx +2 -5
- package/src/catalog/Toolbar/QuickStartCatalogFilterItems.tsx +1 -4
- package/src/controller/QuickStartIntroduction.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/quickstarts",
|
|
3
|
-
"version": "6.3.0-prerelease.
|
|
3
|
+
"version": "6.3.0-prerelease.8",
|
|
4
4
|
"description": "PatternFly quick starts",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@rollup/plugin-commonjs": "^17.0.0",
|
|
67
67
|
"@rollup/plugin-json": "^4.1.0",
|
|
68
68
|
"@rollup/plugin-node-resolve": "^11.1.0",
|
|
69
|
-
"@testing-library/react": "^
|
|
69
|
+
"@testing-library/react": "^13.4.0",
|
|
70
70
|
"@types/dompurify": "^3.0.5",
|
|
71
71
|
"@types/enzyme": "^3.10.7",
|
|
72
72
|
"@types/enzyme-adapter-react-16": "^1.0.6",
|
|
@@ -25,7 +25,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
|
|
|
25
25
|
return node;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// add PF content classes
|
|
28
|
+
// add PF content classes to standard elements (details blocks get handled separately)
|
|
29
29
|
if (node.nodeType === 1) {
|
|
30
30
|
const contentElements = [
|
|
31
31
|
'ul',
|
|
@@ -85,7 +85,90 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
|
|
|
85
85
|
);
|
|
86
86
|
const markdownWithSubstitutedCodeFences = reverseString(reverseMarkdownWithSubstitutedCodeFences);
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Fix malformed HTML entities early in the process
|
|
89
|
+
let preprocessedMarkdown = markdownWithSubstitutedCodeFences;
|
|
90
|
+
preprocessedMarkdown = preprocessedMarkdown
|
|
91
|
+
.replace(/ ([^;])/g, ' $1')
|
|
92
|
+
.replace(/ /g, ' ');
|
|
93
|
+
preprocessedMarkdown = preprocessedMarkdown.replace(/ (?![;])/g, ' ');
|
|
94
|
+
|
|
95
|
+
// Process content in segments to ensure markdown parsing continues after HTML blocks
|
|
96
|
+
const htmlBlockRegex =
|
|
97
|
+
/(<(?:details|div|section|article)[^>]*>[\s\S]*?<\/(?:details|div|section|article)>)/g;
|
|
98
|
+
|
|
99
|
+
let parsedMarkdown = '';
|
|
100
|
+
|
|
101
|
+
// Check if there are any HTML blocks
|
|
102
|
+
if (htmlBlockRegex.test(preprocessedMarkdown)) {
|
|
103
|
+
// Reset regex for actual processing
|
|
104
|
+
htmlBlockRegex.lastIndex = 0;
|
|
105
|
+
|
|
106
|
+
let lastIndex = 0;
|
|
107
|
+
let match;
|
|
108
|
+
|
|
109
|
+
while ((match = htmlBlockRegex.exec(preprocessedMarkdown)) !== null) {
|
|
110
|
+
// Process markdown before the HTML block
|
|
111
|
+
const markdownBefore = preprocessedMarkdown.slice(lastIndex, match.index).trim();
|
|
112
|
+
if (markdownBefore) {
|
|
113
|
+
const parsed = await marked.parse(markdownBefore);
|
|
114
|
+
parsedMarkdown += parsed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Process the HTML block: parse markdown content inside while preserving HTML structure
|
|
118
|
+
let htmlBlock = match[1];
|
|
119
|
+
|
|
120
|
+
// Find and process markdown content inside HTML tags
|
|
121
|
+
const contentRegex = />(\s*[\s\S]*?)\s*</g;
|
|
122
|
+
const contentMatches = [];
|
|
123
|
+
let contentMatch;
|
|
124
|
+
|
|
125
|
+
while ((contentMatch = contentRegex.exec(htmlBlock)) !== null) {
|
|
126
|
+
const content = contentMatch[1];
|
|
127
|
+
// Only process content that has markdown formatting but no extension syntax
|
|
128
|
+
if (
|
|
129
|
+
content.trim() &&
|
|
130
|
+
!content.includes('{{') &&
|
|
131
|
+
(content.includes('**') || content.includes('- ') || content.includes('\n'))
|
|
132
|
+
) {
|
|
133
|
+
// This looks like markdown content without extensions - parse it as block content
|
|
134
|
+
const parsedContent = await marked.parse(content.trim());
|
|
135
|
+
// Remove wrapping <p> tags if they exist since we're inside HTML already
|
|
136
|
+
const cleanedContent = parsedContent.replace(/^<p[^>]*>([\s\S]*)<\/p>[\s]*$/g, '$1');
|
|
137
|
+
contentMatches.push({
|
|
138
|
+
original: contentMatch[0],
|
|
139
|
+
replacement: `>${cleanedContent}<`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Apply the content replacements
|
|
145
|
+
contentMatches.forEach(({ original, replacement }) => {
|
|
146
|
+
htmlBlock = htmlBlock.replace(original, replacement);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Apply extensions (like admonitions) to the processed HTML block
|
|
150
|
+
if (extensions) {
|
|
151
|
+
extensions.forEach(({ regex, replace }) => {
|
|
152
|
+
if (regex) {
|
|
153
|
+
htmlBlock = htmlBlock.replace(regex, replace);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
parsedMarkdown += htmlBlock;
|
|
159
|
+
lastIndex = htmlBlockRegex.lastIndex;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Process any remaining markdown after the last HTML block
|
|
163
|
+
const markdownAfter = preprocessedMarkdown.slice(lastIndex).trim();
|
|
164
|
+
if (markdownAfter) {
|
|
165
|
+
const parsed = await marked.parse(markdownAfter);
|
|
166
|
+
parsedMarkdown += parsed;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// No HTML blocks found, process normally
|
|
170
|
+
parsedMarkdown = await marked.parse(preprocessedMarkdown);
|
|
171
|
+
}
|
|
89
172
|
// Swap the temporary tokens back to code fences before we run the extensions
|
|
90
173
|
let md = parsedMarkdown.replace(/@@@/g, '```');
|
|
91
174
|
|
|
@@ -93,7 +176,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
|
|
|
93
176
|
// Convert code spans back to md format before we run the custom extension regexes
|
|
94
177
|
md = md.replace(/<code>(.*)<\/code>/g, '`$1`');
|
|
95
178
|
|
|
96
|
-
extensions.forEach(({ regex, replace }) => {
|
|
179
|
+
extensions.forEach(({ regex, replace }, _index) => {
|
|
97
180
|
if (regex) {
|
|
98
181
|
md = md.replace(regex, replace);
|
|
99
182
|
}
|
|
@@ -102,6 +185,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
|
|
|
102
185
|
// Convert any remaining backticks back into code spans
|
|
103
186
|
md = md.replace(/`(.*)`/g, '<code>$1</code>');
|
|
104
187
|
}
|
|
188
|
+
|
|
105
189
|
return DOMPurify.sanitize(md);
|
|
106
190
|
};
|
|
107
191
|
|
|
@@ -210,7 +294,10 @@ const InlineMarkdownView: FC<InnerSyncMarkdownProps> = ({
|
|
|
210
294
|
const id = useMemo(() => uniqueId('markdown'), []);
|
|
211
295
|
return (
|
|
212
296
|
<div className={css({ 'is-empty': isEmpty } as any, className)} id={id}>
|
|
213
|
-
<div
|
|
297
|
+
<div
|
|
298
|
+
style={{ marginBlockEnd: 'var(--pf-t-global--spacer--md)' }}
|
|
299
|
+
dangerouslySetInnerHTML={{ __html: markup }}
|
|
300
|
+
/>
|
|
214
301
|
{renderExtension && (
|
|
215
302
|
<RenderExtension renderExtension={renderExtension} selector={`#${id}`} markup={markup} />
|
|
216
303
|
)}
|
|
@@ -299,6 +386,7 @@ const IFrameMarkdownView: FC<InnerSyncMarkdownProps> = ({
|
|
|
299
386
|
return (
|
|
300
387
|
<>
|
|
301
388
|
<iframe
|
|
389
|
+
title="Markdown content preview"
|
|
302
390
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
303
391
|
srcDoc={contents}
|
|
304
392
|
style={{ border: '0px', display: 'block', width: '100%', height: '0' }}
|
package/src/ConsoleShared/src/components/markdown-extensions/__tests__/accordion-extension.spec.tsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by Cursor
|
|
2
|
+
// AI-assisted implementation with human review and modifications
|
|
3
|
+
import { renderHook } from '@testing-library/react';
|
|
4
|
+
import useAccordionShowdownExtension from '../accordion-extension';
|
|
5
|
+
import { ACCORDION_MARKDOWN_BUTTON_ID, ACCORDION_MARKDOWN_CONTENT_ID } from '../const';
|
|
6
|
+
import { marked } from 'marked';
|
|
7
|
+
|
|
8
|
+
// Mock marked
|
|
9
|
+
jest.mock('marked', () => ({
|
|
10
|
+
marked: {
|
|
11
|
+
parseInline: jest.fn((text) => `<em>${text}</em>`),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock DOMPurify
|
|
16
|
+
jest.mock('dompurify', () => ({
|
|
17
|
+
sanitize: jest.fn((html) => html),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('useAccordionShowdownExtension', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return a showdown extension with correct properties', () => {
|
|
26
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
27
|
+
const extension = result.current;
|
|
28
|
+
|
|
29
|
+
expect(extension.type).toBe('lang');
|
|
30
|
+
expect(extension.regex).toEqual(/\[(.+)]{{(accordion) ("(.*?)")}}/g);
|
|
31
|
+
expect(typeof extension.replace).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should match accordion syntax with HTML-encoded quotes', () => {
|
|
35
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
36
|
+
const { regex } = result.current;
|
|
37
|
+
|
|
38
|
+
const testText = '[Some content]{{accordion "My Title"}}';
|
|
39
|
+
const matches = regex.exec(testText);
|
|
40
|
+
|
|
41
|
+
expect(matches).not.toBeNull();
|
|
42
|
+
if (matches) {
|
|
43
|
+
expect(matches[1]).toBe('Some content');
|
|
44
|
+
expect(matches[2]).toBe('accordion');
|
|
45
|
+
expect(matches[4]).toBe('My Title');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should not match accordion syntax with regular quotes', () => {
|
|
50
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
51
|
+
const { regex } = result.current;
|
|
52
|
+
|
|
53
|
+
const testText = '[Some content]{{accordion "My Title"}}';
|
|
54
|
+
expect(testText.match(regex)).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should generate correct accordion HTML structure', () => {
|
|
58
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
59
|
+
const { replace } = result.current;
|
|
60
|
+
|
|
61
|
+
const html = replace(
|
|
62
|
+
'[Test content]{{accordion "Test Title"}}',
|
|
63
|
+
'Test content',
|
|
64
|
+
'accordion',
|
|
65
|
+
'"Test Title"',
|
|
66
|
+
'Test Title',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(html).toContain('pf-v6-c-accordion');
|
|
70
|
+
expect(html).toContain('pf-v6-c-accordion__toggle');
|
|
71
|
+
expect(html).toContain(`${ACCORDION_MARKDOWN_BUTTON_ID}-Test-Title`);
|
|
72
|
+
expect(html).toContain(`${ACCORDION_MARKDOWN_CONTENT_ID}-Test-Title`);
|
|
73
|
+
expect(html).toContain('Test Title');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should process content through marked and sanitize HTML', () => {
|
|
77
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
78
|
+
const { replace } = result.current;
|
|
79
|
+
|
|
80
|
+
replace(
|
|
81
|
+
'[**Bold text**]{{accordion "Title"}}',
|
|
82
|
+
'**Bold text**',
|
|
83
|
+
'accordion',
|
|
84
|
+
'"Title"',
|
|
85
|
+
'Title',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(marked.parseInline).toHaveBeenCalledWith('**Bold text**');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle titles with spaces in IDs', () => {
|
|
92
|
+
const { result } = renderHook(() => useAccordionShowdownExtension());
|
|
93
|
+
const { replace } = result.current;
|
|
94
|
+
|
|
95
|
+
const html = replace(
|
|
96
|
+
'[Content]{{accordion "My Test Title"}}',
|
|
97
|
+
'Content',
|
|
98
|
+
'accordion',
|
|
99
|
+
'"My Test Title"',
|
|
100
|
+
'My Test Title',
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(html).toContain(`${ACCORDION_MARKDOWN_BUTTON_ID}-My-Test-Title`);
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/ConsoleShared/src/components/markdown-extensions/__tests__/admonition-extension.spec.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import useAdmonitionShowdownExtension from '../admonition-extension';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
|
|
5
|
+
// Mock marked
|
|
6
|
+
jest.mock('marked', () => ({
|
|
7
|
+
marked: {
|
|
8
|
+
parseInline: jest.fn((text) => `<strong>${text}</strong>`),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mock DOMPurify
|
|
13
|
+
jest.mock('dompurify', () => ({
|
|
14
|
+
sanitize: jest.fn((html) => html),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('useAdmonitionShowdownExtension', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return a showdown extension with correct properties', () => {
|
|
23
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
24
|
+
const extension = result.current;
|
|
25
|
+
|
|
26
|
+
expect(extension.type).toBe('lang');
|
|
27
|
+
expect(extension.regex).toEqual(/\[(.+)]{{(admonition) ([\w-]+)}}/g);
|
|
28
|
+
expect(typeof extension.replace).toBe('function');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should match different admonition types', () => {
|
|
32
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
33
|
+
const { regex } = result.current;
|
|
34
|
+
|
|
35
|
+
const admonitionTypes = ['note', 'tip', 'important', 'warning', 'caution', 'custom-type'];
|
|
36
|
+
|
|
37
|
+
admonitionTypes.forEach((type) => {
|
|
38
|
+
const testText = `[Content for ${type}]{{admonition ${type}}}`;
|
|
39
|
+
// Reset regex state for global flag
|
|
40
|
+
regex.lastIndex = 0;
|
|
41
|
+
const matches = regex.exec(testText);
|
|
42
|
+
|
|
43
|
+
expect(matches).not.toBeNull();
|
|
44
|
+
if (matches) {
|
|
45
|
+
expect(matches[1]).toBe(`Content for ${type}`);
|
|
46
|
+
expect(matches[2]).toBe('admonition');
|
|
47
|
+
expect(matches[3]).toBe(type);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should not match malformed admonition syntax', () => {
|
|
53
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
54
|
+
const { regex } = result.current;
|
|
55
|
+
|
|
56
|
+
const malformedCases = [
|
|
57
|
+
'Content]{{admonition note}}',
|
|
58
|
+
'[Content{{admonition note}}',
|
|
59
|
+
'[Content]{{admonition}}',
|
|
60
|
+
'[Content]{{notadmonition note}}',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
malformedCases.forEach((testCase) => {
|
|
64
|
+
expect(testCase.match(regex)).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should generate correct alert HTML structure', () => {
|
|
69
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
70
|
+
const { replace } = result.current;
|
|
71
|
+
|
|
72
|
+
const html = replace('[Test message]{{admonition note}}', 'Test message', 'admonition', 'note');
|
|
73
|
+
|
|
74
|
+
expect(html).toContain('pf-v6-c-alert');
|
|
75
|
+
expect(html).toContain('pf-m-info'); // note maps to info variant
|
|
76
|
+
expect(html).toContain('pf-m-inline');
|
|
77
|
+
expect(html).toContain('pfext-markdown-admonition');
|
|
78
|
+
expect(html).toContain('NOTE'); // uppercase title
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle different admonition types with correct variants', () => {
|
|
82
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
83
|
+
const { replace } = result.current;
|
|
84
|
+
|
|
85
|
+
const testCases = [
|
|
86
|
+
{ type: 'note', expectedClass: 'pf-m-info' },
|
|
87
|
+
{ type: 'warning', expectedClass: 'pf-m-warning' },
|
|
88
|
+
{ type: 'important', expectedClass: 'pf-m-danger' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
testCases.forEach(({ type, expectedClass }) => {
|
|
92
|
+
const html = replace(`[Content]{{admonition ${type}}}`, 'Content', 'admonition', type);
|
|
93
|
+
|
|
94
|
+
expect(html).toContain(expectedClass);
|
|
95
|
+
expect(html).toContain(type.toUpperCase());
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should process content through marked', () => {
|
|
100
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
101
|
+
const { replace } = result.current;
|
|
102
|
+
|
|
103
|
+
replace('[**Bold text**]{{admonition note}}', '**Bold text**', 'admonition', 'note');
|
|
104
|
+
|
|
105
|
+
expect(marked.parseInline).toHaveBeenCalledWith('**Bold text**');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return original text for invalid cases', () => {
|
|
109
|
+
const { result } = renderHook(() => useAdmonitionShowdownExtension());
|
|
110
|
+
const { replace } = result.current;
|
|
111
|
+
|
|
112
|
+
// Missing content
|
|
113
|
+
const originalText = '[Content]{{admonition note}}';
|
|
114
|
+
const result1 = replace(originalText, '', 'admonition', 'note');
|
|
115
|
+
expect(result1).toBe(originalText);
|
|
116
|
+
|
|
117
|
+
// Wrong command
|
|
118
|
+
const result2 = replace(originalText, 'Content', 'not-admonition', 'note');
|
|
119
|
+
expect(result2).toBe(originalText);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -8,34 +8,41 @@ import {
|
|
|
8
8
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
9
9
|
import { removeTemplateWhitespace } from './utils';
|
|
10
10
|
import { ACCORDION_MARKDOWN_BUTTON_ID, ACCORDION_MARKDOWN_CONTENT_ID } from './const';
|
|
11
|
+
import { marked } from 'marked';
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
const DOMPurify = require('dompurify');
|
|
11
15
|
|
|
12
16
|
const useAccordionShowdownExtension = () =>
|
|
13
17
|
useMemo(
|
|
14
18
|
() => ({
|
|
15
19
|
type: 'lang',
|
|
16
|
-
regex: /\[(.+)]{{(accordion) (
|
|
20
|
+
regex: /\[(.+)]{{(accordion) ("(.*?)")}}/g,
|
|
17
21
|
replace: (
|
|
18
22
|
_text: string,
|
|
19
23
|
accordionContent: string,
|
|
20
24
|
_command: string,
|
|
25
|
+
_quotedHeading: string,
|
|
21
26
|
accordionHeading: string,
|
|
22
27
|
): string => {
|
|
23
28
|
const accordionId = String(accordionHeading).replace(/\s/g, '-');
|
|
24
29
|
|
|
30
|
+
// Process accordion content with markdown
|
|
31
|
+
const processedContent = marked.parseInline(accordionContent);
|
|
32
|
+
const sanitizedContent = DOMPurify.sanitize(processedContent);
|
|
33
|
+
|
|
25
34
|
return removeTemplateWhitespace(
|
|
26
35
|
renderToStaticMarkup(
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</Accordion>
|
|
38
|
-
</>,
|
|
36
|
+
<Accordion>
|
|
37
|
+
<AccordionItem>
|
|
38
|
+
<AccordionToggle id={`${ACCORDION_MARKDOWN_BUTTON_ID}-${accordionId}`}>
|
|
39
|
+
{accordionHeading}
|
|
40
|
+
</AccordionToggle>
|
|
41
|
+
<AccordionContent id={`${ACCORDION_MARKDOWN_CONTENT_ID}-${accordionId}`} hidden>
|
|
42
|
+
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
|
|
43
|
+
</AccordionContent>
|
|
44
|
+
</AccordionItem>
|
|
45
|
+
</Accordion>,
|
|
39
46
|
),
|
|
40
47
|
);
|
|
41
48
|
},
|
|
@@ -14,14 +14,28 @@ const AccordionShowdownHandler: FC<AccordionShowdownComponentProps> = ({
|
|
|
14
14
|
const [expanded, setExpanded] = useState<boolean>(false);
|
|
15
15
|
|
|
16
16
|
const handleClick = () => {
|
|
17
|
+
const newExpanded = !expanded;
|
|
17
18
|
const expandedModifier = 'pf-m-expanded';
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Find the accordion item element (parent of the button)
|
|
21
|
+
const accordionItem = buttonElement.closest('.pf-v6-c-accordion__item');
|
|
22
|
+
|
|
23
|
+
// Update button - both visual state and aria-expanded
|
|
24
|
+
buttonElement.className = `pf-v6-c-accordion__toggle ${newExpanded ? expandedModifier : ''}`;
|
|
25
|
+
buttonElement.setAttribute('aria-expanded', newExpanded.toString());
|
|
26
|
+
|
|
27
|
+
// Update content - both visual state and hidden attribute
|
|
28
|
+
contentElement.hidden = !newExpanded;
|
|
29
|
+
contentElement.className = `pf-v6-c-accordion__expandable-content ${
|
|
30
|
+
newExpanded ? expandedModifier : ''
|
|
23
31
|
}`;
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
// Update accordion item
|
|
34
|
+
if (accordionItem) {
|
|
35
|
+
accordionItem.className = `pf-v6-c-accordion__item ${newExpanded ? expandedModifier : ''}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setExpanded(newExpanded);
|
|
25
39
|
};
|
|
26
40
|
|
|
27
41
|
useEventListener(buttonElement, 'click', handleClick);
|
|
@@ -4,7 +4,10 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
4
4
|
import { Alert } from '@patternfly/react-core';
|
|
5
5
|
import LightbulbIcon from '@patternfly/react-icons/dist/js/icons/lightbulb-icon';
|
|
6
6
|
import FireIcon from '@patternfly/react-icons/dist/js/icons/fire-icon';
|
|
7
|
-
import
|
|
7
|
+
import { marked } from 'marked';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
const DOMPurify = require('dompurify');
|
|
8
11
|
|
|
9
12
|
enum AdmonitionType {
|
|
10
13
|
TIP = 'TIP',
|
|
@@ -33,15 +36,23 @@ const useAdmonitionShowdownExtension = () =>
|
|
|
33
36
|
content: string,
|
|
34
37
|
admonitionLabel: string,
|
|
35
38
|
admonitionType: string,
|
|
36
|
-
groupId: string,
|
|
37
39
|
): string => {
|
|
38
|
-
if (!content || !admonitionLabel || !admonitionType
|
|
40
|
+
if (!content || !admonitionLabel || !admonitionType) {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
if (admonitionLabel !== 'admonition') {
|
|
39
44
|
return text;
|
|
40
45
|
}
|
|
41
46
|
admonitionType = admonitionType.toUpperCase();
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
const
|
|
48
|
+
// Process markdown content directly using marked
|
|
49
|
+
const processedContent = marked.parseInline(content);
|
|
50
|
+
const sanitizedContent = DOMPurify.sanitize(processedContent);
|
|
51
|
+
|
|
52
|
+
// Handle unknown admonition types by defaulting to NOTE
|
|
53
|
+
const admonitionConfig =
|
|
54
|
+
admonitionToAlertVariantMap[admonitionType] || admonitionToAlertVariantMap.NOTE;
|
|
55
|
+
const { variant, customIcon } = admonitionConfig;
|
|
45
56
|
const pfAlert = (
|
|
46
57
|
<Alert
|
|
47
58
|
variant={variant}
|
|
@@ -50,7 +61,7 @@ const useAdmonitionShowdownExtension = () =>
|
|
|
50
61
|
title={admonitionType}
|
|
51
62
|
className="pfext-markdown-admonition"
|
|
52
63
|
>
|
|
53
|
-
{
|
|
64
|
+
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
|
|
54
65
|
</Alert>
|
|
55
66
|
);
|
|
56
67
|
return removeTemplateWhitespace(renderToStaticMarkup(pfAlert));
|
|
@@ -24,11 +24,8 @@ export const QuickStartDrawerContent: FC<QuickStartDrawerContentProps> = ({
|
|
|
24
24
|
allQuickStarts = [],
|
|
25
25
|
activeQuickStartState,
|
|
26
26
|
} = useContext<QuickStartContextValues>(QuickStartContext);
|
|
27
|
-
const combinedQuickStarts = [
|
|
28
|
-
|
|
29
|
-
...quickStarts,
|
|
30
|
-
].filter(
|
|
31
|
-
(qs, idx, arr) => arr.findIndex(q => q.metadata.name === qs.metadata.name) === idx
|
|
27
|
+
const combinedQuickStarts = [...allQuickStarts, ...quickStarts].filter(
|
|
28
|
+
(qs, idx, arr) => arr.findIndex((q) => q.metadata.name === qs.metadata.name) === idx,
|
|
32
29
|
);
|
|
33
30
|
|
|
34
31
|
const handleClose = () => {
|
|
@@ -72,10 +72,7 @@ export const QuickStartCatalogFilterCount = ({ quickStartsCount }) => {
|
|
|
72
72
|
const { getResource } = useContext<QuickStartContextValues>(QuickStartContext);
|
|
73
73
|
return (
|
|
74
74
|
<ToolbarItem align={{ default: 'alignEnd' }}>
|
|
75
|
-
{getResource(
|
|
76
|
-
'{{count, number}}',
|
|
77
|
-
quickStartsCount,
|
|
78
|
-
)}
|
|
75
|
+
{ getResource("{{count, number}} item", quickStartsCount).replace('{{count, number}}', quickStartsCount) }
|
|
79
76
|
</ToolbarItem>
|
|
80
77
|
);
|
|
81
78
|
};
|
|
@@ -47,7 +47,7 @@ const QuickStartIntroduction: FC<QuickStartIntroductionProps> = ({
|
|
|
47
47
|
<p>
|
|
48
48
|
{getResource(
|
|
49
49
|
'In this quick start, you will complete {{count, number}} task',
|
|
50
|
-
tasks?.length,
|
|
50
|
+
tasks?.length || 0,
|
|
51
51
|
).replace('{{count, number}}', tasks?.length || 0)}
|
|
52
52
|
:
|
|
53
53
|
</p>
|