-
Notifications
You must be signed in to change notification settings - Fork 600
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds TemplateParser to SSR package (#5645)
* 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
1 parent
50d1baa
commit b191c41
Showing
9 changed files
with
583 additions
and
6 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/@microsoft-fast-element-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
packages/web-components/fast-ssr/src/template-parser/attributes.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
91
packages/web-components/fast-ssr/src/template-parser/op-codes.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
97 changes: 97 additions & 0 deletions
97
packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}) |
Oops, something went wrong.