Skip to content

Commit 84038b5

Browse files
committed
Fixes #28
1 parent d5e9563 commit 84038b5

12 files changed

+230
-7
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ We do provide two built-in transforms in WebC: JavaScript Render Functions (`web
274274

275275
See [_`webc:if` on the WebC Reference_](https://www.11ty.dev/docs/languages/webc/#webcif)
276276

277+
### Loops
278+
279+
See [_`webc:for` on the WebC Reference_](https://www.11ty.dev/docs/languages/webc/#webcfor-loops)
280+
277281
### Attributes
278282

279283
See [_Attributes and `webc:root` on the WebC Reference_](https://www.11ty.dev/docs/languages/webc/#attributes-and-webcroot)

src/ast.js

+60-7
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { DepGraph } from "dependency-graph";
55
import { WebC } from "../webc.js";
66
import { Path } from "./path.js";
77
import { AstQuery } from "./astQuery.js";
8+
import { AstModify } from "./astModify.js";
89
import { AssetManager } from "./assetManager.js";
910
import { CssPrefixer } from "./css.js";
11+
import { Looping } from "./looping.js";
1012
import { AttributeSerializer } from "./attributeSerializer.js";
1113
import { ModuleScript } from "./moduleScript.cjs";
1214
import { Streams } from "./streams.js";
@@ -138,7 +140,8 @@ class AstSerializer {
138140
TEXT: "@text",
139141
ATTRIBUTES: "@attributes",
140142
SETUP: "webc:setup",
141-
IGNORE: "webc:ignore", // ignore this node
143+
IGNORE: "webc:ignore", // ignore the node
144+
LOOP: "webc:for",
142145
};
143146

144147
static transformTypes = {
@@ -401,7 +404,7 @@ class AstSerializer {
401404
}
402405
}
403406

404-
let nodeData = this.dataCascade.getData( options.componentProps, options.hostComponentData, parentComponent?.setupScript );
407+
let nodeData = this.dataCascade.getData( options.componentProps, options.hostComponentData, parentComponent?.setupScript, options.injectedData );
405408
let evaluatedAttributes = await AttributeSerializer.evaluateAttributesArray(attrs, nodeData, options.closestParentComponent);
406409
let finalAttributesObject = AttributeSerializer.mergeAttributes(evaluatedAttributes);
407410

@@ -458,7 +461,7 @@ class AstSerializer {
458461
slotsText.default = this.getPreparsedRawTextContent(o.hostComponentNode, o);
459462
}
460463

461-
let context = this.dataCascade.getData(options.componentProps, options.currentTagAttributes, parentComponent?.setupScript, {
464+
let context = this.dataCascade.getData(options.componentProps, options.currentTagAttributes, parentComponent?.setupScript, options.injectedData, {
462465
// Ideally these would be under `webc.*`
463466
filePath: this.filePath,
464467
slots: {
@@ -719,7 +722,7 @@ class AstSerializer {
719722
// Used for @html and webc:if
720723
async evaluateAttribute(name, attrContent, options) {
721724
let parentComponent = this.componentManager.get(options.closestParentComponent);
722-
let data = this.dataCascade.getData(options.componentProps, parentComponent?.setupScript);
725+
let data = this.dataCascade.getData(options.componentProps, parentComponent?.setupScript, options.injectedData);
723726

724727
let { returns } = await ModuleScript.evaluateScriptInline(attrContent, data, `Check the dynamic attribute: \`${name}="${attrContent}"\`.`, options.closestParentComponent);
725728
return returns;
@@ -804,18 +807,18 @@ class AstSerializer {
804807
addImpliedWebCAttributes(node) {
805808
// if(AstQuery.getTagName(node) === "template") {
806809
if(AstQuery.isDeclarativeShadowDomNode(node)) {
807-
node.attrs.push({ name: AstSerializer.attrs.RAW, value: "" });
810+
AstModify.addAttribute(node, AstSerializer.attrs.RAW, "");
808811
}
809812

810813
// webc:type="js" (WebC v0.9.0+) has implied webc:is="template" webc:nokeep
811814
if(AstQuery.getAttributeValue(node, AstSerializer.attrs.TYPE) === AstSerializer.transformTypes.JS) {
812815
// this check is perhaps unnecessary since KEEP has a higher precedence than NOKEEP
813816
if(!AstQuery.hasAttribute(node, AstSerializer.attrs.KEEP)) {
814-
node.attrs.push({ name: AstSerializer.attrs.NOKEEP, value: "" });
817+
AstModify.addAttribute(node, AstSerializer.attrs.NOKEEP, "");
815818
}
816819

817820
if(!AstQuery.hasAttribute(node, AstSerializer.attrs.IS)) {
818-
node.attrs.push({ name: AstSerializer.attrs.IS, value: "template" });
821+
AstModify.addAttribute(node, AstSerializer.attrs.IS, "template");
819822
}
820823
}
821824
}
@@ -839,12 +842,62 @@ class AstSerializer {
839842
}
840843
}
841844

845+
846+
async runLoop(node, slots = {}, options = {}, streamEnabled = true) {
847+
let loopAttrValue = AstQuery.getAttributeValue(node, AstSerializer.attrs.LOOP);
848+
if(!loopAttrValue) {
849+
AstModify.removeAttribute(node, AstSerializer.attrs.LOOP);
850+
return { html: "" };
851+
}
852+
853+
let { keys, type, content } = Looping.parse(loopAttrValue);
854+
let loopContent = await this.evaluateAttribute(AstSerializer.attrs.LOOP, content, options);
855+
856+
AstModify.removeAttribute(node, AstSerializer.attrs.LOOP);
857+
858+
// if falsy, skip
859+
if(!loopContent) {
860+
return { html: "" };
861+
}
862+
863+
let promises = [];
864+
865+
if(type === "Object") {
866+
let index = 0;
867+
for(let loopKey in loopContent) {
868+
options.injectedData = {
869+
[keys.key]: loopKey,
870+
[keys.value]: loopContent[loopKey],
871+
[keys.index]: index++,
872+
};
873+
promises.push(this.compileNode(node, slots, options, streamEnabled));
874+
}
875+
} else if(type === "Array") {
876+
promises = loopContent.map(((loopValue, index) => {
877+
options.injectedData = {
878+
[keys.index]: index,
879+
[keys.value]: loopValue
880+
};
881+
882+
return this.compileNode(node, slots, options, streamEnabled);
883+
}));
884+
}
885+
886+
// Whitespace normalizer
887+
let html = (await Promise.all(promises)).map(entry => entry.html).filter(entry => entry).join("\n");
888+
889+
return { html };
890+
}
891+
842892
async compileNode(node, slots = {}, options = {}, streamEnabled = true) {
843893
options = Object.assign({}, options);
844894

845895
if(AstQuery.hasAnyAttribute(node, [ AstSerializer.attrs.IGNORE, AstSerializer.attrs.SETUP ])) {
846896
return { html: "" };
847897
}
898+
if(AstQuery.hasAttribute(node, AstSerializer.attrs.LOOP)) {
899+
return this.runLoop(node, slots, options, streamEnabled);
900+
}
848901

849902
this.addImpliedWebCAttributes(node);
850903

src/astModify.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Take extreme care when using these utilities, they mutate the Live AST
2+
3+
class AstModify {
4+
static addAttribute(node, name, value) {
5+
node.attrs.push({ name, value });
6+
}
7+
8+
static removeAttribute(node, name) {
9+
let index = node.attrs.findIndex(attr => attr.name === name);
10+
if(index !== -1) {
11+
node.attrs.splice(index, 1);
12+
}
13+
}
14+
}
15+
16+
export { AstModify };

src/looping.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
class Looping {
2+
static parseKey(content, type) {
3+
content = content.trim();
4+
// starting and ending parens are optional
5+
if(content.startsWith("(") && content.endsWith(")")) {
6+
content = content.slice(1, -1);
7+
}
8+
9+
let [first, second, third] = content.split(",").map(entry => entry.trim());
10+
11+
if(type === "Object") {
12+
return {
13+
key: first,
14+
value: second,
15+
index: third,
16+
}
17+
}
18+
19+
return {
20+
value: first,
21+
index: second
22+
};
23+
}
24+
25+
static parse(loopAttr) {
26+
let delimiters = [
27+
{
28+
value: " in ",
29+
type: "Object",
30+
wrap: (content) => {
31+
content = content.trim();
32+
if(content.startsWith("{") && content.endsWith("}")) {
33+
return `(${content})`;
34+
}
35+
return content;
36+
}
37+
},
38+
{
39+
value: " of ",
40+
type: "Array",
41+
}
42+
];
43+
44+
for(let delimiter of delimiters) {
45+
if(loopAttr.includes(delimiter.value)) {
46+
let [keysValue, content] = loopAttr.split(delimiter.value);
47+
return {
48+
keys: Looping.parseKey(keysValue, delimiter.type),
49+
type: delimiter.type,
50+
content: delimiter.wrap ? delimiter.wrap(content) : content,
51+
}
52+
}
53+
}
54+
55+
throw new Error(`Invalid ${AstSerializer.attrs.LOOP} attribute value: ${loopAttr}`);
56+
}
57+
}
58+
59+
export { Looping };

test/looping-test.js

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import test from "ava";
2+
import { WebC } from "../webc.js";
3+
4+
test("Basic webc:for (complex key) over Array", async t => {
5+
let component = new WebC();
6+
7+
component.setInputPath("./test/stubs/looping/array.webc");
8+
9+
let { html } = await component.compile();
10+
11+
t.is(html.trim(), `<div>2-1</div>
12+
<div>3-2</div>
13+
<div>4-3</div>`);
14+
});
15+
16+
test("Basic webc:for (simple key) over Array", async t => {
17+
let component = new WebC();
18+
19+
component.setInputPath("./test/stubs/looping/array-value.webc");
20+
21+
let { html } = await component.compile();
22+
23+
t.is(html.trim(), `<div>2-undefined</div>
24+
<div>3-undefined</div>
25+
<div>4-undefined</div>`);
26+
});
27+
28+
test("webc:for over Array has injected data available on child nodes", async t => {
29+
let component = new WebC();
30+
31+
component.setInputPath("./test/stubs/looping/scoped-data.webc");
32+
33+
let { html } = await component.compile();
34+
35+
t.is(html.trim(), `<div><div>1-0</div></div>
36+
<div><div>2-1</div></div>
37+
<div><div>3-2</div></div>
38+
<div><div>4-3</div></div>`);
39+
});
40+
41+
42+
test("Basic webc:for (complex key) over Object", async t => {
43+
let component = new WebC();
44+
45+
component.setInputPath("./test/stubs/looping/object.webc");
46+
47+
let { html } = await component.compile();
48+
49+
t.is(html.trim(), `<div>a-1-0</div>
50+
<div>c-4-2</div>`);
51+
});
52+
53+
test("Basic webc:for (simple key) over Object", async t => {
54+
let component = new WebC();
55+
56+
component.setInputPath("./test/stubs/looping/object-key.webc");
57+
58+
let { html } = await component.compile();
59+
60+
t.is(html.trim(), `<div>a-undefined</div>
61+
<div>c-undefined</div>`);
62+
});
63+
64+
test("webc:for using Object.keys to convert to Array", async t => {
65+
let component = new WebC();
66+
67+
component.setInputPath("./test/stubs/looping/array-object-keys.webc");
68+
69+
let { html } = await component.compile();
70+
71+
t.is(html.trim(), `<div>a</div>
72+
<div>c</div>`);
73+
});
74+
75+
test("webc:for using Object.values to convert to Array", async t => {
76+
let component = new WebC();
77+
78+
component.setInputPath("./test/stubs/looping/array-object-values.webc");
79+
80+
let { html } = await component.compile();
81+
82+
t.is(html.trim(), `<div>1</div>
83+
<div>4</div>`);
84+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="key of Object.keys({a:1, b:2, c:4})" webc:if="key !== 'b'" @text="`${key}`"></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="value of Object.values({a:1, b:2, c:4})" webc:if="value !== 2" @text="`${value}`"></div>

test/stubs/looping/array-value.webc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="value of [1,2,3,4]" webc:if="value >= 2" @text="`${value}-${index}`"></div>

test/stubs/looping/array.webc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="(value, index) of [1,2,3,4]" webc:if="value >= 2" @text="`${value}-${index}`"></div>

test/stubs/looping/object-key.webc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="key in {a:1, b:2, c:4}" webc:if="key !== 'b'" @text="`${key}-${value}`"></div>

test/stubs/looping/object.webc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="(key, value, index) in {a:1, b:2, c:4}" webc:if="key !== 'b'" @text="`${key}-${value}-${index}`"></div>

test/stubs/looping/scoped-data.webc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div webc:for="(value, index) of [1,2,3,4]"><div @text="`${value}-${index}`"></div></div>

0 commit comments

Comments
 (0)