kempo-ui 0.4.5 → 0.4.7
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/components/Chat.js +201 -0
- package/dist/components/CodeEditor.js +21 -2
- package/dist/components/Dropdown.js +1 -1
- package/dist/components/HtmlEditor.js +42 -3
- package/dist/components/Markdown.js +1 -0
- package/dist/components/MarkdownEditor.js +173 -0
- package/dist/components/Resize.js +12 -2
- package/dist/components/markdownEditorControls/Bold.js +11 -0
- package/dist/components/markdownEditorControls/BulletList.js +11 -0
- package/dist/components/markdownEditorControls/Code.js +11 -0
- package/dist/components/markdownEditorControls/FormatBlock.js +30 -0
- package/dist/components/markdownEditorControls/Heading.js +11 -0
- package/dist/components/markdownEditorControls/Image.js +93 -0
- package/dist/components/markdownEditorControls/Italic.js +11 -0
- package/dist/components/markdownEditorControls/Link.js +11 -0
- package/dist/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/dist/components/markdownEditorControls/Menu.js +30 -0
- package/dist/components/markdownEditorControls/NumberedList.js +11 -0
- package/dist/components/markdownEditorControls/Quote.js +11 -0
- package/dist/components/markdownEditorControls/SpeechToText.js +16 -0
- package/dist/components/markdownEditorControls/Strikethrough.js +11 -0
- package/dist/components/markdownEditorControls/Table.js +80 -0
- package/dist/utils/marked.esm.js +1 -0
- package/dist/utils/renderMarkdown.js +1 -0
- package/dist/utils/sanitizeHtml.js +1 -0
- package/docs/components/accordion.html +16 -0
- package/docs/components/aside.html +16 -0
- package/docs/components/calendar.html +16 -0
- package/docs/components/card.html +16 -0
- package/docs/components/chat.html +719 -0
- package/docs/components/code-editor.html +52 -0
- package/docs/components/color-picker.html +16 -0
- package/docs/components/combobox.html +16 -0
- package/docs/components/content-slider.html +16 -0
- package/docs/components/context.html +16 -0
- package/docs/components/dialog.html +16 -0
- package/docs/components/dropdown.html +16 -0
- package/docs/components/filter-list.html +16 -0
- package/docs/components/focus-capture.html +16 -0
- package/docs/components/html-editor.html +57 -0
- package/docs/components/hybrid-component.html +16 -0
- package/docs/components/icon.html +16 -0
- package/docs/components/import.html +16 -0
- package/docs/components/light-component.html +16 -0
- package/docs/components/markdown-editor.html +928 -0
- package/docs/components/markdown.html +650 -0
- package/docs/components/nav-spacer.html +16 -0
- package/docs/components/nav.html +16 -0
- package/docs/components/photo-viewer.html +16 -0
- package/docs/components/progress.html +16 -0
- package/docs/components/resize.html +19 -0
- package/docs/components/rich-textarea.html +650 -0
- package/docs/components/shadow-component.html +16 -0
- package/docs/components/show-more.html +16 -0
- package/docs/components/slider.html +16 -0
- package/docs/components/sortable.html +16 -0
- package/docs/components/speech-to-text.html +16 -0
- package/docs/components/spinner.html +16 -0
- package/docs/components/split.html +16 -0
- package/docs/components/table.html +16 -0
- package/docs/components/tableControls.html +16 -0
- package/docs/components/tableCustomFields.html +16 -0
- package/docs/components/tableFetchRecords.html +16 -0
- package/docs/components/tableFieldSortHide.html +16 -0
- package/docs/components/tablePagination.html +16 -0
- package/docs/components/tablePlaceholder.html +16 -0
- package/docs/components/tableRecordEditing.html +16 -0
- package/docs/components/tableRecordFiltering.html +16 -0
- package/docs/components/tableRecordHiding.html +16 -0
- package/docs/components/tableRecordSearching.html +16 -0
- package/docs/components/tableRecordSelection.html +16 -0
- package/docs/components/tableRowControls.html +16 -0
- package/docs/components/tableServerSync.html +16 -0
- package/docs/components/tableSorting.html +16 -0
- package/docs/components/tabs.html +16 -0
- package/docs/components/tags.html +16 -0
- package/docs/components/text-to-speech.html +16 -0
- package/docs/components/theme-select.html +16 -0
- package/docs/components/theme-switcher.html +16 -0
- package/docs/components/time.html +16 -0
- package/docs/components/timestamp.html +16 -0
- package/docs/components/toast.html +16 -0
- package/docs/components/toggle.html +16 -0
- package/docs/components/tree.html +16 -0
- package/docs/components/voice-selector.html +16 -0
- package/docs/icons/done_all.svg +1 -0
- package/docs/icons/format_quote.svg +1 -0
- package/docs/icons/format_strikethrough.svg +1 -0
- package/docs/icons/send.svg +1 -0
- package/docs/icons/text_fields.svg +1 -0
- package/docs/index.html +40 -0
- package/docs/src/components/Chat.js +201 -0
- package/docs/src/components/CodeEditor.js +21 -2
- package/docs/src/components/Dropdown.js +1 -1
- package/docs/src/components/HtmlEditor.js +42 -3
- package/docs/src/components/Markdown.js +1 -0
- package/docs/src/components/MarkdownEditor.js +173 -0
- package/docs/src/components/Resize.js +12 -2
- package/docs/src/components/markdownEditorControls/Bold.js +11 -0
- package/docs/src/components/markdownEditorControls/BulletList.js +11 -0
- package/docs/src/components/markdownEditorControls/Code.js +11 -0
- package/docs/src/components/markdownEditorControls/FormatBlock.js +30 -0
- package/docs/src/components/markdownEditorControls/Heading.js +11 -0
- package/docs/src/components/markdownEditorControls/Image.js +93 -0
- package/docs/src/components/markdownEditorControls/Italic.js +11 -0
- package/docs/src/components/markdownEditorControls/Link.js +11 -0
- package/docs/src/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/docs/src/components/markdownEditorControls/Menu.js +30 -0
- package/docs/src/components/markdownEditorControls/NumberedList.js +11 -0
- package/docs/src/components/markdownEditorControls/Quote.js +11 -0
- package/docs/src/components/markdownEditorControls/SpeechToText.js +16 -0
- package/docs/src/components/markdownEditorControls/Strikethrough.js +11 -0
- package/docs/src/components/markdownEditorControls/Table.js +80 -0
- package/docs/src/utils/marked.esm.js +1 -0
- package/docs/src/utils/renderMarkdown.js +1 -0
- package/docs/src/utils/sanitizeHtml.js +1 -0
- package/docs/utils/context.html +16 -0
- package/docs/utils/cookie.html +16 -0
- package/docs/utils/debounce.html +16 -0
- package/docs/utils/drag.html +16 -0
- package/docs/utils/elevation.html +20 -10
- package/docs/utils/formatTimestamp.html +16 -0
- package/docs/utils/object.html +16 -0
- package/docs/utils/propConverters.html +16 -0
- package/docs/utils/sanitizeHtml.html +559 -0
- package/docs/utils/string.html +16 -0
- package/docs/utils/theme.html +16 -0
- package/docs/utils/toTitleCase.html +16 -0
- package/docs/utils/type.html +16 -0
- package/docs/utils/voice.html +16 -0
- package/docs/utils/wait.html +16 -0
- package/docs-src/components/chat.page.html +230 -0
- package/docs-src/components/code-editor.page.html +36 -0
- package/docs-src/components/html-editor.page.html +41 -0
- package/docs-src/components/markdown-editor.page.html +439 -0
- package/docs-src/components/markdown.page.html +161 -0
- package/docs-src/components/resize.page.html +3 -0
- package/docs-src/index.page.html +24 -0
- package/docs-src/nav.fragment.html +16 -0
- package/docs-src/utils/elevation.page.html +4 -10
- package/docs-src/utils/sanitizeHtml.page.html +73 -0
- package/icons/done_all.svg +1 -0
- package/icons/format_quote.svg +1 -0
- package/icons/format_strikethrough.svg +1 -0
- package/icons/send.svg +1 -0
- package/icons/text_fields.svg +1 -0
- package/llms.txt +6 -2
- package/package.json +3 -2
- package/src/components/Chat.js +435 -0
- package/src/components/CodeEditor.js +58 -2
- package/src/components/Dropdown.js +1 -1
- package/src/components/HtmlEditor.js +84 -4
- package/src/components/Markdown.js +125 -0
- package/src/components/MarkdownEditor.js +654 -0
- package/src/components/Resize.js +17 -1
- package/src/components/markdownEditorControls/Bold.js +30 -0
- package/src/components/markdownEditorControls/BulletList.js +30 -0
- package/src/components/markdownEditorControls/Code.js +39 -0
- package/src/components/markdownEditorControls/FormatBlock.js +102 -0
- package/src/components/markdownEditorControls/Heading.js +45 -0
- package/src/components/markdownEditorControls/Image.js +250 -0
- package/src/components/markdownEditorControls/Italic.js +30 -0
- package/src/components/markdownEditorControls/Link.js +74 -0
- package/src/components/markdownEditorControls/MarkdownEditorControl.js +158 -0
- package/src/components/markdownEditorControls/Menu.js +86 -0
- package/src/components/markdownEditorControls/NumberedList.js +69 -0
- package/src/components/markdownEditorControls/Quote.js +30 -0
- package/src/components/markdownEditorControls/SpeechToText.js +65 -0
- package/src/components/markdownEditorControls/Strikethrough.js +35 -0
- package/src/components/markdownEditorControls/Table.js +164 -0
- package/src/utils/marked.esm.js +77 -0
- package/src/utils/renderMarkdown.js +29 -0
- package/src/utils/sanitizeHtml.js +137 -0
- package/tests/components/Chat.browser-test.js +540 -0
- package/tests/components/Markdown.browser-test.js +292 -0
- package/tests/utils/sanitizeHtml.browser-test.js +179 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import Markdown from '../../src/components/Markdown.js';
|
|
2
|
+
import LightComponent from '../../src/components/LightComponent.js';
|
|
3
|
+
|
|
4
|
+
const createMarkdown = async (innerHTML = '', attrs = {}) => {
|
|
5
|
+
const container = document.createElement('div');
|
|
6
|
+
const el = document.createElement('k-markdown');
|
|
7
|
+
for(const [key, value] of Object.entries(attrs)){
|
|
8
|
+
el.setAttribute(key, value);
|
|
9
|
+
}
|
|
10
|
+
if(innerHTML) el.innerHTML = innerHTML;
|
|
11
|
+
container.appendChild(el);
|
|
12
|
+
document.body.appendChild(container);
|
|
13
|
+
await el.updateComplete;
|
|
14
|
+
return { container, el };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const cleanup = (container) => {
|
|
18
|
+
if(container && container.parentNode){
|
|
19
|
+
container.parentNode.removeChild(container);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
/*
|
|
25
|
+
Element Creation
|
|
26
|
+
*/
|
|
27
|
+
'should create k-markdown element': async ({pass, fail}) => {
|
|
28
|
+
const { container, el } = await createMarkdown();
|
|
29
|
+
if(!(el instanceof Markdown)){
|
|
30
|
+
cleanup(container);
|
|
31
|
+
return fail('Element should be instance of Markdown');
|
|
32
|
+
}
|
|
33
|
+
cleanup(container);
|
|
34
|
+
pass('k-markdown element created correctly');
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
'should extend LightComponent': async ({pass, fail}) => {
|
|
38
|
+
const { container, el } = await createMarkdown();
|
|
39
|
+
if(!(el instanceof LightComponent)){
|
|
40
|
+
cleanup(container);
|
|
41
|
+
return fail('Markdown should extend LightComponent');
|
|
42
|
+
}
|
|
43
|
+
cleanup(container);
|
|
44
|
+
pass('Markdown extends LightComponent');
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
Default Properties
|
|
49
|
+
*/
|
|
50
|
+
'should have empty default value': async ({pass, fail}) => {
|
|
51
|
+
const { container, el } = await createMarkdown();
|
|
52
|
+
if(el.value !== ''){
|
|
53
|
+
cleanup(container);
|
|
54
|
+
return fail(`Expected value to be '', got '${el.value}'`);
|
|
55
|
+
}
|
|
56
|
+
cleanup(container);
|
|
57
|
+
pass('Default value is empty string');
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
'should have breaks default false': async ({pass, fail}) => {
|
|
61
|
+
const { container, el } = await createMarkdown();
|
|
62
|
+
if(el.breaks !== false){
|
|
63
|
+
cleanup(container);
|
|
64
|
+
return fail(`Expected breaks to be false, got ${el.breaks}`);
|
|
65
|
+
}
|
|
66
|
+
cleanup(container);
|
|
67
|
+
pass('Default breaks is false');
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
Children → value capture
|
|
72
|
+
*/
|
|
73
|
+
'should capture markdown from child text content': async ({pass, fail}) => {
|
|
74
|
+
const { container, el } = await createMarkdown('# Hello');
|
|
75
|
+
if(el.value !== '# Hello'){
|
|
76
|
+
cleanup(container);
|
|
77
|
+
return fail(`Expected value '# Hello', got '${el.value}'`);
|
|
78
|
+
}
|
|
79
|
+
cleanup(container);
|
|
80
|
+
pass('Captures markdown from children');
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
'should dedent indented child markdown': async ({pass, fail}) => {
|
|
84
|
+
const container = document.createElement('div');
|
|
85
|
+
container.innerHTML = `<k-markdown>
|
|
86
|
+
# Title
|
|
87
|
+
|
|
88
|
+
Some text
|
|
89
|
+
</k-markdown>`;
|
|
90
|
+
document.body.appendChild(container);
|
|
91
|
+
const el = container.querySelector('k-markdown');
|
|
92
|
+
await el.updateComplete;
|
|
93
|
+
const expected = '# Title\n\nSome text';
|
|
94
|
+
if(el.value !== expected){
|
|
95
|
+
cleanup(container);
|
|
96
|
+
return fail(`Expected dedented value '${expected}', got '${el.value}'`);
|
|
97
|
+
}
|
|
98
|
+
cleanup(container);
|
|
99
|
+
pass('Indented child markdown is dedented');
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'value attribute takes precedence over children': async ({pass, fail}) => {
|
|
103
|
+
const container = document.createElement('div');
|
|
104
|
+
container.innerHTML = `<k-markdown value="from attr"># From children</k-markdown>`;
|
|
105
|
+
document.body.appendChild(container);
|
|
106
|
+
const el = container.querySelector('k-markdown');
|
|
107
|
+
await el.updateComplete;
|
|
108
|
+
if(el.value !== 'from attr'){
|
|
109
|
+
cleanup(container);
|
|
110
|
+
return fail(`Expected value 'from attr', got '${el.value}'`);
|
|
111
|
+
}
|
|
112
|
+
cleanup(container);
|
|
113
|
+
pass('value attribute beats children');
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
Rendering
|
|
118
|
+
*/
|
|
119
|
+
'should render markdown to HTML in light DOM': async ({pass, fail}) => {
|
|
120
|
+
const { container, el } = await createMarkdown('# Heading');
|
|
121
|
+
const heading = el.querySelector('h1');
|
|
122
|
+
if(!heading){
|
|
123
|
+
cleanup(container);
|
|
124
|
+
return fail(`Expected h1 in rendered output, got: ${el.innerHTML}`);
|
|
125
|
+
}
|
|
126
|
+
if(heading.textContent !== 'Heading'){
|
|
127
|
+
cleanup(container);
|
|
128
|
+
return fail(`Expected h1 text 'Heading', got '${heading.textContent}'`);
|
|
129
|
+
}
|
|
130
|
+
cleanup(container);
|
|
131
|
+
pass('Renders markdown heading as h1 in light DOM');
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
'should render bold and italic': async ({pass, fail}) => {
|
|
135
|
+
const { container, el } = await createMarkdown('This is **bold** and *italic*.');
|
|
136
|
+
const strong = el.querySelector('strong');
|
|
137
|
+
const em = el.querySelector('em');
|
|
138
|
+
if(!strong || strong.textContent !== 'bold'){
|
|
139
|
+
cleanup(container);
|
|
140
|
+
return fail(`Expected <strong>bold</strong>, got: ${el.innerHTML}`);
|
|
141
|
+
}
|
|
142
|
+
if(!em || em.textContent !== 'italic'){
|
|
143
|
+
cleanup(container);
|
|
144
|
+
return fail(`Expected <em>italic</em>, got: ${el.innerHTML}`);
|
|
145
|
+
}
|
|
146
|
+
cleanup(container);
|
|
147
|
+
pass('Bold and italic render correctly');
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
'should render lists': async ({pass, fail}) => {
|
|
151
|
+
const container = document.createElement('div');
|
|
152
|
+
container.innerHTML = `<k-markdown>
|
|
153
|
+
- one
|
|
154
|
+
- two
|
|
155
|
+
- three
|
|
156
|
+
</k-markdown>`;
|
|
157
|
+
document.body.appendChild(container);
|
|
158
|
+
const el = container.querySelector('k-markdown');
|
|
159
|
+
await el.updateComplete;
|
|
160
|
+
const items = el.querySelectorAll('ul > li');
|
|
161
|
+
if(items.length !== 3){
|
|
162
|
+
cleanup(container);
|
|
163
|
+
return fail(`Expected 3 li elements, got ${items.length}: ${el.innerHTML}`);
|
|
164
|
+
}
|
|
165
|
+
cleanup(container);
|
|
166
|
+
pass('Lists render with three items');
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
'should clear original child markdown source': async ({pass, fail}) => {
|
|
170
|
+
const { container, el } = await createMarkdown('# Hello');
|
|
171
|
+
if(el.textContent.includes('# Hello')){
|
|
172
|
+
cleanup(container);
|
|
173
|
+
return fail(`Original markdown source should be removed, got textContent: '${el.textContent}'`);
|
|
174
|
+
}
|
|
175
|
+
cleanup(container);
|
|
176
|
+
pass('Original child markdown is cleared');
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/*
|
|
180
|
+
Re-rendering on value change
|
|
181
|
+
*/
|
|
182
|
+
'should re-render when value property changes': async ({pass, fail}) => {
|
|
183
|
+
const { container, el } = await createMarkdown('first');
|
|
184
|
+
el.value = '## Updated';
|
|
185
|
+
await el.updateComplete;
|
|
186
|
+
const h2 = el.querySelector('h2');
|
|
187
|
+
if(!h2 || h2.textContent !== 'Updated'){
|
|
188
|
+
cleanup(container);
|
|
189
|
+
return fail(`Expected h2 'Updated' after value change, got: ${el.innerHTML}`);
|
|
190
|
+
}
|
|
191
|
+
cleanup(container);
|
|
192
|
+
pass('Re-renders on value change');
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
breaks attribute
|
|
197
|
+
*/
|
|
198
|
+
'breaks=false joins single newlines (CommonMark)': async ({pass, fail}) => {
|
|
199
|
+
const { container, el } = await createMarkdown();
|
|
200
|
+
el.value = 'one\ntwo';
|
|
201
|
+
await el.updateComplete;
|
|
202
|
+
const br = el.querySelector('br');
|
|
203
|
+
if(br){
|
|
204
|
+
cleanup(container);
|
|
205
|
+
return fail(`Expected no <br> when breaks=false, got: ${el.innerHTML}`);
|
|
206
|
+
}
|
|
207
|
+
cleanup(container);
|
|
208
|
+
pass('breaks=false joins single newlines');
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
'breaks=true converts single newlines to <br>': async ({pass, fail}) => {
|
|
212
|
+
const { container, el } = await createMarkdown('', { breaks: '' });
|
|
213
|
+
el.value = 'one\ntwo';
|
|
214
|
+
await el.updateComplete;
|
|
215
|
+
const br = el.querySelector('br');
|
|
216
|
+
if(!br){
|
|
217
|
+
cleanup(container);
|
|
218
|
+
return fail(`Expected <br> when breaks=true, got: ${el.innerHTML}`);
|
|
219
|
+
}
|
|
220
|
+
cleanup(container);
|
|
221
|
+
pass('breaks=true converts single newlines to <br>');
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
Sanitization
|
|
226
|
+
*/
|
|
227
|
+
'should strip <script> tags by default': async ({pass, fail}) => {
|
|
228
|
+
const { container, el } = await createMarkdown();
|
|
229
|
+
el.value = 'safe <script>alert(1)</script> text';
|
|
230
|
+
await el.updateComplete;
|
|
231
|
+
if(el.querySelector('script')){
|
|
232
|
+
cleanup(container);
|
|
233
|
+
return fail(`Expected no <script>, got: ${el.innerHTML}`);
|
|
234
|
+
}
|
|
235
|
+
if(el.textContent.includes('alert(1)')){
|
|
236
|
+
cleanup(container);
|
|
237
|
+
return fail(`Script content should be stripped completely, got textContent: '${el.textContent}'`);
|
|
238
|
+
}
|
|
239
|
+
cleanup(container);
|
|
240
|
+
pass('Script tags stripped by default');
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
'allowed-tags="*" allows everything': async ({pass, fail}) => {
|
|
244
|
+
const { container, el } = await createMarkdown('', { 'allowed-tags': '*' });
|
|
245
|
+
el.value = '<details><summary>open</summary>hi</details>';
|
|
246
|
+
await el.updateComplete;
|
|
247
|
+
if(!el.querySelector('details') || !el.querySelector('summary')){
|
|
248
|
+
cleanup(container);
|
|
249
|
+
return fail(`Expected details+summary preserved, got: ${el.innerHTML}`);
|
|
250
|
+
}
|
|
251
|
+
cleanup(container);
|
|
252
|
+
pass('allowed-tags="*" allows custom tags');
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
'disallowed-tags drops listed tags but keeps text': async ({pass, fail}) => {
|
|
256
|
+
const { container, el } = await createMarkdown('', { 'disallowed-tags': 'h1' });
|
|
257
|
+
el.value = '# Heading\n\nA paragraph.';
|
|
258
|
+
await el.updateComplete;
|
|
259
|
+
if(el.querySelector('h1')){
|
|
260
|
+
cleanup(container);
|
|
261
|
+
return fail(`Expected no <h1>, got: ${el.innerHTML}`);
|
|
262
|
+
}
|
|
263
|
+
if(!el.textContent.includes('Heading')){
|
|
264
|
+
cleanup(container);
|
|
265
|
+
return fail(`Expected heading text preserved, got textContent: '${el.textContent}'`);
|
|
266
|
+
}
|
|
267
|
+
if(!el.querySelector('p')){
|
|
268
|
+
cleanup(container);
|
|
269
|
+
return fail(`Expected paragraph preserved, got: ${el.innerHTML}`);
|
|
270
|
+
}
|
|
271
|
+
cleanup(container);
|
|
272
|
+
pass('disallowed-tags drops listed tags but keeps text');
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/*
|
|
276
|
+
renderedHtml getter
|
|
277
|
+
*/
|
|
278
|
+
'renderedHtml getter returns sanitized HTML string': async ({pass, fail}) => {
|
|
279
|
+
const { container, el } = await createMarkdown('# Title');
|
|
280
|
+
const out = el.renderedHtml;
|
|
281
|
+
if(typeof out !== 'string'){
|
|
282
|
+
cleanup(container);
|
|
283
|
+
return fail(`Expected string, got ${typeof out}`);
|
|
284
|
+
}
|
|
285
|
+
if(!out.includes('<h1') || !out.includes('Title')){
|
|
286
|
+
cleanup(container);
|
|
287
|
+
return fail(`Expected '<h1...>Title' in renderedHtml, got '${out}'`);
|
|
288
|
+
}
|
|
289
|
+
cleanup(container);
|
|
290
|
+
pass('renderedHtml returns expected HTML string');
|
|
291
|
+
}
|
|
292
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import sanitizeHtml from '../../src/utils/sanitizeHtml.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
/*
|
|
5
|
+
Basic safety
|
|
6
|
+
*/
|
|
7
|
+
'should remove <script> tags entirely': async ({pass, fail}) => {
|
|
8
|
+
const out = sanitizeHtml('Hello<script>alert(1)</script>world');
|
|
9
|
+
if(/script/i.test(out) || out.includes('alert')){
|
|
10
|
+
return fail(`Expected scripts removed, got "${out}"`);
|
|
11
|
+
}
|
|
12
|
+
if(!out.includes('Hello') || !out.includes('world')){
|
|
13
|
+
return fail(`Expected user text preserved, got "${out}"`);
|
|
14
|
+
}
|
|
15
|
+
pass('<script> removed and surrounding text preserved');
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
'should remove <style> tags entirely': async ({pass, fail}) => {
|
|
19
|
+
const out = sanitizeHtml('text<style>body{display:none}</style>more');
|
|
20
|
+
if(/style/i.test(out) || out.includes('display')){
|
|
21
|
+
return fail(`Expected style removed, got "${out}"`);
|
|
22
|
+
}
|
|
23
|
+
pass('<style> removed');
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
'should remove <iframe> tags entirely': async ({pass, fail}) => {
|
|
27
|
+
const out = sanitizeHtml('<iframe src="evil.com"></iframe>safe');
|
|
28
|
+
if(/iframe/i.test(out) || out.includes('evil')){
|
|
29
|
+
return fail(`Expected iframe removed, got "${out}"`);
|
|
30
|
+
}
|
|
31
|
+
if(!out.includes('safe')){
|
|
32
|
+
return fail(`Expected following text preserved, got "${out}"`);
|
|
33
|
+
}
|
|
34
|
+
pass('<iframe> removed');
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
'should strip event-handler attributes': async ({pass, fail}) => {
|
|
38
|
+
const out = sanitizeHtml('<p onclick="alert(1)" onmouseover="x">hi</p>');
|
|
39
|
+
if(/onclick|onmouseover/i.test(out)){
|
|
40
|
+
return fail(`Expected event handlers stripped, got "${out}"`);
|
|
41
|
+
}
|
|
42
|
+
if(!out.includes('hi')){
|
|
43
|
+
return fail(`Expected text preserved, got "${out}"`);
|
|
44
|
+
}
|
|
45
|
+
pass('Event-handler attributes stripped');
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
'should reject javascript: URLs in href': async ({pass, fail}) => {
|
|
49
|
+
const out = sanitizeHtml('<a href="javascript:alert(1)">click</a>');
|
|
50
|
+
if(/javascript:/i.test(out)){
|
|
51
|
+
return fail(`Expected javascript: scheme rejected, got "${out}"`);
|
|
52
|
+
}
|
|
53
|
+
pass('javascript: URL stripped from href');
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
'should reject data: URLs in href': async ({pass, fail}) => {
|
|
57
|
+
const out = sanitizeHtml('<a href="data:text/html,<script>alert(1)</script>">click</a>');
|
|
58
|
+
if(/data:/i.test(out)){
|
|
59
|
+
return fail(`Expected data: scheme rejected, got "${out}"`);
|
|
60
|
+
}
|
|
61
|
+
pass('data: URL stripped from href');
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
Allowed content
|
|
66
|
+
*/
|
|
67
|
+
'should preserve common formatting tags': async ({pass, fail}) => {
|
|
68
|
+
const out = sanitizeHtml('<p><b>bold</b> <i>italic</i> <u>under</u></p>');
|
|
69
|
+
if(!out.includes('<b>') || !out.includes('<i>') || !out.includes('<u>') || !out.includes('<p>')){
|
|
70
|
+
return fail(`Expected b/i/u/p preserved, got "${out}"`);
|
|
71
|
+
}
|
|
72
|
+
pass('Common formatting tags preserved');
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
'should preserve safe https links': async ({pass, fail}) => {
|
|
76
|
+
const out = sanitizeHtml('<a href="https://example.com">link</a>');
|
|
77
|
+
if(!out.includes('href="https://example.com"')){
|
|
78
|
+
return fail(`Expected https link preserved, got "${out}"`);
|
|
79
|
+
}
|
|
80
|
+
pass('https link preserved');
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
'should preserve mailto: links': async ({pass, fail}) => {
|
|
84
|
+
const out = sanitizeHtml('<a href="mailto:hi@example.com">email</a>');
|
|
85
|
+
if(!out.includes('mailto:hi@example.com')){
|
|
86
|
+
return fail(`Expected mailto preserved, got "${out}"`);
|
|
87
|
+
}
|
|
88
|
+
pass('mailto: link preserved');
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
'should preserve relative URLs': async ({pass, fail}) => {
|
|
92
|
+
const out = sanitizeHtml('<a href="/about">about</a>');
|
|
93
|
+
if(!out.includes('href="/about"')){
|
|
94
|
+
return fail(`Expected relative URL preserved, got "${out}"`);
|
|
95
|
+
}
|
|
96
|
+
pass('Relative URL preserved');
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
target=_blank rel-noopener auto-fix
|
|
101
|
+
*/
|
|
102
|
+
'should add rel="noopener noreferrer" to target="_blank" links': async ({pass, fail}) => {
|
|
103
|
+
const out = sanitizeHtml('<a href="https://example.com" target="_blank">x</a>');
|
|
104
|
+
if(!out.includes('target="_blank"') || !out.includes('rel="noopener noreferrer"')){
|
|
105
|
+
return fail(`Expected target+rel auto-fixed, got "${out}"`);
|
|
106
|
+
}
|
|
107
|
+
pass('rel="noopener noreferrer" forced on target="_blank" links');
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
Disallowed-tag fallback (preserve children)
|
|
112
|
+
*/
|
|
113
|
+
'should unwrap unknown tags but keep their text': async ({pass, fail}) => {
|
|
114
|
+
const out = sanitizeHtml('<custom>hello <b>world</b></custom>');
|
|
115
|
+
if(/custom/i.test(out)){
|
|
116
|
+
return fail(`Expected <custom> unwrapped, got "${out}"`);
|
|
117
|
+
}
|
|
118
|
+
if(!out.includes('hello') || !out.includes('<b>world</b>')){
|
|
119
|
+
return fail(`Expected children preserved, got "${out}"`);
|
|
120
|
+
}
|
|
121
|
+
pass('Disallowed tag unwrapped, children kept');
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
Comments
|
|
126
|
+
*/
|
|
127
|
+
'should remove HTML comments': async ({pass, fail}) => {
|
|
128
|
+
const out = sanitizeHtml('hi<!-- evil --><b>there</b>');
|
|
129
|
+
if(out.includes('evil')){
|
|
130
|
+
return fail(`Expected comment removed, got "${out}"`);
|
|
131
|
+
}
|
|
132
|
+
pass('HTML comments removed');
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
Edge cases
|
|
137
|
+
*/
|
|
138
|
+
'should handle null/undefined/empty input': async ({pass, fail}) => {
|
|
139
|
+
if(sanitizeHtml(null) !== '') return fail('null should produce ""');
|
|
140
|
+
if(sanitizeHtml(undefined) !== '') return fail('undefined should produce ""');
|
|
141
|
+
if(sanitizeHtml('') !== '') return fail('empty string should produce ""');
|
|
142
|
+
pass('null/undefined/empty all produce ""');
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
'should preserve plain text': async ({pass, fail}) => {
|
|
146
|
+
const out = sanitizeHtml('just some text');
|
|
147
|
+
if(out !== 'just some text'){
|
|
148
|
+
return fail(`Expected pass-through plain text, got "${out}"`);
|
|
149
|
+
}
|
|
150
|
+
pass('Plain text untouched');
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
'should keep data-* attributes': async ({pass, fail}) => {
|
|
154
|
+
const out = sanitizeHtml('<span data-id="42">x</span>');
|
|
155
|
+
if(!out.includes('data-id="42"')){
|
|
156
|
+
return fail(`Expected data-id preserved, got "${out}"`);
|
|
157
|
+
}
|
|
158
|
+
pass('data-* attributes preserved');
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
Custom whitelist
|
|
163
|
+
*/
|
|
164
|
+
'should respect a custom allowedTags whitelist': async ({pass, fail}) => {
|
|
165
|
+
const out = sanitizeHtml('<b>bold</b><i>italic</i>', {
|
|
166
|
+
allowedTags: new Set(['B'])
|
|
167
|
+
});
|
|
168
|
+
if(!out.includes('<b>bold</b>')){
|
|
169
|
+
return fail(`Expected <b> kept, got "${out}"`);
|
|
170
|
+
}
|
|
171
|
+
if(out.includes('<i>')){
|
|
172
|
+
return fail(`Expected <i> stripped, got "${out}"`);
|
|
173
|
+
}
|
|
174
|
+
if(!out.includes('italic')){
|
|
175
|
+
return fail(`Expected italic text preserved (unwrapped), got "${out}"`);
|
|
176
|
+
}
|
|
177
|
+
pass('Custom whitelist honored');
|
|
178
|
+
}
|
|
179
|
+
};
|