-
Notifications
You must be signed in to change notification settings - Fork 600
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
Changes from 26 commits
1a16a55
4473f68
5acb026
ee977b6
06500fd
dc9a30e
deadcb6
23acadf
a88624a
03d9c19
144acec
a1c36ab
347fa09
6ed2d38
496e13e
22180fb
588f1c7
661eb05
39d01f0
b37032a
f137af3
5a3b6b5
7bde799
41cd0a0
504bbf3
14dd90f
cdc612f
f6f4418
74e5040
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} |
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, | ||
} |
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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool that all worked, I was able to remove this file |
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); | ||
}); | ||
}) |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.