Skip to content

Commit

Permalink
Adds TemplateParser to SSR package (#5645)
Browse files Browse the repository at this point in the history
* adding template-renderer feature and spec files

* adding DOM emission configuration to TemplateRenderer

* adding render function to TemplateRenderer

* adding method description to TemplateRenderer.render

* add template parser files and entry functions

* parseTemplateToOpCodes should throw when used with an HTMLTemplateElement template

* change script structure to allow breakpoints to percist in files from build to build

* adding parse5 HTML parser

* generate AST from ViewTemplate

* implement AST traverser

* adding node type checks

* implement parser class that acts as a node visitor for node traversal

* adding flushTo method to TemplateParser

* implement completion method

* writing a few test fixtures for pure HTML templates

* add directive type and parsing test

* move template-parser files to own directory

* adding tests for directive ops

* fix-up after rebase

* emit op-codes for custom elements

* adding attribute binding ops

* formatting

* adding tests for custom element attribute bindings

* organize imports

* fix processing of interpolated bindings and add test

* Change files

* Update packages/web-components/fast-ssr/src/template-parser/template-parser.ts

Co-authored-by: Jane Chu <[email protected]>

* rename ast variable

* remove dependency on Markup.marker

Co-authored-by: nicholasrice <[email protected]>
Co-authored-by: Jane Chu <[email protected]>
  • Loading branch information
3 people committed May 5, 2022
1 parent 50d1baa commit b191c41
Show file tree
Hide file tree
Showing 9 changed files with 583 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "automated update of api-report.md",
"packageName": "@microsoft/fast-element",
"email": "[email protected]",
"dependentChangeType": "none"
}
9 changes: 4 additions & 5 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class AttributeDefinition implements Accessor {
onAttributeChangedCallback(element: HTMLElement, value: any): void;
readonly Owner: Function;
setValue(source: HTMLElement, newValue: any): void;
}
}

// @public
export type AttributeMode = "reflect" | "boolean" | "fromView";
Expand Down Expand Up @@ -467,14 +467,14 @@ export class RepeatBehavior<TSource = any> implements Behavior, Subscriber {
// @internal (undocumented)
handleChange(source: any, args: Splice[]): void;
unbind(): void;
}
}

// @public
export class RepeatDirective<TSource = any> extends HTMLDirective {
constructor(itemsBinding: Binding, templateBinding: Binding<TSource, SyntheticViewTemplate>, options: RepeatOptions);
createBehavior(targets: ViewBehaviorTargets): RepeatBehavior<TSource>;
createPlaceholder: (index: number) => string;
}
}

// @public
export interface RepeatOptions {
Expand Down Expand Up @@ -617,15 +617,14 @@ export class ViewTemplate<TSource = any, TParent = any, TGrandparent = any> impl
readonly directives: ReadonlyArray<HTMLDirective>;
readonly html: string | HTMLTemplateElement;
render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView<TSource, TParent, TGrandparent>;
}
}

// @public
export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor;

// @public
export function when<TSource = any, TReturn = any>(binding: Binding<TSource, TReturn>, templateOrTemplateBinding: SyntheticViewTemplate | Binding<TSource, SyntheticViewTemplate>): CaptureType<TSource>;


// (No @packageDocumentation comment for this package)

```
5 changes: 4 additions & 1 deletion packages/web-components/fast-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"url": "https://github.com/Microsoft/fast/issues/new/choose"
},
"scripts": {
"build": "tsc -b --clean src && tsc -b src",
"clean": "tsc -b --clean src",
"build": "tsc -b src",
"prepare": "yarn run clean && yarn run build",
"build-server": "tsc -b server",
"eslint": "eslint . --ext .ts",
"eslint:fix": "eslint . --ext .ts --fix",
Expand All @@ -34,6 +36,7 @@
"dependencies": {
"@lit-labs/ssr": "^1.0.0-rc.2",
"@microsoft/fast-element": "^1.5.0",
"parse5": "^6.0.1",
"tslib": "^1.11.1"
},
"devDependencies": {
Expand Down
14 changes: 14 additions & 0 deletions packages/web-components/fast-ssr/src/template-parser/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Extracts the attribute type from an attribute name
*/
export const attributeTypeRegExp = /([:?@])?(.*)/;

/**
* The types of attributes applied in a template
*/
export enum AttributeType {
content,
booleanContent,
idl,
event,
}
91 changes: 91 additions & 0 deletions packages/web-components/fast-ssr/src/template-parser/op-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { InlinableHTMLDirective } from "@microsoft/fast-element";
import { AttributeType } from "./attributes.js";

/**
* Allows fast identification of operation types
*/
export enum OpType {
customElementOpen,
customElementClose,
customElementAttributes,
customElementShadow,
attributeBinding,
directive,
text,
}

/**
* Operation to emit static text
*/
export type TextOp = {
type: OpType.text;
value: string;
};

/**
* Operation to open a custom element
*/
export type CustomElementOpenOp = {
type: OpType.customElementOpen;
/**
* The tagname of the custom element
*/
tagName: string;

/**
* The constructor of the custom element
*/
ctor: typeof HTMLElement;

/**
* Attributes of the custom element, non-inclusive of any attributes
* that are the product of bindings
*/
staticAttributes: Map<string, string>;
};

/**
* Operation to close a custom element
*/
export type CustomElementCloseOp = {
type: OpType.customElementClose;
};

export type CustomElementShadowOp = {
type: OpType.customElementShadow;
};

/**
* Operation to emit static text
*/
export type DirectiveOp = {
type: OpType.directive;
directive: InlinableHTMLDirective;
};

/**
* Operation to emit a bound attribute
*/
export type AttributeBindingOp = {
type: OpType.attributeBinding;
directive: InlinableHTMLDirective;
name: string;
attributeType: AttributeType;
useCustomElementInstance: boolean;
};

/**
* Operation to emit to custom-element attributes
*/
export type CustomElementAttributes = {
type: OpType.customElementAttributes;
};

export type Op =
| AttributeBindingOp
| TextOp
| CustomElementOpenOp
| CustomElementCloseOp
| DirectiveOp
| CustomElementAttributes
| CustomElementShadowOp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@

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 { AttributeType } from "./attributes.js";

@customElement("hello-world")
class HelloWorld extends FASTElement {}

test.describe("parseTemplateToOpCodes", () => {
test("should throw when invoked with a ViewTemplate with a HTMLTemplateElement template", () => {
expect(() => {
parseTemplateToOpCodes(new ViewTemplate(document.createElement("template"), []));
}).toThrow();
});
test("should not throw when invoked with a ViewTemplate with a string template", () => {
expect(() => {
parseTemplateToOpCodes(new ViewTemplate("", []));
}).not.toThrow();
});

test("should emit a single text op for a template with no bindings or directives", () => {
expect(parseTemplateToOpCodes(html`<p>Hello world</p>`)).toEqual([{type: OpType.text, value: "<p>Hello world</p>"}])
});
test("should emit doctype, html, head, and body elements as part of text op", () => {
expect(parseTemplateToOpCodes(html`<!DOCTYPE html><html><head></head><body></body></html>`)).toEqual([{type: OpType.text, value: "<!DOCTYPE html><html><head></head><body></body></html>"}])
})
test("should emit a directive op from a binding", () => {
const input = html`${() => "hello world"}`;
expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.directive, directive: input.directives[0]}])
});
test("should emit a directive op from text and a binding ", () => {
const input = html`Hello ${() => "World"}.`;

const codes = parseTemplateToOpCodes(input);
const code = codes[0] as DirectiveOp;
expect(codes.length).toBe(1);
expect(code.type).toBe(OpType.directive);
expect(code.directive.binding(null, defaultExecutionContext)).toBe("Hello World.")
});
test("should sandwich directive ops between text ops when binding native element content", () => {

const input = html`<p>${() => "hello world"}</p>`;
expect(parseTemplateToOpCodes(input)).toEqual([
{ type: OpType.text, value: "<p>"},
{ type: OpType.directive, directive: input.directives[0]},
{ type: OpType.text, value: "</p>"},
])
});
test("should emit a custom element as text if it has not been defined", () => {
const input = html`<undefined-element test-attribute="test" test-bool></undefined-element>`;
expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.text, value: "<undefined-element test-attribute=\"test\" test-bool></undefined-element>"}])
})

test("should emit custom element open, close, attribute, and shadow ops for a defined custom element", () => {
const input = html`<hello-world></hello-world>`;
expect(parseTemplateToOpCodes(input)).toEqual([
{type: OpType.customElementOpen, ctor: HelloWorld, tagName: "hello-world", staticAttributes: new Map()},
{type: OpType.text, value: "<hello-world"},
{type: OpType.customElementAttributes},
{type: OpType.text, value: ">"},
{type: OpType.customElementShadow},
{type: OpType.customElementClose},
{type: OpType.text, value: "</hello-world>"}
])
});
test("should emit static attributes of a custom element custom element open, close, attribute, and shadow ops for a defined custom element", () => {
const input = html`<hello-world string-value="test" bool-value></hello-world>`;
const code = parseTemplateToOpCodes(input).find((op) => op.type ===OpType.customElementOpen) as CustomElementOpenOp | undefined ;
expect(code).not.toBeUndefined();
expect(code?.staticAttributes.get("string-value")).toBe("test");
expect(code?.staticAttributes.get("bool-value")).toBe("");
expect(code?.staticAttributes.size).toBe(2);
});
test("should emit attributes binding ops for a native element with attribute bindings", () => {
const input = html`<p string-value="${x => "value"}" ?bool-value="${x => false}" :property-value="${x => "value"}" @event="${x => {}}"></p>`;
const codes = parseTemplateToOpCodes(input).filter(x => x.type === OpType.attributeBinding) as AttributeBindingOp[];

expect(codes.length).toBe(4);
expect(codes[0].attributeType).toBe(AttributeType.content);
expect(codes[1].attributeType).toBe(AttributeType.booleanContent);
expect(codes[2].attributeType).toBe(AttributeType.idl);
expect(codes[3].attributeType).toBe(AttributeType.event);
});
test("should emit attributes binding ops for a custom element with attribute bindings", () => {
const input = html`<hello-world string-value="${x => "value"}" ?bool-value="${x => false}" :property-value="${x => "value"}" @event="${x => {}}"></hello-world>`;
const codes = parseTemplateToOpCodes(input).filter(x => x.type === OpType.attributeBinding) as AttributeBindingOp[];

expect(codes.length).toBe(4);
expect(codes[0].attributeType).toBe(AttributeType.content);
expect(codes[1].attributeType).toBe(AttributeType.booleanContent);
expect(codes[2].attributeType).toBe(AttributeType.idl);
expect(codes[3].attributeType).toBe(AttributeType.event);
});
})
Loading

0 comments on commit b191c41

Please sign in to comment.