From c28f62a117f4d6b4ad9249f0b8322464cfe3d49c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 May 2018 19:55:36 -0400 Subject: [PATCH] quote slot names if necessary - fixes #1461 --- src/compile/nodes/Component.ts | 8 ++++---- src/compile/nodes/Element.ts | 18 ++++++++++-------- src/compile/nodes/Slot.ts | 13 ++++++++++--- src/utils/quoteIfNecessary.ts | 7 ++++++- .../Nested.html | 3 +++ .../component-slot-name-with-hyphen/_config.js | 3 +++ .../component-slot-name-with-hyphen/main.html | 13 +++++++++++++ 7 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 test/runtime/samples/component-slot-name-with-hyphen/Nested.html create mode 100644 test/runtime/samples/component-slot-name-with-hyphen/_config.js create mode 100644 test/runtime/samples/component-slot-name-with-hyphen/main.html diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts index f7acea4066..06c461cae9 100644 --- a/src/compile/nodes/Component.ts +++ b/src/compile/nodes/Component.ts @@ -5,7 +5,7 @@ import stringifyProps from '../../utils/stringifyProps'; import CodeBuilder from '../../utils/CodeBuilder'; import getTailSnippet from '../../utils/getTailSnippet'; import getObject from '../../utils/getObject'; -import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import { quoteNameIfNecessary } from '../../utils/quoteIfNecessary'; import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import Node from './shared/Node'; import Block from '../dom/Block'; @@ -126,7 +126,7 @@ export default class Component extends Node { const componentInitProperties = [`root: #component.root`]; if (this.children.length > 0) { - const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name)}: @createFragment()`); + const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`); componentInitProperties.push(`slots: { ${slots.join(', ')} }`); this.children.forEach((child: Node) => { @@ -185,7 +185,7 @@ export default class Component extends Node { changes.push(condition ? `${condition} && ${value}` : value); } else { - const obj = `{ ${quoteIfNecessary(name)}: ${attr.getValue()} }`; + const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`; initialProps.push(obj); changes.push(condition ? `${condition} && ${obj}` : obj); @@ -602,7 +602,7 @@ export default class Component extends Node { }); const slotted = Object.keys(appendTarget.slots) - .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) + .map(name => `${quoteNameIfNecessary(name)}: () => \`${appendTarget.slots[name]}\``) .join(', '); options.push(`slotted: { ${slotted} }`); diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 9749aa09fb..58eeafafeb 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -5,7 +5,7 @@ import isVoidElementName from '../../utils/isVoidElementName'; import validCalleeObjects from '../../utils/validCalleeObjects'; import reservedNames from '../../utils/reservedNames'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; -import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary'; import Compiler from '../Compiler'; import Node from './shared/Node'; import Block from '../dom/Block'; @@ -260,8 +260,9 @@ export default class Element extends Node { const name = this.var; const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot'); + const prop = slot && quotePropIfNecessary(slot.chunks[0].data); const initialMountNode = this.slotted ? - `${this.findNearest(/^Component/).var}._slotted.${slot.chunks[0].data}` : // TODO this looks bonkers + `${this.findNearest(/^Component/).var}._slotted${prop}` : // TODO this looks bonkers parentNode; block.addVariable(name); @@ -571,7 +572,7 @@ export default class Element extends Node { updates.push(condition ? `${condition} && ${snippet}` : snippet); } else { - const snippet = `{ ${quoteIfNecessary(attr.name)}: ${attr.getValue()} }`; + const snippet = `{ ${quoteNameIfNecessary(attr.name)}: ${attr.getValue()} }`; initialProps.push(snippet); updates.push(condition ? `${condition} && ${snippet}` : snippet); @@ -824,7 +825,8 @@ export default class Element extends Node { remount(name: string) { const slot = this.attributes.find(attribute => attribute.name === 'slot'); if (slot) { - return `@appendNode(${this.var}, ${name}._slotted.${this.getStaticAttributeValue('slot')});`; + const prop = quotePropIfNecessary(slot.chunks[0].data); + return `@appendNode(${this.var}, ${name}._slotted${prop});`; } return `@appendNode(${this.var}, ${name}._slotted.default);`; @@ -881,16 +883,16 @@ export default class Element extends Node { if (attribute.name === 'value' && this.name === 'textarea') { textareaContents = attribute.stringifyForSsr(); } else if (attribute.isTrue) { - args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`); + args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`); } else if ( booleanAttributes.has(attribute.name) && attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text' ) { // a boolean attribute with one non-Text chunk - args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`); + args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`); } else { - args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`); + args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`); } } }); @@ -962,7 +964,7 @@ function getClaimStatement( ) { const attributes = node.attributes .filter((attr: Node) => attr.type === 'Attribute') - .map((attr: Node) => `${quoteIfNecessary(attr.name)}: true`) + .map((attr: Node) => `${quoteNameIfNecessary(attr.name)}: true`) .join(', '); const name = namespace ? node.name : node.name.toUpperCase(); diff --git a/src/compile/nodes/Slot.ts b/src/compile/nodes/Slot.ts index 7e55b02055..4fe44f0710 100644 --- a/src/compile/nodes/Slot.ts +++ b/src/compile/nodes/Slot.ts @@ -5,6 +5,11 @@ import Node from './shared/Node'; import Element from './Element'; import Attribute from './Attribute'; import Block from '../dom/Block'; +import { quotePropIfNecessary } from '../../utils/quoteIfNecessary'; + +function sanitize(name) { + return name.replace(/[^a-zA-Z]+/g, '_').replace(/^_/, '').replace(/_$/, ''); +} export default class Slot extends Element { type: 'Element'; @@ -36,8 +41,8 @@ export default class Slot extends Element { const slotName = this.getStaticAttributeValue('name') || 'default'; compiler.slots.add(slotName); - const content_name = block.getUniqueName(`slot_content_${slotName}`); - const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`; + const content_name = block.getUniqueName(`slot_content_${sanitize(slotName)}`); + const prop = quotePropIfNecessary(slotName); block.addVariable(content_name, `#component._slotted${prop}`); const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode; @@ -151,9 +156,11 @@ export default class Slot extends Element { ssr() { const name = this.attributes.find(attribute => attribute.name === 'name'); + const slotName = name && name.chunks[0].data || 'default'; + const prop = quotePropIfNecessary(slotName); - this.compiler.target.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``); + this.compiler.target.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``); this.children.forEach((child: Node) => { child.ssr(); diff --git a/src/utils/quoteIfNecessary.ts b/src/utils/quoteIfNecessary.ts index 118c4909d7..6a6f829e98 100644 --- a/src/utils/quoteIfNecessary.ts +++ b/src/utils/quoteIfNecessary.ts @@ -1,7 +1,12 @@ import isValidIdentifier from './isValidIdentifier'; import reservedNames from './reservedNames'; -export default function quoteIfNecessary(name) { +export function quoteNameIfNecessary(name) { if (!isValidIdentifier(name)) return `"${name}"`; return name; +} + +export function quotePropIfNecessary(name) { + if (!isValidIdentifier(name)) return `["${name}"]`; + return `.${name}`; } \ No newline at end of file diff --git a/test/runtime/samples/component-slot-name-with-hyphen/Nested.html b/test/runtime/samples/component-slot-name-with-hyphen/Nested.html new file mode 100644 index 0000000000..7faee7a9a1 --- /dev/null +++ b/test/runtime/samples/component-slot-name-with-hyphen/Nested.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/test/runtime/samples/component-slot-name-with-hyphen/_config.js b/test/runtime/samples/component-slot-name-with-hyphen/_config.js new file mode 100644 index 0000000000..e824c6c7bc --- /dev/null +++ b/test/runtime/samples/component-slot-name-with-hyphen/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

Hello

' +}; diff --git a/test/runtime/samples/component-slot-name-with-hyphen/main.html b/test/runtime/samples/component-slot-name-with-hyphen/main.html new file mode 100644 index 0000000000..b3b6f8e9f6 --- /dev/null +++ b/test/runtime/samples/component-slot-name-with-hyphen/main.html @@ -0,0 +1,13 @@ + +

Hello

+
+ +