Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds TemplateParser to SSR package #5645

Merged
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1a16a55
adding template-renderer feature and spec files
nicholasrice Feb 17, 2022
4473f68
adding DOM emission configuration to TemplateRenderer
nicholasrice Feb 17, 2022
5acb026
adding render function to TemplateRenderer
nicholasrice Feb 17, 2022
ee977b6
adding method description to TemplateRenderer.render
nicholasrice Feb 17, 2022
06500fd
add template parser files and entry functions
nicholasrice Feb 17, 2022
dc9a30e
parseTemplateToOpCodes should throw when used with an HTMLTemplateEle…
nicholasrice Feb 17, 2022
deadcb6
change script structure to allow breakpoints to percist in files from…
nicholasrice Feb 17, 2022
23acadf
adding parse5 HTML parser
nicholasrice Feb 17, 2022
a88624a
generate AST from ViewTemplate
nicholasrice Feb 17, 2022
03d9c19
implement AST traverser
nicholasrice Feb 18, 2022
144acec
adding node type checks
nicholasrice Feb 18, 2022
a1c36ab
implement parser class that acts as a node visitor for node traversal
nicholasrice Feb 22, 2022
347fa09
adding flushTo method to TemplateParser
nicholasrice Feb 22, 2022
6ed2d38
implement completion method
nicholasrice Feb 22, 2022
496e13e
writing a few test fixtures for pure HTML templates
nicholasrice Feb 22, 2022
22180fb
add directive type and parsing test
nicholasrice Feb 23, 2022
588f1c7
move template-parser files to own directory
nicholasrice Feb 23, 2022
661eb05
adding tests for directive ops
nicholasrice Feb 23, 2022
39d01f0
fix-up after rebase
nicholasrice Feb 24, 2022
b37032a
emit op-codes for custom elements
nicholasrice Feb 24, 2022
f137af3
adding attribute binding ops
nicholasrice Feb 24, 2022
5a3b6b5
formatting
nicholasrice Feb 24, 2022
7bde799
adding tests for custom element attribute bindings
nicholasrice Feb 24, 2022
41cd0a0
organize imports
nicholasrice Feb 24, 2022
504bbf3
fix processing of interpolated bindings and add test
nicholasrice Feb 25, 2022
14dd90f
Change files
nicholasrice Feb 25, 2022
cdc612f
Update packages/web-components/fast-ssr/src/template-parser/template-…
nicholasrice Feb 25, 2022
f6f4418
rename ast variable
nicholasrice Feb 25, 2022
74e5040
remove dependency on Markup.marker
nicholasrice Mar 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = /([:?@])?(.*)/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the html tagged template function does this internally and associates it with the directive, do we also need it here? Is there some way the SSR parser could use what is already available?

Copy link
Contributor Author

@nicholasrice nicholasrice Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this processing when we're rendering the element, we need to know which API to call setProperty, setAttribute or set up event delegation. This regex essentially captures that information. Is that something we can infer from the directive?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make that information available. The binding will know that. It probably just needs to be officially exposed.


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

const { marker } = Markup;
const blockMarker = new RegExp(`${marker}:\\d+`);
export function isMarkerComment(node: DefaultTreeCommentNode): boolean {
return blockMarker.test(node.data);
}

const interpolationMarker = new RegExp(`${marker}\\{(?<id>\\d+)\\}${marker}`);
export function isInterpolationMarker(node: { value: string }): boolean {
return interpolationMarker.test(node.value);
}

export function extractInterpolationMarkerId(node: { value: string }): number | null {
const id = interpolationMarker.exec(node.value)?.groups?.id;

return id === undefined ? null : parseInt(id, 10);
}
Copy link
Contributor

@EisenbergEffect EisenbergEffect Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the above functions, is there any way we could leverage what's in fast-element? If there were additional functions that took strings or something like that. Just trying to see if we can remove the marker dependency and not duplicate some of the pattern/parsing bits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah actually, I think I might just be able to use the parser for all of this - let me try that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool that all worked, I was able to remove this file

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