Skip to content

Commit c9635a9

Browse files
committed
Introduce the foreign namespace and skip certain handling and warnings
Allows the use of svelte for DOM implementations that aren't html5
1 parent 8180c5d commit c9635a9

File tree

9 files changed

+231
-135
lines changed

9 files changed

+231
-135
lines changed

src/compiler/compile/nodes/Element.ts

Lines changed: 155 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -136,44 +136,45 @@ export default class Element extends Node {
136136

137137
this.namespace = get_namespace(parent as Element, this, component.namespace);
138138

139-
if (this.name === 'textarea') {
140-
if (info.children.length > 0) {
141-
const value_attribute = info.attributes.find(node => node.name === 'value');
142-
if (value_attribute) {
143-
component.error(value_attribute, {
144-
code: 'textarea-duplicate-value',
145-
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
146-
});
147-
}
139+
if (this.namespace !== namespaces.foreign) {
140+
if (this.name === 'textarea') {
141+
if (info.children.length > 0) {
142+
const value_attribute = info.attributes.find(node => node.name === 'value');
143+
if (value_attribute) {
144+
component.error(value_attribute, {
145+
code: 'textarea-duplicate-value',
146+
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
147+
});
148+
}
148149

149-
// this is an egregious hack, but it's the easiest way to get <textarea>
150-
// children treated the same way as a value attribute
151-
info.attributes.push({
152-
type: 'Attribute',
153-
name: 'value',
154-
value: info.children
155-
});
150+
// this is an egregious hack, but it's the easiest way to get <textarea>
151+
// children treated the same way as a value attribute
152+
info.attributes.push({
153+
type: 'Attribute',
154+
name: 'value',
155+
value: info.children
156+
});
156157

157-
info.children = [];
158+
info.children = [];
159+
}
158160
}
159-
}
160161

161-
if (this.name === 'option') {
162-
// Special case — treat these the same way:
163-
// <option>{foo}</option>
164-
// <option value={foo}>{foo}</option>
165-
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
162+
if (this.name === 'option') {
163+
// Special case — treat these the same way:
164+
// <option>{foo}</option>
165+
// <option value={foo}>{foo}</option>
166+
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
166167

167-
if (!value_attribute) {
168-
info.attributes.push({
169-
type: 'Attribute',
170-
name: 'value',
171-
value: info.children,
172-
synthetic: true
173-
});
168+
if (!value_attribute) {
169+
info.attributes.push({
170+
type: 'Attribute',
171+
name: 'value',
172+
value: info.children,
173+
synthetic: true
174+
});
175+
}
174176
}
175177
}
176-
177178
const has_let = info.attributes.some(node => node.type === 'Let');
178179
if (has_let) {
179180
scope = scope.child();
@@ -253,65 +254,83 @@ export default class Element extends Node {
253254
});
254255
}
255256

256-
if (a11y_distracting_elements.has(this.name)) {
257-
// no-distracting-elements
258-
this.component.warn(this, {
259-
code: 'a11y-distracting-elements',
260-
message: `A11y: Avoid <${this.name}> elements`
261-
});
257+
this.validate_attributes();
258+
this.validate_event_handlers();
259+
if (this.namespace === namespaces.foreign) {
260+
this.validate_bindings_foreign();
261+
} else {
262+
this.validate_attributes_a11y();
263+
this.validate_special_cases();
264+
this.validate_bindings();
265+
this.validate_content();
262266
}
263267

264-
if (this.name === 'figcaption') {
265-
let { parent } = this;
266-
let is_figure_parent = false;
268+
}
267269

268-
while (parent) {
269-
if ((parent as Element).name === 'figure') {
270-
is_figure_parent = true;
271-
break;
272-
}
273-
if (parent.type === 'Element') {
274-
break;
275-
}
276-
parent = parent.parent;
277-
}
270+
validate_attributes() {
271+
const { component, parent } = this;
278272

279-
if (!is_figure_parent) {
280-
this.component.warn(this, {
281-
code: 'a11y-structure',
282-
message: 'A11y: <figcaption> must be an immediate child of <figure>'
273+
this.attributes.forEach(attribute => {
274+
if (attribute.is_spread) return;
275+
276+
const name = attribute.name.toLowerCase();
277+
278+
// Errors
279+
280+
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
281+
component.error(attribute, {
282+
code: 'illegal-attribute',
283+
message: `'${name}' is not a valid attribute name`
283284
});
284285
}
285-
}
286286

287-
if (this.name === 'figure') {
288-
const children = this.children.filter(node => {
289-
if (node.type === 'Comment') return false;
290-
if (node.type === 'Text') return /\S/.test(node.data);
291-
return true;
292-
});
287+
if (name === 'slot') {
288+
if (!attribute.is_static) {
289+
component.error(attribute, {
290+
code: 'invalid-slot-attribute',
291+
message: 'slot attribute cannot have a dynamic value'
292+
});
293+
}
293294

294-
const index = children.findIndex(child => (child as Element).name === 'figcaption');
295+
if (component.slot_outlets.has(name)) {
296+
component.error(attribute, {
297+
code: 'duplicate-slot-attribute',
298+
message: `Duplicate '${name}' slot`
299+
});
295300

296-
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
297-
this.component.warn(children[index], {
298-
code: 'a11y-structure',
299-
message: 'A11y: <figcaption> must be first or last child of <figure>'
300-
});
301+
component.slot_outlets.add(name);
302+
}
303+
304+
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
305+
component.error(attribute, {
306+
code: 'invalid-slotted-content',
307+
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
308+
});
309+
}
301310
}
302-
}
303311

304-
this.validate_attributes();
305-
this.validate_special_cases();
306-
this.validate_bindings();
307-
this.validate_content();
308-
this.validate_event_handlers();
309-
}
312+
// Warnings
310313

311-
validate_attributes() {
312-
const { component, parent } = this;
314+
if (this.namespace !== namespaces.foreign) {
315+
if (name === 'is') {
316+
component.warn(attribute, {
317+
code: 'avoid-is',
318+
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
319+
});
320+
}
313321

314-
const attribute_map = new Map();
322+
if (react_attributes.has(attribute.name)) {
323+
component.warn(attribute, {
324+
code: 'invalid-html-attribute',
325+
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
326+
});
327+
}
328+
}
329+
});
330+
}
331+
332+
validate_attributes_a11y() {
333+
const { component } = this;
315334

316335
this.attributes.forEach(attribute => {
317336
if (attribute.is_spread) return;
@@ -408,60 +427,13 @@ export default class Element extends Node {
408427
});
409428
}
410429
}
411-
412-
413-
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
414-
component.error(attribute, {
415-
code: 'illegal-attribute',
416-
message: `'${name}' is not a valid attribute name`
417-
});
418-
}
419-
420-
if (name === 'slot') {
421-
if (!attribute.is_static) {
422-
component.error(attribute, {
423-
code: 'invalid-slot-attribute',
424-
message: 'slot attribute cannot have a dynamic value'
425-
});
426-
}
427-
428-
if (component.slot_outlets.has(name)) {
429-
component.error(attribute, {
430-
code: 'duplicate-slot-attribute',
431-
message: `Duplicate '${name}' slot`
432-
});
433-
434-
component.slot_outlets.add(name);
435-
}
436-
437-
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
438-
component.error(attribute, {
439-
code: 'invalid-slotted-content',
440-
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
441-
});
442-
}
443-
}
444-
445-
if (name === 'is') {
446-
component.warn(attribute, {
447-
code: 'avoid-is',
448-
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
449-
});
450-
}
451-
452-
if (react_attributes.has(attribute.name)) {
453-
component.warn(attribute, {
454-
code: 'invalid-html-attribute',
455-
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
456-
});
457-
}
458-
459-
attribute_map.set(attribute.name, attribute);
460430
});
461431
}
462432

433+
463434
validate_special_cases() {
464435
const { component, attributes, handlers } = this;
436+
465437
const attribute_map = new Map();
466438
const handlers_map = new Map();
467439

@@ -576,6 +548,63 @@ export default class Element extends Node {
576548
});
577549
}
578550
}
551+
552+
if (a11y_distracting_elements.has(this.name)) {
553+
// no-distracting-elements
554+
component.warn(this, {
555+
code: 'a11y-distracting-elements',
556+
message: `A11y: Avoid <${this.name}> elements`
557+
});
558+
}
559+
560+
if (this.name === 'figcaption') {
561+
let { parent } = this;
562+
let is_figure_parent = false;
563+
564+
while (parent) {
565+
if ((parent as Element).name === 'figure') {
566+
is_figure_parent = true;
567+
break;
568+
}
569+
if (parent.type === 'Element') {
570+
break;
571+
}
572+
parent = parent.parent;
573+
}
574+
575+
if (!is_figure_parent) {
576+
component.warn(this, {
577+
code: 'a11y-structure',
578+
message: 'A11y: <figcaption> must be an immediate child of <figure>'
579+
});
580+
}
581+
}
582+
583+
if (this.name === 'figure') {
584+
const children = this.children.filter(node => {
585+
if (node.type === 'Comment') return false;
586+
if (node.type === 'Text') return /\S/.test(node.data);
587+
return true;
588+
});
589+
590+
const index = children.findIndex(child => (child as Element).name === 'figcaption');
591+
592+
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
593+
component.warn(children[index], {
594+
code: 'a11y-structure',
595+
message: 'A11y: <figcaption> must be first or last child of <figure>'
596+
});
597+
}
598+
}
599+
}
600+
601+
validate_bindings_foreign() {
602+
this.bindings.forEach(binding => {
603+
this.component.error(binding, {
604+
code: 'invalid-binding',
605+
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
606+
});
607+
});
579608
}
580609

581610
validate_bindings() {

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Expression from '../../../nodes/shared/Expression';
88
import Text from '../../../nodes/Text';
99
import handle_select_value_binding from './handle_select_value_binding';
1010
import { Identifier, Node } from 'estree';
11+
import { namespaces } from '../../../../utils/namespaces';
1112

1213
export class BaseAttributeWrapper {
1314
node: Attribute;
@@ -67,15 +68,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
6768
}
6869
}
6970

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;
71+
if (this.parent.node.namespace == namespaces.foreign) {
72+
// leave attribute case alone for elements in the "foreign" namespace
73+
this.name = this.node.name;
74+
this.metadata = this.get_metadata();
75+
this.is_indirectly_bound_value = false;
76+
this.property_name = null;
77+
this.is_select_value_attribute = false;
78+
this.is_input_value = false;
79+
} else {
80+
this.name = fix_attribute_casing(this.node.name);
81+
this.metadata = this.get_metadata();
82+
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
83+
this.property_name = this.is_indirectly_bound_value
84+
? '__value'
85+
: this.metadata && this.metadata.property_name;
86+
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
87+
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
88+
}
89+
7690
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';
7991
this.should_cache = should_cache(this);
8092
}
8193

0 commit comments

Comments
 (0)