Skip to content

Commit 96c027c

Browse files
committed
Introduce a native namespace and skip certain handling and warnings
1 parent 9745b61 commit 96c027c

File tree

9 files changed

+172
-85
lines changed

9 files changed

+172
-85
lines changed

src/compiler/compile/nodes/Element.ts

+97-76
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export default class Element extends Node {
131131

132132
this.namespace = get_namespace(parent, this, component.namespace);
133133

134-
if (this.name === 'textarea') {
134+
if (this.name === 'textarea' && !this.is_native()) {
135135
if (info.children.length > 0) {
136136
const value_attribute = info.attributes.find(node => node.name === 'value');
137137
if (value_attribute) {
@@ -153,7 +153,7 @@ export default class Element extends Node {
153153
}
154154
}
155155

156-
if (this.name === 'option') {
156+
if (this.name === 'option' && !this.is_native()) {
157157
// Special case — treat these the same way:
158158
// <option>{foo}</option>
159159
// <option value={foo}>{foo}</option>
@@ -248,51 +248,53 @@ export default class Element extends Node {
248248
});
249249
}
250250

251-
if (a11y_distracting_elements.has(this.name)) {
252-
// no-distracting-elements
253-
this.component.warn(this, {
254-
code: 'a11y-distracting-elements',
255-
message: `A11y: Avoid <${this.name}> elements`
256-
});
257-
}
251+
if (!this.is_native()) {
252+
if (a11y_distracting_elements.has(this.name)) {
253+
// no-distracting-elements
254+
this.component.warn(this, {
255+
code: 'a11y-distracting-elements',
256+
message: `A11y: Avoid <${this.name}> elements`
257+
});
258+
}
258259

259-
if (this.name === 'figcaption') {
260-
let { parent } = this;
261-
let is_figure_parent = false;
260+
if (this.name === 'figcaption') {
261+
let { parent } = this;
262+
let is_figure_parent = false;
262263

263-
while (parent) {
264-
if ((parent as Element).name === 'figure') {
265-
is_figure_parent = true;
266-
break;
264+
while (parent) {
265+
if ((parent as Element).name === 'figure') {
266+
is_figure_parent = true;
267+
break;
268+
}
269+
if (parent.type === 'Element') {
270+
break;
271+
}
272+
parent = parent.parent;
267273
}
268-
if (parent.type === 'Element') {
269-
break;
274+
275+
if (!is_figure_parent) {
276+
this.component.warn(this, {
277+
code: 'a11y-structure',
278+
message: 'A11y: <figcaption> must be an immediate child of <figure>'
279+
});
270280
}
271-
parent = parent.parent;
272281
}
273282

274-
if (!is_figure_parent) {
275-
this.component.warn(this, {
276-
code: 'a11y-structure',
277-
message: 'A11y: <figcaption> must be an immediate child of <figure>'
283+
if (this.name === 'figure') {
284+
const children = this.children.filter(node => {
285+
if (node.type === 'Comment') return false;
286+
if (node.type === 'Text') return /\S/.test(node.data);
287+
return true;
278288
});
279-
}
280-
}
281-
282-
if (this.name === 'figure') {
283-
const children = this.children.filter(node => {
284-
if (node.type === 'Comment') return false;
285-
if (node.type === 'Text') return /\S/.test(node.data);
286-
return true;
287-
});
288289

289-
const index = children.findIndex(child => (child as Element).name === 'figcaption');
290+
const index = children.findIndex(child => (child as Element).name === 'figcaption');
290291

291-
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
292-
this.component.warn(children[index], {
293-
code: 'a11y-structure',
294-
message: 'A11y: <figcaption> must be first or last child of <figure>'
295-
});
292+
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
293+
this.component.warn(children[index], {
294+
code: 'a11y-structure',
295+
message: 'A11y: <figcaption> must be first or last child of <figure>'
296+
});
297+
}
296298
}
297299
}
298300

@@ -306,13 +308,50 @@ export default class Element extends Node {
306308
validate_attributes() {
307309
const { component, parent } = this;
308310

309-
const attribute_map = new Map();
310-
311311
this.attributes.forEach(attribute => {
312312
if (attribute.is_spread) return;
313313

314314
const name = attribute.name.toLowerCase();
315315

316+
// Errors
317+
318+
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
319+
component.error(attribute, {
320+
code: 'illegal-attribute',
321+
message: `'${name}' is not a valid attribute name`
322+
});
323+
}
324+
325+
if (name === 'slot') {
326+
if (!attribute.is_static) {
327+
component.error(attribute, {
328+
code: 'invalid-slot-attribute',
329+
message: 'slot attribute cannot have a dynamic value'
330+
});
331+
}
332+
333+
if (component.slot_outlets.has(name)) {
334+
component.error(attribute, {
335+
code: 'duplicate-slot-attribute',
336+
message: `Duplicate '${name}' slot`
337+
});
338+
339+
component.slot_outlets.add(name);
340+
}
341+
342+
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
343+
component.error(attribute, {
344+
code: 'invalid-slotted-content',
345+
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
346+
});
347+
}
348+
}
349+
350+
// The rest of the warnings don't apply to native elements
351+
if (this.is_native()) return;
352+
353+
// Warnings
354+
316355
// aria-props
317356
if (name.startsWith('aria-')) {
318357
if (invisible_elements.has(this.name)) {
@@ -404,52 +443,20 @@ export default class Element extends Node {
404443
}
405444
}
406445

407-
408-
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
409-
component.error(attribute, {
410-
code: 'illegal-attribute',
411-
message: `'${name}' is not a valid attribute name`
412-
});
413-
}
414-
415-
if (name === 'slot') {
416-
if (!attribute.is_static) {
417-
component.error(attribute, {
418-
code: 'invalid-slot-attribute',
419-
message: 'slot attribute cannot have a dynamic value'
420-
});
421-
}
422-
423-
if (component.slot_outlets.has(name)) {
424-
component.error(attribute, {
425-
code: 'duplicate-slot-attribute',
426-
message: `Duplicate '${name}' slot`
427-
});
428-
429-
component.slot_outlets.add(name);
430-
}
431-
432-
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
433-
component.error(attribute, {
434-
code: 'invalid-slotted-content',
435-
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
436-
});
437-
}
438-
}
439-
440446
if (name === 'is') {
441447
component.warn(attribute, {
442448
code: 'avoid-is',
443449
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
444450
});
445451
}
446-
447-
attribute_map.set(attribute.name, attribute);
448452
});
449453
}
450454

451455
validate_special_cases() {
452456
const { component, attributes, handlers } = this;
457+
458+
if (this.is_native()) return;
459+
453460
const attribute_map = new Map();
454461
const handlers_map = new Map();
455462

@@ -595,6 +602,16 @@ export default class Element extends Node {
595602
return value;
596603
};
597604

605+
if (this.is_native()) {
606+
this.bindings.forEach(binding => {
607+
component.error(binding, {
608+
code: 'invalid-binding',
609+
message: `'${binding.name}' is not a valid binding. Native elements only support bind:this`
610+
});
611+
});
612+
return;
613+
}
614+
598615
this.bindings.forEach(binding => {
599616
const { name } = binding;
600617

@@ -754,7 +771,7 @@ export default class Element extends Node {
754771
}
755772

756773
validate_content() {
757-
if (!a11y_required_content.has(this.name)) return;
774+
if (!a11y_required_content.has(this.name) || this.is_native()) return;
758775
if (
759776
this.bindings
760777
.some((binding) => ['textContent', 'innerHTML'].includes(binding.name))
@@ -831,6 +848,10 @@ export default class Element extends Node {
831848
return this.name === 'audio' || this.name === 'video';
832849
}
833850

851+
is_native() {
852+
return this.namespace == namespaces['native'];
853+
}
854+
834855
add_css_class() {
835856
if (this.attributes.some(attr => attr.is_spread)) {
836857
this.needs_manual_style_scoping = true;

src/compiler/compile/render_dom/wrappers/Element/Attribute.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
6767
}
6868
}
6969

70-
this.name = fix_attribute_casing(this.node.name);
71-
this.metadata = this.get_metadata();
72-
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
73-
this.property_name = this.is_indirectly_bound_value
74-
? '__value'
75-
: this.metadata && this.metadata.property_name;
70+
if (this.parent.node.is_native()) {
71+
// leave attribute case alone for elements in the "native" namespace
72+
this.name = this.node.name;
73+
this.metadata = this.get_metadata();
74+
this.is_indirectly_bound_value = false;
75+
this.property_name = null;
76+
this.is_select_value_attribute = false;
77+
this.is_input_value = false;
78+
} else {
79+
this.name = fix_attribute_casing(this.node.name);
80+
this.metadata = this.get_metadata();
81+
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
82+
this.property_name = this.is_indirectly_bound_value
83+
? '__value'
84+
: this.metadata && this.metadata.property_name;
85+
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
86+
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
87+
}
88+
7689
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
77-
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
78-
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
7990
this.should_cache = should_cache(this);
8091
}
8192

src/compiler/utils/namespaces.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
export const html = 'http://www.w3.org/1999/xhtml';
22
export const mathml = 'http://www.w3.org/1998/Math/MathML';
3+
export const native = 'http://svelte-native.technology/2019/native';
34
export const svg = 'http://www.w3.org/2000/svg';
45
export const xlink = 'http://www.w3.org/1999/xlink';
56
export const xml = 'http://www.w3.org/XML/1998/namespace';
67
export const xmlns = 'http://www.w3.org/2000/xmlns';
78

9+
810
export const valid_namespaces = [
911
'html',
1012
'mathml',
13+
'native',
1114
'svg',
1215
'xlink',
1316
'xml',
1417
'xmlns',
1518
html,
1619
mathml,
20+
native,
1721
svg,
1822
xlink,
1923
xml,
2024
xmlns
2125
];
2226

23-
export const namespaces: Record<string, string> = { html, mathml, svg, xlink, xml, xmlns };
27+
export const namespaces: Record<string, string> = { html, mathml, native, svg, xlink, xml, xmlns };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Test support for the native namespaces preserving attribute case (eg svelte-native, svelte-nodegui).
2+
3+
export default {
4+
html: `
5+
<page horizontalAlignment="center">
6+
<button textWrap="true" text="button">
7+
</page>
8+
`,
9+
options: {
10+
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
11+
},
12+
13+
test({ assert, target }) {
14+
const attr = sel => target.querySelector(sel).attributes[0].name;
15+
assert.equal(attr('page'), 'horizontalAlignment');
16+
assert.equal(attr('button'), 'textWrap');
17+
}
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<svelte:options namespace="native" />
2+
<page horizontalAlignment="center">
3+
<button textWrap="true" text="button">
4+
</page>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<svelte:options namespace="native" />
2+
<page>
3+
<a>not actually a link</a>
4+
<label>This isn't a html label</label>
5+
<figure>This is maybe a QT figure</figure>
6+
</page>
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[{
2+
"code": "invalid-binding",
3+
"message": "'value' is not a valid binding. Native elements only support bind:this",
4+
"pos": 80,
5+
"start": {
6+
"line": 6,
7+
"column": 7,
8+
"character": 80
9+
},
10+
"end": {
11+
"line": 6,
12+
"column": 28,
13+
"character": 101
14+
}
15+
}]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<svelte:options namespace="native" />
2+
<script>
3+
let whatever;
4+
</script>
5+
6+
<input bind:value={whatever} />

0 commit comments

Comments
 (0)