Skip to content

Commit

Permalink
Fixes #80
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Nov 30, 2022
1 parent 9551290 commit 568a4be
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 53 deletions.
63 changes: 33 additions & 30 deletions src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,46 +707,46 @@ class AstSerializer {

async renderStartTag(node, tagName, component, renderingMode, options) {
let content = "";
let attrObject;

if(tagName) {
// parse5 doesn’t preserve whitespace around <html>, <head>, and after </body>
if(renderingMode === "page" && tagName === "head") {
content += AstSerializer.EOL;
}
if(!tagName) {
return { content };
}

let attrs = this.getAttributes(node, component, options);
let parentComponent = this.components[options.closestParentComponent];
let attrObject;
// parse5 doesn’t preserve whitespace around <html>, <head>, and after </body>
if(renderingMode === "page" && tagName === "head") {
content += AstSerializer.EOL;
}

// webc:keep webc:root should use the style hash class name and host attributes since they won’t be added to the host component
if(parentComponent && parentComponent.ignoreRootTag && this.hasAttribute(node, AstSerializer.attrs.ROOT) && this.hasAttribute(node, AstSerializer.attrs.KEEP)) {
if(parentComponent.scopedStyleHash) {
attrs.push({ name: "class", value: parentComponent.scopedStyleHash });
}
let attrs = this.getAttributes(node, component, options);
let parentComponent = this.components[options.closestParentComponent];

for(let hostAttr of options.hostComponentNode.attrs) {
attrs.push(hostAttr);
}
// webc:keep webc:root should use the style hash class name and host attributes since they won’t be added to the host component
if(parentComponent && parentComponent.ignoreRootTag && this.hasAttribute(node, AstSerializer.attrs.ROOT) && this.hasAttribute(node, AstSerializer.attrs.KEEP)) {
if(parentComponent.scopedStyleHash) {
attrs.push({ name: "class", value: parentComponent.scopedStyleHash });
}
for(let hostAttr of options.hostComponentNode?.attrs || []) {
attrs.push(hostAttr);
}
}

attrObject = AttributeSerializer.dedupeAttributes(attrs);
attrObject = AttributeSerializer.dedupeAttributes(attrs);

if(options.isMatchingSlotSource) {
delete attrObject.slot;
}
if(options.isMatchingSlotSource) {
delete attrObject.slot;
}

let showInRawMode = options.rawMode && !this.hasAttribute(node, AstSerializer.attrs.NOKEEP);
if(showInRawMode || !this.isTagIgnored(node, component, renderingMode)) {
let data = Object.assign({}, this.helpers, options.componentProps, this.globalData);
content += `<${tagName}${await AttributeSerializer.getString(attrObject, data, {
filePath: options.closestParentComponent || this.filePath
})}>`;
}
let nodeData = Object.assign({}, this.helpers, options.componentProps, options.hostComponentData, this.globalData);
let showInRawMode = options.rawMode && !this.hasAttribute(node, AstSerializer.attrs.NOKEEP);
if(showInRawMode || !this.isTagIgnored(node, component, renderingMode)) {
content += `<${tagName}${await AttributeSerializer.getString(attrObject, nodeData)}>`;
}

return {
content,
attrs: attrObject
attrs: attrObject,
nodeData,
};
}

Expand Down Expand Up @@ -1098,6 +1098,7 @@ class AstSerializer {
return false;
}

// Used for @html and webc:if
async evaluateAttribute(name, attrContent, options) {
let data = Object.assign({}, this.helpers, options.componentProps, this.globalData);
let content = await ModuleScript.evaluateScript(name, attrContent, data, {
Expand Down Expand Up @@ -1268,11 +1269,11 @@ class AstSerializer {
// TODO warning if top level page component using a style hash but has no root element (text only?)

// Start tag
let { content: startTagContent, attrs } = await this.renderStartTag(node, tagName, component, renderingMode, options);
let { content: startTagContent, attrs, nodeData } = await this.renderStartTag(node, tagName, component, renderingMode, options);
content += this.outputHtml(startTagContent, streamEnabled);

if(component) {
options.componentProps = AttributeSerializer.normalizeAttributesForData(attrs) || {};
options.componentProps = await AttributeSerializer.normalizeAttributesForData(attrs, nodeData);
options.componentProps.uid = options.closestParentUid;
}

Expand Down Expand Up @@ -1307,7 +1308,9 @@ class AstSerializer {
// Component content (foreshadow dom)
if(!options.rawMode && component) {
this.addComponentDependency(component, tagName, options);

options.hostComponentNode = node;
options.hostComponentData = AttributeSerializer.convertAttributesToDataObject(node.attrs);

let slots = this.getSlottedContentNodes(node, defaultSlotNodes);
let { html: foreshadowDom } = await this.compileNode(component.ast, slots, options, streamEnabled);
Expand Down
84 changes: 64 additions & 20 deletions src/attributeSerializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,55 +79,99 @@ class AttributeSerializer {
return name;
}

static async normalizeAttribute(name, value, data) {
static peekAttribute(name) {
if(name.startsWith(AstSerializer.prefixes.props)) {
return {
name: name.slice(AstSerializer.prefixes.props.length),
type: "private", // property
};
}

if(name.startsWith(AstSerializer.prefixes.dynamic)) {
let attrValue = await ModuleScript.evaluateScript(name, value, data);
return {
name: name.slice(AstSerializer.prefixes.dynamic.length),
type: "script",
};
}

return {
name,
};
}

static async normalizeAttribute(rawName, value, data) {
let {name, type} = AttributeSerializer.peekAttribute(rawName);

if(type === "script") {
let attrValue = await ModuleScript.evaluateScript(rawName, value, data);

return {
name: name.slice(1),
name,
value: attrValue,
};
}

return {
name,
value
value,
};
}

// Remove props prefixes, swaps dash to camelcase
static normalizeAttributesForData(attrs) {
let data = Object.assign({}, attrs);
for(let name in data) {
let newName = name;
static async normalizeAttributesForData(attrs, data) {
let newData = {};

// dynamic props from host components need to be normalized
for(let key in data) {
let {type} = AttributeSerializer.peekAttribute(key);
if(type === "script") {
let { name, value } = await AttributeSerializer.normalizeAttribute(key, data[key], data || {});
data[name] = value;
delete data[key];
}
}

for(let originalName in attrs) {
let { name, value } = await AttributeSerializer.normalizeAttribute(originalName, attrs[originalName], data || {});

// TODO #71 default enabled in WebC v0.8.0
// prop does nothing
// prop-name becomes propName
// @prop-name becomes propName
if(name.startsWith(AstSerializer.prefixes.props)) {
newName = name.slice(AstSerializer.prefixes.props.length);
}
// TODO #71 default enabled in WebC v0.8.0
// newName = AttributeSerializer.camelCaseAttributeName(newName);
// name = AttributeSerializer.camelCaseAttributeName(newName);

if(newName !== name) {
data[newName] = data[name];
delete data[name];
}
newData[name] = value;
}

return newData;
}

// Change :dynamic to `dynamic` for data resolution
static convertAttributesToDataObject(attrs) {
let data = {};
for(let {name, value} of attrs || []) {
data[name] = value;
}
return data;
}

static async getString(attrs, data, options) {
static async getString(attrs, data) {
let str = [];
let attrObject = attrs;
if(Array.isArray(attrObject)) {
attrObject = AttributeSerializer.dedupeAttributes(attrs);
}

for(let key in attrObject) {
let {name, value} = await AttributeSerializer.normalizeAttribute(key, attrObject[key], data, options);
let {type} = AttributeSerializer.peekAttribute(key);
if(type === "private") { // properties
continue;
}

let {name, value} = await AttributeSerializer.normalizeAttribute(key, attrObject[key], data);

// Note we filter any falsy attributes (except "")
if(name.startsWith(AstSerializer.prefixes.props) || !value && value !== "") {
if(!value && value !== "") {
continue;
}

Expand Down
12 changes: 9 additions & 3 deletions test/attributeSerializerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import test from "ava";
import { AttributeSerializer } from "../src/attributeSerializer.js";

// Inputs are guaranteed to be lower case (per the HTML specification)
test("Normalize attribute", async t => {
t.deepEqual(await AttributeSerializer.normalizeAttribute("test", "value"), { name: "test", value: "value" });
t.deepEqual(await AttributeSerializer.normalizeAttribute("@test", "value"), { name: "test", value: "value" });
t.deepEqual(await AttributeSerializer.normalizeAttribute(":test", "value", { value: 1 }), { name: "test", value: 1 });
});

test("Normalize attribute name", async t => {
t.is(AttributeSerializer.camelCaseAttributeName("test"), "test");
t.is(AttributeSerializer.camelCaseAttributeName("my-test"), "myTest");
Expand All @@ -15,7 +21,7 @@ test("Normalize attribute name", async t => {
});

test("Normalize attributes for data", async t => {
t.deepEqual(AttributeSerializer.normalizeAttributesForData({"test": 1 }), {"test": 1 });
t.deepEqual(AttributeSerializer.normalizeAttributesForData({"my-test": 1 }), {"my-test": 1 });
t.deepEqual(AttributeSerializer.normalizeAttributesForData({"my-other-test": 1 }), {"my-other-test": 1 });
t.deepEqual(await AttributeSerializer.normalizeAttributesForData({"test": 1 }), {"test": 1 });
t.deepEqual(await AttributeSerializer.normalizeAttributesForData({"my-test": 1 }), {"my-test": 1 });
t.deepEqual(await AttributeSerializer.normalizeAttributesForData({"my-other-test": 1 }), {"my-other-test": 1 });
});
30 changes: 30 additions & 0 deletions test/issue80Test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from "ava";
import { WebC } from "../webc.js";

test("Dynamic attributes in components (attribute -> dynamic -> dynamic) #80", async t => {
let component = new WebC();

component.setInputPath("./test/stubs/issue-80/page.webc");
component.defineComponents("./test/stubs/issue-80/b.webc");
component.defineComponents("./test/stubs/issue-80/c.webc");

let { html } = await component.compile();

t.is(html, `<div foo="xyz">
<img src="xyz">
</div>`);
});

test("Dynamic attributes in components (dynamic -> dynamic -> dynamic) #80", async t => {
let component = new WebC();

component.setInputPath("./test/stubs/issue-80-b/page.webc");
component.defineComponents("./test/stubs/issue-80-b/b.webc");
component.defineComponents("./test/stubs/issue-80-b/c.webc");

let { html } = await component.compile();

t.is(html, `<div foo="xyz">
<img src="xyz">
</div>`);
});
3 changes: 3 additions & 0 deletions test/stubs/issue-80-b/b.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div :foo="foo">
<c :foo="foo"></c>
</div>
1 change: 1 addition & 0 deletions test/stubs/issue-80-b/c.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img :src="foo" />
1 change: 1 addition & 0 deletions test/stubs/issue-80-b/page.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<b :foo="`xyz`" />
3 changes: 3 additions & 0 deletions test/stubs/issue-80/b.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div :foo="foo">
<c :foo="foo"></c>
</div>
1 change: 1 addition & 0 deletions test/stubs/issue-80/c.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img :src="foo" />
1 change: 1 addition & 0 deletions test/stubs/issue-80/page.webc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<b foo="xyz" />

0 comments on commit 568a4be

Please sign in to comment.