Skip to content

Commit

Permalink
Adds TemplateOp to SSR template parser (#5706)
Browse files Browse the repository at this point in the history
* adding template element open and close ops

* emit template open and close codes from template parser

* fleshes out template op emission

Co-authored-by: nicholasrice <[email protected]>
  • Loading branch information
nicholasrice and nicholasrice committed May 3, 2022
1 parent 1afd294 commit c1f674d
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 42 deletions.
26 changes: 24 additions & 2 deletions packages/web-components/fast-ssr/src/template-parser/op-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export enum OpType {
customElementShadow,
attributeBinding,
directive,
templateElementOpen,
templateElementClose,
text,
}

Expand Down Expand Up @@ -74,6 +76,24 @@ export type AttributeBindingOp = {
useCustomElementInstance: boolean;
};

/**
* Operation to emit a template element open tag
*/
export type TemplateElementOpenOp = {
type: OpType.templateElementOpen;
staticAttributes: Map<string, string>;
// We need dynamic attributes here so we can emit the `<template`, all attributes, and then `>`
// from one operation
dynamicAttributes: Pick<AttributeBindingOp, "name" | "attributeType" | "directive">[];
};

/**
* Operation to emit a template element closing tag
*/
export type TemplateElementCloseOp = {
type: OpType.templateElementClose;
};

/**
* Operation to emit to custom-element attributes
*/
Expand All @@ -83,9 +103,11 @@ export type CustomElementAttributes = {

export type Op =
| AttributeBindingOp
| TextOp
| CustomElementOpenOp
| CustomElementCloseOp
| DirectiveOp
| CustomElementAttributes
| CustomElementShadowOp;
| CustomElementShadowOp
| TemplateElementOpenOp
| TemplateElementCloseOp
| TextOp;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "@lit-labs/ssr/lib/install-global-dom-shim.js";
import { test, expect } from "@playwright/test";
import { parseTemplateToOpCodes} from "./template-parser.js";
import { ViewTemplate, html, FASTElement, customElement, defaultExecutionContext } from "@microsoft/fast-element"
import { Op, OpType, CustomElementOpenOp, AttributeBindingOp, DirectiveOp } from "./op-codes.js";
import { Op, OpType, CustomElementOpenOp, AttributeBindingOp, DirectiveOp, TemplateElementOpenOp, TextOp } from "./op-codes.js";
import { AttributeType } from "./attributes.js";

@customElement("hello-world")
Expand Down Expand Up @@ -90,8 +90,66 @@ test.describe("parseTemplateToOpCodes", () => {

expect(codes.length).toBe(4);
expect(codes[0].attributeType).toBe(AttributeType.content);
expect(codes[0].name).toBe("string-value");
expect(codes[1].attributeType).toBe(AttributeType.booleanContent);
expect(codes[1].name).toBe("bool-value");
expect(codes[2].attributeType).toBe(AttributeType.idl);
expect(codes[2].name).toBe("property-value");
expect(codes[3].attributeType).toBe(AttributeType.event);
expect(codes[3].name).toBe("event");
});
})
test("should emit template open and close ops for a template element", () => {
const input = html`<template></template>`;
const codes = parseTemplateToOpCodes(input);

expect(codes.length).toBe(2);
expect(codes[0].type).toBe(OpType.templateElementOpen);
expect(codes[1].type).toBe(OpType.templateElementClose);
});
test("should emit template open ops with static attributes", () => {
const input = html`<template id="foo" boolean></template>`;
const open = parseTemplateToOpCodes(input)[0] as TemplateElementOpenOp;

expect(open.staticAttributes.get("id")).toBe("foo");
expect(open.staticAttributes.get("boolean")).toBe("");
});
test("should emit template open ops with dynamic attributes", () => {
const input = html`<template id=${x => "foo"} ?boolean=${x => true} @event=${x => undefined} :property=${x => "value"}></template>`;
const open = parseTemplateToOpCodes(input)[0] as TemplateElementOpenOp;

const attrs = new Map(open.dynamicAttributes.map(x => {
return [x.name, x];
}))

expect(attrs.has("id")).toBe(true);
expect(attrs.get("id")!.attributeType).toBe(AttributeType.content);
expect(attrs.has("boolean")).toBe(true);
expect(attrs.get("boolean")!.attributeType).toBe(AttributeType.booleanContent);
expect(attrs.has("event")).toBe(true);
expect(attrs.get("event")!.attributeType).toBe(AttributeType.event);
expect(attrs.has("property")).toBe(true);
expect(attrs.get("property")!.attributeType).toBe(AttributeType.idl);
});
test("should emit template open ops with static and dynamic attributes", () => {
const input = html`<template id="foo" ?boolean=${x => true}></template>`;
const open = parseTemplateToOpCodes(input)[0] as TemplateElementOpenOp;

expect(open.staticAttributes.size).toBe(1);
expect(open.staticAttributes.get("id")).toBe("foo");
expect(open.dynamicAttributes.length).toBe(1);
expect(open.dynamicAttributes[0].name).toBe("boolean");
});

test("should emit template template ops between other ops when nested inside of another element", () => {
const input = html`<div><template></template></div>`;
const codes = parseTemplateToOpCodes(input);

expect(codes[0].type).toBe(OpType.text);
expect((codes[0] as TextOp).value).toBe(`<div>`);
expect(codes[1].type).toBe(OpType.templateElementOpen);
expect(codes[2].type).toBe(OpType.templateElementClose);
expect(codes[3].type).toBe(OpType.text);
expect((codes[3] as TextOp).value).toBe(`</div>`);
})
});
// TODO add test that name for property, bool, and event attrs has the prefix removed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
parseFragment,
} from "parse5";
import { AttributeType, attributeTypeRegExp } from "./attributes.js";
import { Op, OpType } from "./op-codes.js";
import { AttributeBindingOp, Op, OpType } from "./op-codes.js";

/**
* Cache the results of template parsing.
Expand Down Expand Up @@ -154,66 +154,74 @@ export function parseTemplateToOpCodes(template: ViewTemplate): Op[] {
// as well as any element with attribute bindings
let augmentOpeningTag = false;
const { tagName } = node;
let ctor: typeof HTMLElement | undefined;
const ctor: typeof HTMLElement | undefined = customElements.get(node.tagName);

// Sort attributes by whether they're related to a binding or if they have
// static value
const attributes: {
static: Map<string, string>;
dynamic: Attribute[];
dynamic: Map<Attribute, AttributeBindingOp>;
} = node.attrs.reduce(
(prev, current) => {
if (Parser.parse(current.value, directives)) {
prev.dynamic.push(current);
const parsed = Parser.parse(current.value, directives);
const attributeType = getAttributeType(current);
if (parsed) {
prev.dynamic.set(current, {
type: OpType.attributeBinding,
name:
attributeType === AttributeType.content
? current.name
: current.name.substring(1),
directive: Parser.aggregate(parsed),
attributeType,
useCustomElementInstance: Boolean(node.isDefinedCustomElement),
});
} else {
prev.static.set(current.name, current.value);
}

return prev;
},
{ static: new Map(), dynamic: [] as Attribute[] }
{
static: new Map<string, string>(),
dynamic: new Map<Attribute, AttributeBindingOp>(),
}
);

// Special processing for any custom element
if (node.tagName.includes("-")) {
ctor = customElements.get(tagName);

if (ctor !== undefined) {
augmentOpeningTag = true;
node.isDefinedCustomElement = true;
opCodes.push({
type: OpType.customElementOpen,
tagName,
ctor,
staticAttributes: attributes.static,
});
}
if (ctor !== undefined) {
augmentOpeningTag = true;
node.isDefinedCustomElement = true;
opCodes.push({
type: OpType.customElementOpen,
tagName,
ctor,
staticAttributes: attributes.static,
});
} else if (node.tagName === "template") {
flushTo(node.sourceCodeLocation?.startTag.startOffset);
opCodes.push({
type: OpType.templateElementOpen,
staticAttributes: attributes.static,
dynamicAttributes: Array.from(attributes.dynamic.values()),
});
skipTo(node.sourceCodeLocation!.startTag.endOffset);
return;
}

// Push attribute binding op codes for any attributes that
// are dynamic
if (attributes.dynamic.length) {
for (const attr of attributes.dynamic) {
if (attributes.dynamic.size) {
for (const [attr, code] of attributes.dynamic) {
const location = node.sourceCodeLocation!.attrs[attr.name];
flushTo(location.startOffset);
const attributeType = getAttributeType(attr);
const parsed = Parser.parse(attr.value, directives);

if (parsed !== null) {
augmentOpeningTag = true;
opCodes.push({
type: OpType.attributeBinding,
name: attr.name,
directive: Parser.aggregate(parsed),
attributeType,
useCustomElementInstance: Boolean(node.isDefinedCustomElement),
});
skipTo(location.endOffset);
}
augmentOpeningTag = true;
opCodes.push(code);
skipTo(location.endOffset);
}
}

if (augmentOpeningTag) {
if (augmentOpeningTag && node.tagName !== "template") {
if (ctor) {
flushTo(node.sourceCodeLocation!.startTag.endOffset - 1);
opCodes.push({ type: OpType.customElementAttributes });
Expand Down Expand Up @@ -298,8 +306,14 @@ export function parseTemplateToOpCodes(template: ViewTemplate): Op[] {
},

leave(node: DefaultTreeNode): void {
if (isElementNode(node) && node.isDefinedCustomElement) {
opCodes.push({ type: OpType.customElementClose });
if (isElementNode(node)) {
if (node.isDefinedCustomElement) {
opCodes.push({ type: OpType.customElementClose });
} else if (node.tagName === "template") {
flushTo(node.sourceCodeLocation?.endTag.startOffset);
opCodes.push({ type: OpType.templateElementClose });
skipTo(node.sourceCodeLocation!.endTag.endOffset);
}
}
},
});
Expand Down

0 comments on commit c1f674d

Please sign in to comment.