diff --git a/packages/parse5/lib/common/html.ts b/packages/parse5/lib/common/html.ts
index d9d341243..f4a10905f 100644
--- a/packages/parse5/lib/common/html.ts
+++ b/packages/parse5/lib/common/html.ts
@@ -17,6 +17,11 @@ export enum ATTRS {
COLOR = 'color',
FACE = 'face',
SIZE = 'size',
+ SHADOWROOTMODE = 'shadowrootmode',
+ SHADOWROOTCLONABLE = 'shadowrootclonable',
+ SHADOWROOTSERIALIZABLE = 'shadowrootserializable',
+ SHADOWROOTDELEGATESFOCUS = 'shadowrootdelegatesfocus',
+ SHADOWROOTCUSTOMELEMENTREGISTRY = 'shadowrootcustomelementregistry',
}
/**
diff --git a/packages/parse5/lib/common/token.ts b/packages/parse5/lib/common/token.ts
index 4b1a1283d..637d2ad8c 100644
--- a/packages/parse5/lib/common/token.ts
+++ b/packages/parse5/lib/common/token.ts
@@ -87,6 +87,10 @@ export function getTokenAttr(token: TagToken, attrName: string): string | null {
return null;
}
+export function hasTokenAttr(token: TagToken, attrName: string): boolean {
+ return getTokenAttr(token, attrName) !== null;
+}
+
export interface CommentToken extends TokenBase {
readonly type: TokenType.COMMENT;
data: string;
diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts
index 9779f68f3..4c1f1a489 100644
--- a/packages/parse5/lib/parser/index.ts
+++ b/packages/parse5/lib/parser/index.ts
@@ -28,6 +28,7 @@ import {
type EOFToken,
type LocationWithAttributes,
type ElementLocation,
+ hasTokenAttr,
} from '../common/token.js';
//Misc constants
@@ -96,6 +97,13 @@ export interface ParserOptions {
*/
sourceCodeLocationInfo?: boolean;
+ /**
+ * Whether declarative shadow roots should be parsed or treated as ordinary templates.
+ *
+ * @default `false`
+ */
+ allowDeclarativeShadowRoots?: boolean;
+
/**
* Specifies the resulting tree format.
*
@@ -114,6 +122,7 @@ export interface ParserOptions {
const defaultParserOptions: Required> = {
scriptingEnabled: true,
sourceCodeLocationInfo: false,
+ allowDeclarativeShadowRoots: false,
treeAdapter: defaultTreeAdapter,
onParseError: null,
};
@@ -143,6 +152,10 @@ export class Parser implements TokenHandler, Stack
this.treeAdapter = this.options.treeAdapter;
this.onParseError = this.options.onParseError;
+ if (this.options.allowDeclarativeShadowRoots && !this.treeAdapter.declarativeShadowRootAdapter) {
+ throw new TypeError(`the given tree adapter does not have declarative shadow dom support`);
+ }
+
// Always enable location info if we report parse errors.
if (this.onParseError) {
this.options.sourceCodeLocationInfo = true;
@@ -453,14 +466,17 @@ export class Parser implements TokenHandler, Stack
}
/** @protected */
- _insertTemplate(token: TagToken): void {
+ _insertTemplate(token: TagToken, onlyAddElementToStack: boolean = false): T['element'] {
const tmpl = this.treeAdapter.createElement(token.tagName, NS.HTML, token.attrs);
const content = this.treeAdapter.createDocumentFragment();
this.treeAdapter.setTemplateContent(tmpl, content);
- this._attachElementToTree(tmpl, token.location);
+ if (!onlyAddElementToStack) {
+ this._attachElementToTree(tmpl, token.location);
+ }
this.openElements.push(tmpl, token.tagID);
if (this.options.sourceCodeLocationInfo) this.treeAdapter.setNodeSourceCodeLocation(content, null);
+ return tmpl;
}
/** @protected */
@@ -1723,11 +1739,48 @@ function startTagInHead(p: Parser, token: TagTo
break;
}
case $.TEMPLATE: {
- p._insertTemplate(token);
p.activeFormattingElements.insertMarker();
p.framesetOk = false;
p.insertionMode = InsertionMode.IN_TEMPLATE;
p.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
+
+ const mode = getTokenAttr(token, ATTRS.SHADOWROOTMODE);
+ if (
+ !p.options.allowDeclarativeShadowRoots ||
+ (mode !== 'open' && mode !== 'closed') ||
+ p._getAdjustedCurrentElement() !== p.openElements.current
+ ) {
+ p._insertTemplate(token);
+ } else {
+ const declarativeShadowHostElement = p._getAdjustedCurrentElement();
+ const shadowRoot =
+ p.treeAdapter.declarativeShadowRootAdapter!.getShadowRoot(declarativeShadowHostElement);
+ if (shadowRoot) {
+ p._insertTemplate(token);
+ } else {
+ const template = p._insertTemplate(token, true);
+ const clonable = hasTokenAttr(token, ATTRS.SHADOWROOTCLONABLE);
+ const serializable = hasTokenAttr(token, ATTRS.SHADOWROOTSERIALIZABLE);
+ const delegatesFocus = hasTokenAttr(token, ATTRS.SHADOWROOTDELEGATESFOCUS);
+ const customElementRegistry = getTokenAttr(token, ATTRS.SHADOWROOTCUSTOMELEMENTREGISTRY);
+ const shadowRoot = p.treeAdapter.declarativeShadowRootAdapter!.attachDeclarativeShadowRoot(
+ declarativeShadowHostElement,
+ {
+ mode,
+ clonable,
+ serializable,
+ delegatesFocus,
+ customElementRegistry,
+ declarativeTemplateAttributes: token.attrs,
+ },
+ );
+ p.treeAdapter.declarativeShadowRootAdapter!.setTemplateContentForDeclarativeShadowRootParsing(
+ template,
+ shadowRoot,
+ );
+ }
+ }
+
break;
}
case $.HEAD: {
diff --git a/packages/parse5/lib/serializer/index.ts b/packages/parse5/lib/serializer/index.ts
index ef7326abb..9dc5ff1be 100644
--- a/packages/parse5/lib/serializer/index.ts
+++ b/packages/parse5/lib/serializer/index.ts
@@ -1,7 +1,8 @@
-import { TAG_NAMES as $, NS, hasUnescapedText } from '../common/html.js';
+import { TAG_NAMES as $, ATTRS, NS, hasUnescapedText } from '../common/html.js';
import { escapeText, escapeAttribute } from 'entities/escape';
import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface.js';
import { defaultTreeAdapter, type DefaultTreeAdapterMap } from '../tree-adapters/default.js';
+import type { Attribute } from '../common/token.js';
// Sets
const VOID_ELEMENTS = new Set([
@@ -33,6 +34,21 @@ function isVoidElement(node: T['node'], options: I
);
}
+const KNOWN_DECLARATIVE_SHADOW_ROOT_ATTRIBUTES = new Set([
+ ATTRS.SHADOWROOTCLONABLE,
+ ATTRS.SHADOWROOTCUSTOMELEMENTREGISTRY,
+ ATTRS.SHADOWROOTDELEGATESFOCUS,
+ ATTRS.SHADOWROOTMODE,
+ ATTRS.SHADOWROOTSERIALIZABLE,
+]);
+
+function isKnownDeclarativeShadowRootAttribute(attr: Attribute): boolean {
+ if (attr.namespace !== undefined) {
+ return false;
+ }
+ return KNOWN_DECLARATIVE_SHADOW_ROOT_ATTRIBUTES.has(attr.name);
+}
+
export interface SerializerOptions {
/**
* Specifies input tree format.
@@ -47,11 +63,40 @@ export interface SerializerOptions {
* @default `true`
*/
scriptingEnabled?: boolean;
+ /**
+ * Whether to serialize shadow roots.
+ *
+ * @default `false`
+ */
+ serializableShadowRoots?: boolean;
+
+ /**
+ * Shadow roots to always serialize.
+ *
+ * @default `[]`
+ */
+ shadowRoots?: Array;
}
type InternalOptions = Required>;
-const defaultOpts: InternalOptions = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true };
+const defaultOpts: InternalOptions = {
+ treeAdapter: defaultTreeAdapter,
+ scriptingEnabled: true,
+ serializableShadowRoots: false,
+ shadowRoots: [],
+};
+
+function validateDeclarativeShadowRootSupport(
+ opts: InternalOptions,
+): void {
+ if (
+ (opts.serializableShadowRoots || opts.shadowRoots.length > 0) &&
+ !opts.treeAdapter.declarativeShadowRootAdapter
+ ) {
+ throw new TypeError(`the given tree adapter does not support serializing shadow roots`);
+ }
+}
/**
* Serializes an AST node to an HTML string.
@@ -81,6 +126,8 @@ export function serialize(
): string {
const opts = { ...defaultOpts, ...options } as InternalOptions;
+ validateDeclarativeShadowRootSupport(opts);
+
if (isVoidElement(node, opts)) {
return '';
}
@@ -112,6 +159,7 @@ export function serializeOuter,
): string {
const opts = { ...defaultOpts, ...options } as InternalOptions;
+ validateDeclarativeShadowRootSupport(opts);
return serializeNode(node, opts);
}
@@ -158,17 +206,50 @@ function serializeNode(node: T['node'], options: I
function serializeElement(node: T['element'], options: InternalOptions): string {
const tn = options.treeAdapter.getTagName(node);
- return `<${tn}${serializeAttributes(node, options)}>${
- isVoidElement(node, options) ? '' : `${serializeChildNodes(node, options)}${tn}>`
- }`;
+ let html = `<${tn}${serializeAttributes(options.treeAdapter.getAttrList(node))}>`;
+ if (isVoidElement(node, options)) {
+ return html;
+ }
+ const shadowRoot = options.treeAdapter.declarativeShadowRootAdapter!.getShadowRoot(node);
+ if (shadowRoot !== null) {
+ const shadowRootInit = options.treeAdapter.declarativeShadowRootAdapter!.getShadowRootInit(shadowRoot);
+
+ if (
+ (options.serializableShadowRoots && shadowRootInit.serializable) ||
+ options.shadowRoots.includes(shadowRoot)
+ ) {
+ const declarativeTemplateAttrs: Attribute[] = [];
+ declarativeTemplateAttrs.push({ name: ATTRS.SHADOWROOTMODE, value: shadowRootInit.mode });
+ if (shadowRootInit.delegatesFocus) {
+ declarativeTemplateAttrs.push({ name: ATTRS.SHADOWROOTMODE, value: '' });
+ }
+ if (shadowRootInit.serializable) {
+ declarativeTemplateAttrs.push({ name: ATTRS.SHADOWROOTSERIALIZABLE, value: '' });
+ }
+ if (shadowRootInit.clonable) {
+ declarativeTemplateAttrs.push({ name: ATTRS.SHADOWROOTCLONABLE, value: '' });
+ }
+ if (shadowRootInit.customElementRegistry !== null) {
+ declarativeTemplateAttrs.push({ name: ATTRS.SHADOWROOTCUSTOMELEMENTREGISTRY, value: '' });
+ }
+
+ declarativeTemplateAttrs.push(
+ ...shadowRootInit.declarativeTemplateAttributes.filter(
+ (attr) => !isKnownDeclarativeShadowRootAttribute(attr),
+ ),
+ );
+
+ html += `${serializeChildNodes(options.treeAdapter.getChildNodes(shadowRoot), options)}`;
+ }
+ }
+
+ html += `${tn}>`;
+ return html;
}
-function serializeAttributes(
- node: T['element'],
- { treeAdapter }: InternalOptions,
-): string {
+function serializeAttributes(attrs: Attribute[]): string {
let html = '';
- for (const attr of treeAdapter.getAttrList(node)) {
+ for (const attr of attrs) {
html += ' ';
if (attr.namespace) {
diff --git a/packages/parse5/lib/tree-adapters/default.ts b/packages/parse5/lib/tree-adapters/default.ts
index 415a9d79c..ac36c10c9 100644
--- a/packages/parse5/lib/tree-adapters/default.ts
+++ b/packages/parse5/lib/tree-adapters/default.ts
@@ -1,6 +1,6 @@
import { DOCUMENT_MODE, type NS } from '../common/html.js';
import type { Attribute, Location, ElementLocation } from '../common/token.js';
-import type { TreeAdapter, TreeAdapterTypeMap } from './interface.js';
+import type { ShadowRootInit, TreeAdapter, TreeAdapterTypeMap } from './interface.js';
export interface Document {
/** The name of the node. */
@@ -18,13 +18,30 @@ export interface Document {
export interface DocumentFragment {
/** The name of the node. */
- nodeName: '#document-fragment';
+ nodeName: '#document-fragment' | '#shadow-root';
/** The node's children. */
childNodes: ChildNode[];
/** Comment source code location info. Available if location info is enabled. */
sourceCodeLocation?: Location | null;
}
+export interface ShadowRoot extends DocumentFragment {
+ /** The name of the node. */
+ nodeName: '#shadow-root';
+ /** The shadow root mode. */
+ mode: 'open' | 'closed';
+ /** Whether the shadow root delegates focus. */
+ delegatesFocus: boolean;
+ /** Whether the shadow root is clonable or not. */
+ clonable: boolean;
+ /** Whether the shadow root is clonable or not. */
+ serializable: boolean;
+ /** The custom element registry this node belongs to. */
+ customElementRegistry: string | null;
+ /** Attributes that appear on the element when serialized as a declarative shadow root. */
+ declarativeTemplateAttributes: Attribute[];
+}
+
export interface Element {
/** Element tag name. Same as {@link tagName}. */
nodeName: string;
@@ -38,6 +55,8 @@ export interface Element {
sourceCodeLocation?: ElementLocation | null;
/** Parent node. */
parentNode: ParentNode | null;
+ /** Shadow root if present. */
+ shadowRoot: ShadowRoot | null;
/** The node's children. */
childNodes: ChildNode[];
}
@@ -85,7 +104,7 @@ export interface DocumentType {
sourceCodeLocation?: Location | null;
}
-export type ParentNode = Document | DocumentFragment | Element | Template;
+export type ParentNode = Document | DocumentFragment | ShadowRoot | Element | Template;
export type ChildNode = Element | Template | CommentNode | TextNode | DocumentType;
export type Node = ParentNode | ChildNode;
@@ -127,6 +146,7 @@ export const defaultTreeAdapter: TreeAdapter = {
namespaceURI,
childNodes: [],
parentNode: null,
+ shadowRoot: null,
};
},
@@ -311,4 +331,44 @@ export const defaultTreeAdapter: TreeAdapter = {
updateNodeSourceCodeLocation(node: Node, endLocation: ElementLocation): void {
node.sourceCodeLocation = { ...node.sourceCodeLocation, ...endLocation };
},
+
+ declarativeShadowRootAdapter: {
+ // Shadow roots
+ attachDeclarativeShadowRoot(
+ element: Element,
+ {
+ mode,
+ clonable,
+ serializable,
+ delegatesFocus,
+ customElementRegistry,
+ declarativeTemplateAttributes,
+ }: ShadowRootInit,
+ ): ShadowRoot {
+ const shadowRoot: ShadowRoot = {
+ nodeName: '#shadow-root',
+ childNodes: [],
+ mode,
+ clonable,
+ serializable,
+ delegatesFocus,
+ customElementRegistry,
+ declarativeTemplateAttributes,
+ };
+ element.shadowRoot = shadowRoot;
+ return shadowRoot;
+ },
+
+ getShadowRoot(element: Element): ShadowRoot | null {
+ return element.shadowRoot;
+ },
+
+ getShadowRootInit(shadowRoot: ShadowRoot): ShadowRootInit {
+ return shadowRoot;
+ },
+
+ setTemplateContentForDeclarativeShadowRootParsing(template: Template, shadowRoot: ShadowRoot): void {
+ template.content = shadowRoot;
+ },
+ },
};
diff --git a/packages/parse5/lib/tree-adapters/interface.ts b/packages/parse5/lib/tree-adapters/interface.ts
index 3d43d20e3..5b02f9f8b 100644
--- a/packages/parse5/lib/tree-adapters/interface.ts
+++ b/packages/parse5/lib/tree-adapters/interface.ts
@@ -12,6 +12,7 @@ export interface TreeAdapterTypeMap<
TextNode = unknown,
Template = unknown,
DocumentType = unknown,
+ ShadowRoot = unknown,
> {
node: Node;
parentNode: ParentNode;
@@ -23,8 +24,18 @@ export interface TreeAdapterTypeMap<
textNode: TextNode;
template: Template;
documentType: DocumentType;
+ shadowRoot: ShadowRoot;
}
+export type ShadowRootInit = {
+ mode: 'open' | 'closed';
+ clonable: boolean;
+ serializable: boolean;
+ delegatesFocus: boolean;
+ customElementRegistry: string | null;
+ declarativeTemplateAttributes: Attribute[];
+};
+
/**
* Tree adapter is a set of utility functions that provides minimal required abstraction layer beetween parser and a specific AST format.
* Note that `TreeAdapter` is not designed to be a general purpose AST manipulation library. You can build such library
@@ -285,7 +296,7 @@ export interface TreeAdapter
* @param templateElement - `` element.
* @param contentElement - Content element.
*/
- setTemplateContent(templateElement: T['template'], contentElement: T['documentFragment']): void;
+ setTemplateContent(templateElement: T['template'], contentElement: T['documentFragment'] | T['shadowRoot']): void;
/**
* Optional callback for elements being pushed to the stack of open elements.
@@ -300,4 +311,39 @@ export interface TreeAdapter
* @param item The element being popped.
*/
onItemPop?: (item: T['element'], newTop: T['parentNode']) => void;
+
+ /**
+ * Adapter methods for supporting declarative shadow roots. If not provided then the associated options for declarative shadow roots will cause an error to be thrown.
+ */
+ declarativeShadowRootAdapter?: {
+ /**
+ * Attaches a declarative shadow root to the given element.
+ *
+ * @param element - Element
+ *
+ */
+ attachDeclarativeShadowRoot(element: T['element'], shadowRootInit: ShadowRootInit): T['shadowRoot'];
+
+ /**
+ * Returns the element's shadow root if it has one.
+ *
+ * @param element - Element
+ */
+ getShadowRoot(element: T['element']): T['shadowRoot'] | null;
+
+ /**
+ * Returns an object with the neccessary properties to initialize the given shadow root.
+ *
+ * @param shadowRoot - Shadow root
+ */
+ getShadowRootInit(shadowRoot: T['shadowRoot']): ShadowRootInit;
+
+ /**
+ * Sets the template's content to the given shadow root. Unlike setTemplateContent this must not change the shadow root's host.
+ *
+ * @param template - The template to attach to
+ * @param shadowRoot - The shadow root to attach
+ */
+ setTemplateContentForDeclarativeShadowRootParsing(template: T['template'], shadowRoot: T['shadowRoot']): void;
+ };
}