diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index c0da7ea5c0..6d9c8059f5 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -636,23 +636,6 @@ export default class Element extends Node { }); } - get_static_attribute_value(name: string) { - const attribute = this.attributes.find( - (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name - ); - - if (!attribute) return null; - - if (attribute.is_true) return true; - if (attribute.chunks.length === 0) return ''; - - if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') { - return attribute.chunks[0].data; - } - - return null; - } - is_media_node() { return this.name === 'audio' || this.name === 'video'; } diff --git a/src/compile/nodes/Slot.ts b/src/compile/nodes/Slot.ts index 0a4f4af848..1540fa6271 100644 --- a/src/compile/nodes/Slot.ts +++ b/src/compile/nodes/Slot.ts @@ -5,6 +5,7 @@ import Attribute from './Attribute'; export default class Slot extends Element { type: 'Element'; name: string; + slot_name: string; attributes: Attribute[]; children: Node[]; @@ -27,8 +28,8 @@ export default class Slot extends Element { }); } - const slot_name = attr.value[0].data; - if (slot_name === 'default') { + this.slot_name = attr.value[0].data; + if (this.slot_name === 'default') { component.error(attr, { code: `invalid-slot-name`, message: `default is a reserved word — it cannot be used as a slot name` @@ -46,6 +47,8 @@ export default class Slot extends Element { // validator.slots.add(slot_name); }); + if (!this.slot_name) this.slot_name = 'default'; + // if (node.attributes.length === 0) && validator.slots.has('default')) { // validator.error(node, { // code: `duplicate-slot`, @@ -53,21 +56,4 @@ export default class Slot extends Element { // }); // } } - - get_static_attribute_value(name: string) { - const attribute = this.attributes.find( - attr => attr.name.toLowerCase() === name - ); - - if (!attribute) return null; - - if (attribute.is_true) return true; - if (attribute.chunks.length === 0) return ''; - - if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') { - return attribute.chunks[0].data; - } - - return null; - } } \ No newline at end of file diff --git a/src/compile/nodes/shared/Node.ts b/src/compile/nodes/shared/Node.ts index 4317e23e4f..92c1cc40d7 100644 --- a/src/compile/nodes/shared/Node.ts +++ b/src/compile/nodes/shared/Node.ts @@ -37,17 +37,34 @@ export default class Node { } } + find_nearest(selector: RegExp) { + if (selector.test(this.type)) return this; + if (this.parent) return this.parent.find_nearest(selector); + } + + get_static_attribute_value(name: string) { + const attribute = this.attributes.find( + (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name + ); + + if (!attribute) return null; + + if (attribute.is_true) return true; + if (attribute.chunks.length === 0) return ''; + + if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') { + return attribute.chunks[0].data; + } + + return null; + } + has_ancestor(type: string) { return this.parent ? this.parent.type === type || this.parent.has_ancestor(type) : false; } - find_nearest(selector: RegExp) { - if (selector.test(this.type)) return this; - if (this.parent) return this.parent.find_nearest(selector); - } - warn_if_empty_block() { if (!/Block$/.test(this.type) || !this.children) return; if (this.children.length > 1) return; diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index aef29a194b..363d807916 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -10,7 +10,6 @@ import add_to_set from '../utils/add_to_set'; import get_object from '../utils/get_object'; import { extract_names } from '../utils/scope'; import { nodes_match } from '../../utils/nodes_match'; -import { sanitize } from '../../utils/names'; export default function dom( component: Component, @@ -305,8 +304,7 @@ export default function dom( const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$'); if (renderer.slots.size > 0) { - const arr = Array.from(renderer.slots); - filtered_declarations.push(...arr.map(name => `$$slot_${sanitize(name)}`), '$$scope'); + filtered_declarations.push('$$slots', '$$scope'); } if (renderer.binding_groups.length > 0) { @@ -399,7 +397,7 @@ export default function dom( ${component.javascript} - ${renderer.slots.size && `let { ${[...renderer.slots].map(name => `$$slot_${sanitize(name)}`).join(', ')}, $$scope } = $$props;`} + ${renderer.slots.size && `let { $$slots = {}, $$scope } = $$props;`} ${renderer.binding_groups.length > 0 && `const $$binding_groups = [${renderer.binding_groups.map(_ => `[]`).join(', ')}];`} diff --git a/src/compile/render-dom/wrappers/Element/Attribute.ts b/src/compile/render-dom/wrappers/Element/Attribute.ts index 5b581677d8..c0c8327bb0 100644 --- a/src/compile/render-dom/wrappers/Element/Attribute.ts +++ b/src/compile/render-dom/wrappers/Element/Attribute.ts @@ -117,7 +117,7 @@ export default class AttributeWrapper { updater = `@set_input_type(${element.var}, ${should_cache ? last : value});`; } else if (is_select_value_attribute) { // annoying special case - const is_multiple_select = element.get_static_attribute_value('multiple'); + const is_multiple_select = element.node.get_static_attribute_value('multiple'); const i = block.get_unique_name('i'); const option = block.get_unique_name('option'); diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts index 8657a1b054..f2bf82a826 100644 --- a/src/compile/render-dom/wrappers/Element/Binding.ts +++ b/src/compile/render-dom/wrappers/Element/Binding.ts @@ -153,7 +153,7 @@ export default class BindingWrapper { break; case 'value': - if (parent.get_static_attribute_value('type') === 'file') { + if (parent.node.get_static_attribute_value('type') === 'file') { update_dom = null; } } diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index d54a66dffa..fa025bacbe 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -20,6 +20,7 @@ import add_event_handlers from '../shared/add_event_handlers'; import add_actions from '../shared/add_actions'; import create_debugging_comment from '../shared/create_debugging_comment'; import { get_context_merger } from '../shared/get_context_merger'; +import Slot from '../../../nodes/Slot'; const events = [ { @@ -213,8 +214,7 @@ export default class ElementWrapper extends Wrapper { const { renderer } = this; if (this.node.name === 'slot') { - const slotName = this.get_static_attribute_value('name') || 'default'; - renderer.slots.add(slotName); + renderer.slots.add((this.node as Slot).slot_name); } if (this.node.name === 'noscript') return; @@ -804,23 +804,6 @@ export default class ElementWrapper extends Wrapper { }); } - get_static_attribute_value(name: string) { - const attribute = this.node.attributes.find( - (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name - ); - - if (!attribute) return null; - - if (attribute.is_true) return true; - if (attribute.chunks.length === 0) return ''; - - if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') { - return attribute.chunks[0].data; - } - - return null; - } - add_css_class(class_name = this.component.stylesheet.id) { const class_attribute = this.attributes.find(a => a.name === 'class'); if (class_attribute && !class_attribute.is_true) { diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts index 958a98bcb5..bc13014a29 100644 --- a/src/compile/render-dom/wrappers/InlineComponent/index.ts +++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts @@ -119,16 +119,19 @@ export default class InlineComponentWrapper extends Wrapper { const uses_spread = !!this.node.attributes.find(a => a.is_spread); - const slot_props = Array.from(this.slots).map(([name, slot]) => `$$slot_${sanitize(name)}: [${slot.block.name}${slot.fn ? `, ${slot.fn}` : ''}]`); - if (slot_props.length > 0) slot_props.push(`$$scope: { ctx }`); + const slot_props = Array.from(this.slots).map(([name, slot]) => `${quote_name_if_necessary(name)}: [${slot.block.name}${slot.fn ? `, ${slot.fn}` : ''}]`); + + const initial_props = slot_props.length > 0 + ? [`$$slots: ${stringify_props(slot_props)}`, `$$scope: { ctx }`] + : []; const attribute_object = uses_spread - ? stringify_props(slot_props) + ? stringify_props(initial_props) : stringify_props( - this.node.attributes.map(attr => `${quote_name_if_necessary(attr.name)}: ${attr.get_value(block)}`).concat(slot_props) + this.node.attributes.map(attr => `${quote_name_if_necessary(attr.name)}: ${attr.get_value(block)}`).concat(initial_props) ); - if (this.node.attributes.length || this.node.bindings.length || slot_props.length) { + if (this.node.attributes.length || this.node.bindings.length || initial_props.length) { if (!uses_spread && this.node.bindings.length === 0) { component_opts.push(`props: ${attribute_object}`); } else { diff --git a/src/compile/render-dom/wrappers/Slot.ts b/src/compile/render-dom/wrappers/Slot.ts index 3760e73ce0..38896b4546 100644 --- a/src/compile/render-dom/wrappers/Slot.ts +++ b/src/compile/render-dom/wrappers/Slot.ts @@ -4,7 +4,7 @@ import Block from '../Block'; import Slot from '../../nodes/Slot'; import FragmentWrapper from './Fragment'; import deindent from '../../utils/deindent'; -import { sanitize } from '../../../utils/names'; +import { sanitize, quote_prop_if_necessary } from '../../../utils/names'; import add_to_set from '../../utils/add_to_set'; import get_slot_data from '../../utils/get_slot_data'; import { stringify_props } from '../../utils/stringify_props'; @@ -55,7 +55,7 @@ export default class SlotWrapper extends Wrapper { ) { const { renderer } = this; - const slot_name = this.node.get_static_attribute_value('name') || 'default'; + const { slot_name } = this.node; renderer.slots.add(slot_name); let get_slot_changes; @@ -99,7 +99,7 @@ export default class SlotWrapper extends Wrapper { const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot`); block.builders.init.add_block(deindent` - const ${slot_definition} = ctx.$$slot_${sanitize(slot_name)}; + const ${slot_definition} = ctx.$$slots${quote_prop_if_necessary(slot_name)}; const ${slot} = @create_slot(${slot_definition}, ctx, ${get_slot_context}); `); diff --git a/src/compile/render-ssr/handlers/Slot.ts b/src/compile/render-ssr/handlers/Slot.ts index 51e37ab9cf..77273e8009 100644 --- a/src/compile/render-ssr/handlers/Slot.ts +++ b/src/compile/render-ssr/handlers/Slot.ts @@ -2,10 +2,7 @@ import { quote_prop_if_necessary } from '../../../utils/names'; import get_slot_data from '../../utils/get_slot_data'; export default function(node, renderer, options) { - const name = node.attributes.find(attribute => attribute.name === 'name'); - - const slot_name = name && name.chunks[0].data || 'default'; - const prop = quote_prop_if_necessary(slot_name); + const prop = quote_prop_if_necessary(node.slot_name); const slot_data = get_slot_data(node.attributes, true); diff --git a/src/utils/names.ts b/src/utils/names.ts index 5047fe850c..1f0a9cc29e 100644 --- a/src/utils/names.ts +++ b/src/utils/names.ts @@ -111,5 +111,9 @@ export function quote_prop_if_necessary(name: string) { } export function sanitize(name: string) { - return name.replace(/[^a-zA-Z]+/g, '_').replace(/^_/, '').replace(/_$/, ''); -} \ No newline at end of file + return name + .replace(/[^a-zA-Z0-9_]+/g, '_') + .replace(/^_/, '') + .replace(/_$/, '') + .replace(/^[0-9]/, '_$&'); +} diff --git a/test/runtime/samples/component-slot-names-sanitized/Nested.svelte b/test/runtime/samples/component-slot-names-sanitized/Nested.svelte new file mode 100644 index 0000000000..ca8328920d --- /dev/null +++ b/test/runtime/samples/component-slot-names-sanitized/Nested.svelte @@ -0,0 +1,8 @@ +