Skip to content

Commit 3cb6697

Browse files
tzdesignTobias Zimmermann
andauthored
Add explicit tag nesting rules for <picture> and <button> elements (#7798)
* fix button and picture children * added changeset * revert table bit mask entries * refactored tests and updated isInAnysthing --------- Co-authored-by: Tobias Zimmermann <[email protected]>
1 parent 2dd89a6 commit 3cb6697

File tree

3 files changed

+283
-42
lines changed

3 files changed

+283
-42
lines changed

.changeset/slick-clowns-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
Add explicit tag nesting rules for <picture> and <button> elements

packages/qwik/src/server/tag-nesting.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const enum TagNesting {
2525
PHRASING_ANY = /* ----------- */ 0b0000_0001_0000_0010,
2626
PHRASING_INSIDE_INPUT = /* -- */ 0b0000_0010_0000_0010,
2727
PHRASING_CONTAINER = /* ----- */ 0b0000_0100_0000_0010,
28+
PICTURE = /* ---------------- */ 0b0000_1000_0000_0010,
29+
BUTTON = /* ----------------- */ 0b0001_0000_0000_0010,
2830
/** Table related tags. */
2931
TABLE = /* ------------------ */ 0b0001_0000_0000_0000,
3032
TABLE_BODY = /* ------------- */ 0b0010_0000_0000_0000,
@@ -63,6 +65,10 @@ export const allowedContent = (state: TagNesting): [string, string | null] => {
6365
case TagNesting.PHRASING_INSIDE_INPUT:
6466
case TagNesting.PHRASING_CONTAINER:
6567
return ['phrasing content', '<a>, <b>, <img>, <input> ... (no <div>, <p> ...)'];
68+
case TagNesting.PICTURE:
69+
return ['picture content', '<source>, <img>'];
70+
case TagNesting.BUTTON:
71+
return ['button content', 'phrasing content except interactive elements'];
6672
case TagNesting.DOCUMENT:
6773
return ['document', '<html>'];
6874
}
@@ -107,6 +113,10 @@ export function isTagAllowed(state: number, tag: string): TagNesting {
107113
return isInPhrasing(tag, true);
108114
case TagNesting.PHRASING_INSIDE_INPUT:
109115
return isInPhrasing(tag, false);
116+
case TagNesting.PICTURE:
117+
return isInPicture(tag);
118+
case TagNesting.BUTTON:
119+
return isInButton(tag);
110120
case TagNesting.DOCUMENT:
111121
if (tag === 'html') {
112122
return TagNesting.HTML;
@@ -191,9 +201,12 @@ function isInAnything(text: string): TagNesting {
191201
case 'body':
192202
return TagNesting.NOT_ALLOWED;
193203
case 'button':
204+
return TagNesting.BUTTON;
194205
case 'input':
195206
case 'textarea':
196207
return TagNesting.PHRASING_INSIDE_INPUT;
208+
case 'picture':
209+
return TagNesting.PICTURE;
197210

198211
default:
199212
return TagNesting.ANYTHING;
@@ -243,12 +256,37 @@ function isInTableColGroup(text: string): TagNesting {
243256
}
244257
}
245258

259+
function isInPicture(text: string): TagNesting {
260+
switch (text) {
261+
case 'source':
262+
return TagNesting.EMPTY;
263+
case 'img':
264+
return TagNesting.EMPTY;
265+
default:
266+
return TagNesting.NOT_ALLOWED;
267+
}
268+
}
269+
270+
function isInButton(text: string): TagNesting {
271+
switch (text) {
272+
case 'button':
273+
case 'input':
274+
case 'textarea':
275+
case 'select':
276+
case 'a':
277+
return TagNesting.NOT_ALLOWED;
278+
case 'picture':
279+
return TagNesting.PICTURE;
280+
default:
281+
return isInPhrasing(text, false);
282+
}
283+
}
284+
246285
function isInPhrasing(text: string, allowInput: boolean): TagNesting {
247286
switch (text) {
248287
case 'svg':
249288
case 'math':
250289
return TagNesting.PHRASING_CONTAINER;
251-
case 'button':
252290
case 'input':
253291
case 'textarea':
254292
return allowInput ? TagNesting.PHRASING_INSIDE_INPUT : TagNesting.NOT_ALLOWED;
@@ -260,6 +298,7 @@ function isInPhrasing(text: string, allowInput: boolean): TagNesting {
260298
case 'bdi':
261299
case 'bdo':
262300
case 'br':
301+
case 'button':
263302
case 'canvas':
264303
case 'cite':
265304
case 'code':
@@ -287,7 +326,6 @@ function isInPhrasing(text: string, allowInput: boolean): TagNesting {
287326
case 'object':
288327
case 'option':
289328
case 'output':
290-
case 'picture':
291329
case 'progress':
292330
case 'q':
293331
case 'ruby':
@@ -310,6 +348,8 @@ function isInPhrasing(text: string, allowInput: boolean): TagNesting {
310348
return allowInput ? TagNesting.PHRASING_ANY : TagNesting.PHRASING_INSIDE_INPUT;
311349
case 'style':
312350
return TagNesting.TEXT;
351+
case 'picture':
352+
return TagNesting.PICTURE;
313353
default:
314354
return TagNesting.NOT_ALLOWED;
315355
}

packages/qwik/src/server/tag-nesting.unit.ts

Lines changed: 236 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,242 @@ import { describe, it, expect } from 'vitest';
22
import { TagNesting, isTagAllowed } from './tag-nesting';
33

44
describe('tag-nesting', () => {
5-
it('debug', () => {
6-
expect(isValidNesting('html>body>button>span')).toBe(true);
7-
});
8-
it('should test cases', () => {
9-
const cases: Array<[string, string | true]> = [
10-
['html>head>script>#text', true],
11-
['html>head>noscript>#text', true],
12-
['html>head>style>#text', true],
13-
['html>head>meta', true],
14-
['html>head>meta>#text', '#text'],
15-
['html>head>meta>div', 'div'],
16-
['html>head>link', true],
17-
['html>head>link>#text', '#text'],
18-
['html>head>link>div', 'div'],
19-
['html>head>base', true],
20-
['html>head>base>#text', '#text'],
21-
['html>head>base>div', 'div'],
22-
['html>head>template', true],
23-
['html>head>template>#text', true],
24-
['html>head>template>div', true],
25-
['html>body>p>div', 'div'],
26-
['html>body>div>custom-element>div>#text', true],
27-
['html>body>style>#text', true],
28-
['html>body>style>div', 'div'],
29-
['html>body>area>div', 'div'],
30-
['html>body>img>div', 'div'],
31-
['html>body>p>p', 'p'],
32-
['html>body>p>div', 'div'],
33-
['html>body>p>b>textarea>#text', true],
34-
['html>body>textarea>textarea', 'textarea'],
35-
['html>body>table>tr', 'tr'],
36-
['html>body>button>button', 'button'],
37-
['html>body>button>span', true],
38-
['html>body>table>thead>th>div', 'th'],
39-
['html>body>table>thead>tr>th>div', true],
40-
['html>body>table>tbody>tr>td>div', true],
41-
['html>body>button>svg>circle', true],
42-
['html>body>math>mrow', true],
43-
];
44-
cases.forEach(([path, expectation]) => expect(isValidNesting(path)).toBe(expectation));
5+
// Head element tests
6+
describe('head element content', () => {
7+
it('should allow text content in script elements', () => {
8+
expect(isValidNesting('html>head>script>#text')).toBe(true);
9+
});
10+
11+
it('should allow text content in noscript elements', () => {
12+
expect(isValidNesting('html>head>noscript>#text')).toBe(true);
13+
});
14+
15+
it('should allow text content in style elements', () => {
16+
expect(isValidNesting('html>head>style>#text')).toBe(true);
17+
});
18+
19+
it('should allow meta elements', () => {
20+
expect(isValidNesting('html>head>meta')).toBe(true);
21+
});
22+
23+
it('should not allow text content in meta elements', () => {
24+
expect(isValidNesting('html>head>meta>#text')).toBe('#text');
25+
});
26+
27+
it('should not allow div elements in meta elements', () => {
28+
expect(isValidNesting('html>head>meta>div')).toBe('div');
29+
});
30+
31+
it('should allow link elements', () => {
32+
expect(isValidNesting('html>head>link')).toBe(true);
33+
});
34+
35+
it('should not allow text content in link elements', () => {
36+
expect(isValidNesting('html>head>link>#text')).toBe('#text');
37+
});
38+
39+
it('should not allow div elements in link elements', () => {
40+
expect(isValidNesting('html>head>link>div')).toBe('div');
41+
});
42+
43+
it('should allow base elements', () => {
44+
expect(isValidNesting('html>head>base')).toBe(true);
45+
});
46+
47+
it('should not allow text content in base elements', () => {
48+
expect(isValidNesting('html>head>base>#text')).toBe('#text');
49+
});
50+
51+
it('should not allow div elements in base elements', () => {
52+
expect(isValidNesting('html>head>base>div')).toBe('div');
53+
});
54+
55+
it('should allow template elements', () => {
56+
expect(isValidNesting('html>head>template')).toBe(true);
57+
});
58+
59+
it('should allow text content in template elements', () => {
60+
expect(isValidNesting('html>head>template>#text')).toBe(true);
61+
});
62+
63+
it('should allow div elements in template elements', () => {
64+
expect(isValidNesting('html>head>template>div')).toBe(true);
65+
});
66+
});
67+
68+
// Body element tests
69+
describe('body element content', () => {
70+
it('should not allow div elements in p elements', () => {
71+
expect(isValidNesting('html>body>p>div')).toBe('div');
72+
});
73+
74+
it('should allow custom elements with div and text content', () => {
75+
expect(isValidNesting('html>body>div>custom-element>div>#text')).toBe(true);
76+
});
77+
78+
it('should allow text content in style elements', () => {
79+
expect(isValidNesting('html>body>style>#text')).toBe(true);
80+
});
81+
82+
it('should not allow div elements in style elements', () => {
83+
expect(isValidNesting('html>body>style>div')).toBe('div');
84+
});
85+
86+
it('should not allow div elements in self-closing area elements', () => {
87+
expect(isValidNesting('html>body>area>div')).toBe('div');
88+
});
89+
90+
it('should not allow div elements in self-closing img elements', () => {
91+
expect(isValidNesting('html>body>img>div')).toBe('div');
92+
});
93+
94+
it('should not allow p elements nested in p elements', () => {
95+
expect(isValidNesting('html>body>p>p')).toBe('p');
96+
});
97+
98+
it('should allow textarea with text content inside phrasing elements', () => {
99+
expect(isValidNesting('html>body>p>b>textarea>#text')).toBe(true);
100+
});
101+
102+
it('should not allow nested textarea elements', () => {
103+
expect(isValidNesting('html>body>textarea>textarea')).toBe('textarea');
104+
});
105+
});
106+
107+
// Table tests
108+
describe('table element content', () => {
109+
it('should not allow tr elements directly in table elements', () => {
110+
expect(isValidNesting('html>body>table>tr')).toBe('tr');
111+
});
112+
113+
it('should not allow div elements directly in th elements', () => {
114+
expect(isValidNesting('html>body>table>thead>th>div')).toBe('th');
115+
});
116+
117+
it('should allow div elements in th elements inside tr elements', () => {
118+
expect(isValidNesting('html>body>table>thead>tr>th>div')).toBe(true);
119+
});
120+
121+
it('should allow div elements in td elements', () => {
122+
expect(isValidNesting('html>body>table>tbody>tr>td>div')).toBe(true);
123+
});
124+
});
125+
126+
// SVG and Math tests
127+
describe('svg and math content', () => {
128+
it('should allow svg elements with circle inside button elements', () => {
129+
expect(isValidNesting('html>body>button>svg>circle')).toBe(true);
130+
});
131+
132+
it('should allow math elements with mrow', () => {
133+
expect(isValidNesting('html>body>math>mrow')).toBe(true);
134+
});
135+
});
136+
137+
// Picture element tests
138+
describe('picture element content', () => {
139+
it('should allow source elements in picture elements', () => {
140+
expect(isValidNesting('html>body>picture>source')).toBe(true);
141+
});
142+
143+
it('should allow img elements in picture elements', () => {
144+
expect(isValidNesting('html>body>picture>img')).toBe(true);
145+
});
146+
147+
it('should not allow div elements in picture elements', () => {
148+
expect(isValidNesting('html>body>picture>div')).toBe('div');
149+
});
150+
151+
it('should not allow span elements in picture elements', () => {
152+
expect(isValidNesting('html>body>picture>span')).toBe('span');
153+
});
154+
155+
it('should allow picture with source in p elements', () => {
156+
expect(isValidNesting('html>body>p>picture>source')).toBe(true);
157+
});
158+
159+
it('should allow picture with img in p elements', () => {
160+
expect(isValidNesting('html>body>p>picture>img')).toBe(true);
161+
});
162+
163+
it('should allow picture with source in button elements', () => {
164+
expect(isValidNesting('html>body>button>picture>source')).toBe(true);
165+
});
166+
167+
it('should allow picture with img in button elements', () => {
168+
expect(isValidNesting('html>body>button>picture>img')).toBe(true);
169+
});
170+
171+
it('should allow picture with source in div elements', () => {
172+
expect(isValidNesting('html>body>div>picture>source')).toBe(true);
173+
});
174+
});
175+
176+
// Button element tests
177+
describe('button element content', () => {
178+
it('should allow span elements in button elements', () => {
179+
expect(isValidNesting('html>body>button>span')).toBe(true);
180+
});
181+
182+
it('should allow img elements in button elements', () => {
183+
expect(isValidNesting('html>body>button>img')).toBe(true);
184+
});
185+
186+
it('should allow b elements in button elements', () => {
187+
expect(isValidNesting('html>body>button>b')).toBe(true);
188+
});
189+
190+
it('should allow strong elements in button elements', () => {
191+
expect(isValidNesting('html>body>button>strong')).toBe(true);
192+
});
193+
194+
it('should allow picture elements in button elements', () => {
195+
expect(isValidNesting('html>body>button>picture')).toBe(true);
196+
});
197+
198+
it('should allow picture with source in button elements', () => {
199+
expect(isValidNesting('html>body>button>picture>source')).toBe(true);
200+
});
201+
202+
it('should allow picture with img in button elements', () => {
203+
expect(isValidNesting('html>body>button>picture>img')).toBe(true);
204+
});
205+
});
206+
207+
describe('button element interactive content restrictions', () => {
208+
it('should not allow nested button elements', () => {
209+
expect(isValidNesting('html>body>button>button')).toBe('button');
210+
});
211+
212+
it('should not allow input elements in button elements', () => {
213+
expect(isValidNesting('html>body>button>input')).toBe('input');
214+
});
215+
216+
it('should not allow textarea elements in button elements', () => {
217+
expect(isValidNesting('html>body>button>textarea')).toBe('textarea');
218+
});
219+
220+
it('should not allow select elements in button elements', () => {
221+
expect(isValidNesting('html>body>button>select')).toBe('select');
222+
});
223+
224+
it('should not allow anchor elements in button elements', () => {
225+
expect(isValidNesting('html>body>button>a')).toBe('a');
226+
});
227+
});
228+
229+
describe('button element placement', () => {
230+
it('should allow button elements in p elements', () => {
231+
expect(isValidNesting('html>body>p>button')).toBe(true);
232+
});
233+
234+
it('should allow button elements in div elements', () => {
235+
expect(isValidNesting('html>body>div>button')).toBe(true);
236+
});
237+
238+
it('should allow button elements in span elements', () => {
239+
expect(isValidNesting('html>body>span>button')).toBe(true);
240+
});
45241
});
46242
});
47243

0 commit comments

Comments
 (0)