Introduce the foreign namespace and skip certain handling and warnings

Allows the use of svelte for DOM implementations that aren't html5
pull/5652/head
halfnelson 6 years ago
parent 8180c5dd6c
commit c9635a9afd

@ -136,44 +136,45 @@ export default class Element extends Node {
this.namespace = get_namespace(parent as Element, this, component.namespace);
if (this.name === 'textarea') {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
}
if (this.namespace !== namespaces.foreign) {
if (this.name === 'textarea') {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
}
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
info.children = [];
info.children = [];
}
}
}
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
if (!value_attribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
if (!value_attribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
}
}
}
const has_let = info.attributes.some(node => node.type === 'Let');
if (has_let) {
scope = scope.child();
@ -253,65 +254,83 @@ export default class Element extends Node {
});
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
this.component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
this.validate_attributes();
this.validate_event_handlers();
if (this.namespace === namespaces.foreign) {
this.validate_bindings_foreign();
} else {
this.validate_attributes_a11y();
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
}
if (this.name === 'figcaption') {
let { parent } = this;
let is_figure_parent = false;
}
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
validate_attributes() {
const { component, parent } = this;
if (!is_figure_parent) {
this.component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
this.attributes.forEach(attribute => {
if (attribute.is_spread) return;
const name = attribute.name.toLowerCase();
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
const index = children.findIndex(child => (child as Element).name === 'figcaption');
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
this.component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
component.slot_outlets.add(name);
}
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
}
}
this.validate_attributes();
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
this.validate_event_handlers();
}
// Warnings
validate_attributes() {
const { component, parent } = this;
if (this.namespace !== namespaces.foreign) {
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
const attribute_map = new Map();
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
}
});
}
validate_attributes_a11y() {
const { component } = this;
this.attributes.forEach(attribute => {
if (attribute.is_spread) return;
@ -408,60 +427,13 @@ export default class Element extends Node {
});
}
}
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
}
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
component.slot_outlets.add(name);
}
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
}
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
attribute_map.set(attribute.name, attribute);
});
}
validate_special_cases() {
const { component, attributes, handlers } = this;
const attribute_map = new Map();
const handlers_map = new Map();
@ -576,6 +548,63 @@ export default class Element extends Node {
});
}
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
}
if (this.name === 'figcaption') {
let { parent } = this;
let is_figure_parent = false;
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
if (!is_figure_parent) {
component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.findIndex(child => (child as Element).name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
}
}
}
validate_bindings_foreign() {
this.bindings.forEach(binding => {
this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
});
}
validate_bindings() {

@ -8,6 +8,7 @@ import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces';
export class BaseAttributeWrapper {
node: Attribute;
@ -67,15 +68,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
}
}
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = false;
this.property_name = null;
this.is_select_value_attribute = false;
this.is_input_value = false;
} else {
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
this.should_cache = should_cache(this);
}

@ -1,3 +1,4 @@
export const foreign = 'https://svelte.dev/docs#svelte_options';
export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg';
@ -5,13 +6,16 @@ export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns';
export const valid_namespaces = [
'foreign',
'html',
'mathml',
'svg',
'xlink',
'xml',
'xmlns',
foreign,
html,
mathml,
svg,
@ -20,4 +24,4 @@ export const valid_namespaces = [
xmlns
];
export const namespaces: Record<string, string> = { html, mathml, svg, xlink, xml, xmlns };
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };

@ -0,0 +1,18 @@
// Test support for the native namespaces preserving attribute case (eg svelte-native, svelte-nodegui).
export default {
html: `
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
`,
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
assert.equal(attr('page'), 'horizontalAlignment');
assert.equal(attr('button'), 'textWrap');
}
};

@ -0,0 +1,4 @@
<svelte:options namespace="foreign" />
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>

@ -0,0 +1,7 @@
<svelte:options namespace="foreign" />
<page>
<a>not actually a link</a>
<label>This isn't a html label</label>
<figure>This is maybe a QT figure</figure>
</page>

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'value' is not a valid binding. Foreign elements only support bind:this",
"pos": 81,
"start": {
"line": 6,
"column": 7,
"character": 81
},
"end": {
"line": 6,
"column": 28,
"character": 102
}
}]

@ -0,0 +1,6 @@
<svelte:options namespace="foreign" />
<script>
let whatever;
</script>
<input bind:value={whatever} />
Loading…
Cancel
Save