Skip to content

Commit

Permalink
Introduce a native namespace and skip certain handling and warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
halfnelson committed Nov 7, 2020
1 parent 9745b61 commit 96c027c
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 85 deletions.
173 changes: 97 additions & 76 deletions src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default class Element extends Node {

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

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

if (this.name === 'option') {
if (this.name === 'option' && !this.is_native()) {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
Expand Down Expand Up @@ -248,51 +248,53 @@ 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`
});
}
if (!this.is_native()) {
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
this.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;
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;
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
if (parent.type === 'Element') {
break;

if (!is_figure_parent) {
this.component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
}
parent = parent.parent;
}

if (!is_figure_parent) {
this.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;
});
}
}

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');
const index = children.findIndex(child => (child as Element).name === 'figcaption');

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>'
});
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>'
});
}
}
}

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

const attribute_map = new Map();

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 (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'
});
}
}

// The rest of the warnings don't apply to native elements
if (this.is_native()) return;

// Warnings

// aria-props
if (name.startsWith('aria-')) {
if (invisible_elements.has(this.name)) {
Expand Down Expand Up @@ -404,52 +443,20 @@ 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'
});
}

attribute_map.set(attribute.name, attribute);
});
}

validate_special_cases() {
const { component, attributes, handlers } = this;

if (this.is_native()) return;

const attribute_map = new Map();
const handlers_map = new Map();

Expand Down Expand Up @@ -595,6 +602,16 @@ export default class Element extends Node {
return value;
};

if (this.is_native()) {
this.bindings.forEach(binding => {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Native elements only support bind:this`
});
});
return;
}

this.bindings.forEach(binding => {
const { name } = binding;

Expand Down Expand Up @@ -754,7 +771,7 @@ export default class Element extends Node {
}

validate_content() {
if (!a11y_required_content.has(this.name)) return;
if (!a11y_required_content.has(this.name) || this.is_native()) return;
if (
this.bindings
.some((binding) => ['textContent', 'innerHTML'].includes(binding.name))
Expand Down Expand Up @@ -831,6 +848,10 @@ export default class Element extends Node {
return this.name === 'audio' || this.name === 'video';
}

is_native() {
return this.namespace == namespaces['native'];
}

add_css_class() {
if (this.attributes.some(attr => attr.is_spread)) {
this.needs_manual_style_scoping = true;
Expand Down
27 changes: 19 additions & 8 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,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.is_native()) {
// leave attribute case alone for elements in the "native" 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);
}

Expand Down
6 changes: 5 additions & 1 deletion src/compiler/utils/namespaces.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const native = 'http://svelte-native.technology/2019/native';
export const svg = 'http://www.w3.org/2000/svg';
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 = [
'html',
'mathml',
'native',
'svg',
'xlink',
'xml',
'xmlns',
html,
mathml,
native,
svg,
xlink,
xml,
xmlns
];

export const namespaces: Record<string, string> = { html, mathml, svg, xlink, xml, xmlns };
export const namespaces: Record<string, string> = { html, mathml, native, svg, xlink, xml, xmlns };
18 changes: 18 additions & 0 deletions test/runtime/samples/attribute-casing-native-namespace/_config.js
Original file line number Diff line number Diff line change
@@ -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');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<svelte:options namespace="native" />
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
7 changes: 7 additions & 0 deletions test/validator/samples/a11y-in-native-namespace/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<svelte:options namespace="native" />
<page>
<a>not actually a link</a>
<label>This isn't a html label</label>
<figure>This is maybe a QT figure</figure>
</page>

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'value' is not a valid binding. Native elements only support bind:this",
"pos": 80,
"start": {
"line": 6,
"column": 7,
"character": 80
},
"end": {
"line": 6,
"column": 28,
"character": 101
}
}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<svelte:options namespace="native" />
<script>
let whatever;
</script>

<input bind:value={whatever} />

0 comments on commit 96c027c

Please sign in to comment.