Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/parse5/lib/common/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export enum ATTRS {
COLOR = 'color',
FACE = 'face',
SIZE = 'size',
SHADOWROOTMODE = 'shadowrootmode',
SHADOWROOTCLONABLE = 'shadowrootclonable',
SHADOWROOTSERIALIZABLE = 'shadowrootserializable',
SHADOWROOTDELEGATESFOCUS = 'shadowrootdelegatesfocus',
SHADOWROOTCUSTOMELEMENTREGISTRY = 'shadowrootcustomelementregistry',
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/parse5/lib/common/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
59 changes: 56 additions & 3 deletions packages/parse5/lib/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type EOFToken,
type LocationWithAttributes,
type ElementLocation,
hasTokenAttr,
} from '../common/token.js';

//Misc constants
Expand Down Expand Up @@ -96,6 +97,13 @@ export interface ParserOptions<T extends TreeAdapterTypeMap> {
*/
sourceCodeLocationInfo?: boolean;

/**
* Whether declarative shadow roots should be parsed or treated as ordinary templates.
*
* @default `false`
*/
allowDeclarativeShadowRoots?: boolean;

/**
* Specifies the resulting tree format.
*
Expand All @@ -114,6 +122,7 @@ export interface ParserOptions<T extends TreeAdapterTypeMap> {
const defaultParserOptions: Required<ParserOptions<DefaultTreeAdapterMap>> = {
scriptingEnabled: true,
sourceCodeLocationInfo: false,
allowDeclarativeShadowRoots: false,
treeAdapter: defaultTreeAdapter,
onParseError: null,
};
Expand Down Expand Up @@ -143,6 +152,10 @@ export class Parser<T extends TreeAdapterTypeMap> 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;
Expand Down Expand Up @@ -453,14 +466,17 @@ export class Parser<T extends TreeAdapterTypeMap> 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 */
Expand Down Expand Up @@ -1723,11 +1739,48 @@ function startTagInHead<T extends TreeAdapterTypeMap>(p: Parser<T>, 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: {
Expand Down
101 changes: 91 additions & 10 deletions packages/parse5/lib/serializer/index.ts
Original file line number Diff line number Diff line change
@@ -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<string>([
Expand Down Expand Up @@ -33,6 +34,21 @@ function isVoidElement<T extends TreeAdapterTypeMap>(node: T['node'], options: I
);
}

const KNOWN_DECLARATIVE_SHADOW_ROOT_ATTRIBUTES = new Set<string>([
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<T extends TreeAdapterTypeMap> {
/**
* Specifies input tree format.
Expand All @@ -47,11 +63,40 @@ export interface SerializerOptions<T extends TreeAdapterTypeMap> {
* @default `true`
*/
scriptingEnabled?: boolean;
/**
* Whether to serialize shadow roots.
*
* @default `false`
*/
serializableShadowRoots?: boolean;

/**
* Shadow roots to always serialize.
*
* @default `[]`
*/
shadowRoots?: Array<T['shadowRoot']>;
}

type InternalOptions<T extends TreeAdapterTypeMap> = Required<SerializerOptions<T>>;

const defaultOpts: InternalOptions<DefaultTreeAdapterMap> = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true };
const defaultOpts: InternalOptions<DefaultTreeAdapterMap> = {
treeAdapter: defaultTreeAdapter,
scriptingEnabled: true,
serializableShadowRoots: false,
shadowRoots: [],
};

function validateDeclarativeShadowRootSupport<T extends TreeAdapterTypeMap = DefaultTreeAdapterMap>(
opts: InternalOptions<T>,
): 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.
Expand Down Expand Up @@ -81,6 +126,8 @@ export function serialize<T extends TreeAdapterTypeMap = DefaultTreeAdapterMap>(
): string {
const opts = { ...defaultOpts, ...options } as InternalOptions<T>;

validateDeclarativeShadowRootSupport(opts);

if (isVoidElement(node, opts)) {
return '';
}
Expand Down Expand Up @@ -112,6 +159,7 @@ export function serializeOuter<T extends TreeAdapterTypeMap = DefaultTreeAdapter
options?: SerializerOptions<T>,
): string {
const opts = { ...defaultOpts, ...options } as InternalOptions<T>;
validateDeclarativeShadowRootSupport(opts);
return serializeNode(node, opts);
}

Expand Down Expand Up @@ -158,17 +206,50 @@ function serializeNode<T extends TreeAdapterTypeMap>(node: T['node'], options: I
function serializeElement<T extends TreeAdapterTypeMap>(node: T['element'], options: InternalOptions<T>): 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 += `<template${serializeAttributes(declarativeTemplateAttrs)}>${serializeChildNodes(options.treeAdapter.getChildNodes(shadowRoot), options)}</template>`;
}
}

html += `</${tn}>`;
return html;
}

function serializeAttributes<T extends TreeAdapterTypeMap>(
node: T['element'],
{ treeAdapter }: InternalOptions<T>,
): string {
function serializeAttributes(attrs: Attribute[]): string {
let html = '';
for (const attr of treeAdapter.getAttrList(node)) {
for (const attr of attrs) {
html += ' ';

if (attr.namespace) {
Expand Down
66 changes: 63 additions & 3 deletions packages/parse5/lib/tree-adapters/default.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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 <template> element when serialized as a declarative shadow root. */
declarativeTemplateAttributes: Attribute[];
}

export interface Element {
/** Element tag name. Same as {@link tagName}. */
nodeName: string;
Expand All @@ -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[];
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -127,6 +146,7 @@ export const defaultTreeAdapter: TreeAdapter<DefaultTreeAdapterMap> = {
namespaceURI,
childNodes: [],
parentNode: null,
shadowRoot: null,
};
},

Expand Down Expand Up @@ -311,4 +331,44 @@ export const defaultTreeAdapter: TreeAdapter<DefaultTreeAdapterMap> = {
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;
},
},
};
Loading