-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updates annotation format to use a
<rules_prerender:annotation />
t…
…ag instead of a comment. Refs #71. Preact can't easily render comments, meaning it is tricky to support `includeScript()` and `inlineStyle()` with Preact when they use comments. Instead, we render `<rules_prerender:annotation>...</rules_prerender:annotation>`. The `rules_prerender:` technically makes it an XML namespace, though without any explicit declaration. It should be fine since these elements never make it to the browser. Since this is no a full `HTMLElement` and not a `Node`, we can simplify some of the implementation and drop `UpdateableNode`. For now, I left the actual JSON content in the tag's content. We no longer really need the `RULES_PRERENDER_DO_NOT_DEPEND_OR_ELSE` prefix given that `rules_prerender:annotation` already identifies the relevant tag. I might remove that in a future commit. Later on we might also want to support multiple child nodes for something like SSR. Of course it might be better to just escape the HTML content inside the element. Most of the changes here are adjusting tests, the actual change in `packages/rules_prerender/{scripts,styles}.mts` is relatively straightforward and just required some updates to `prerender_annotation_walker.mts` to extract the new format.
- Loading branch information
Showing
14 changed files
with
228 additions
and
317 deletions.
There are no files selected for viewing
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 |
---|---|---|
@@ -1,141 +1,55 @@ | ||
import { CommentNode, HTMLElement, Node } from 'node-html-parser'; | ||
import { HTMLElement } from 'node-html-parser'; | ||
import { parseAnnotation, PrerenderAnnotation } from './models/prerender_annotation.mjs'; | ||
|
||
/** | ||
* A reference to a `node-html-parser` `Node` which contains a | ||
* {@link PrerenderAnnotation} and can be updated (removed from / replaced in the tree). | ||
*/ | ||
export class AnnotationNode { | ||
export interface AnnotationEl { | ||
/** The annotation this node contains. */ | ||
public annotation: PrerenderAnnotation; | ||
annotation: PrerenderAnnotation; | ||
|
||
private updateableNode: UpdateableNode<CommentNode>; | ||
|
||
private constructor({ annotation, updateableNode }: { | ||
annotation: PrerenderAnnotation, | ||
updateableNode: UpdateableNode<CommentNode>, | ||
}) { | ||
this.annotation = annotation; | ||
this.updateableNode = updateableNode; | ||
} | ||
|
||
/** Returns an `AnnotationNode` for the given annotation and its node. */ | ||
public static from( | ||
annotation: PrerenderAnnotation, | ||
updateableNode: UpdateableNode<CommentNode>, | ||
): AnnotationNode { | ||
return new AnnotationNode({ annotation, updateableNode }); | ||
} | ||
|
||
/** | ||
* Removes this annotation's node from the tree. | ||
* @throws if the annotation's node has already been updated. | ||
*/ | ||
public remove(): void { | ||
this.updateableNode.remove(); | ||
} | ||
|
||
/** | ||
* Replaces this annotation's node in the DOM tree with the given node. | ||
* @throws if the node has already been updated. | ||
*/ | ||
public replace(replacement: Node): void { | ||
this.updateableNode.replace(replacement); | ||
} | ||
} | ||
|
||
/** | ||
* A reference to a `node-html-parser` `Node` which can be updated (removed / replaced). | ||
* This is useful a `Node` cannot normally be removed or replaced without also knowing | ||
* its parent, which can be annoying to track. | ||
*/ | ||
class UpdateableNode<T extends Node> { | ||
/** The `node-html-parser` `Node` this references. */ | ||
public node: T; | ||
|
||
private parent: HTMLElement; | ||
private updated = false; | ||
|
||
private constructor({ node, parent }: { node: T, parent: HTMLElement }) { | ||
this.node = node; | ||
this.parent = parent; | ||
} | ||
|
||
/** Returns an `UpdateableNode` for the given node and its parent. */ | ||
public static from<T extends Node>( | ||
{ node, parent }: { node: T, parent: HTMLElement }, | ||
): UpdateableNode<T> { | ||
return new UpdateableNode({ node, parent }); | ||
} | ||
|
||
/** | ||
* Removes this node from the tree. | ||
* @throws if the node has already been updated. | ||
*/ | ||
public remove(): void { | ||
if (this.updated) throw new Error(`Node was already updated, cannot remove it.`); | ||
this.updated = true; | ||
|
||
this.parent.removeChild(this.node); | ||
} | ||
|
||
/** | ||
* Replaces this node in the DOM tree with the given node. | ||
* @throws if the node has already been updated. | ||
*/ | ||
public replace(replacement: Node): void { | ||
if (this.updated) throw new Error(`Node was already updated, cannot replace it.`); | ||
this.updated = true; | ||
|
||
this.parent.exchangeChild(this.node, replacement); | ||
} | ||
/** The element containing the annotation. */ | ||
el: HTMLElement; | ||
} | ||
|
||
/** Returns a {@link Generator} of all annotations in the tree. */ | ||
export function walkAllAnnotations(root: HTMLElement): | ||
Generator<AnnotationNode, void, void> { | ||
return walkAnnotations(walkComments(walk(root))); | ||
Generator<AnnotationEl, void, void> { | ||
return walkAnnotations(walk(root)); | ||
} | ||
|
||
/** | ||
* Parses the given {@link Generator} of {@link CommentNode}s, filtering out any which | ||
* aren't annotations and returning a new {@link Generator} of {@link AnnotationNode}. | ||
* Parses the given {@link Generator} of {@link HTMLElement}s, filtering | ||
* out any which aren't annotations and returning a new {@link Generator} of | ||
* {@link AnnotationEl}. | ||
*/ | ||
function* walkAnnotations( | ||
nodes: Generator<UpdateableNode<CommentNode>, void, void>, | ||
): Generator<AnnotationNode, void, void> { | ||
for (const updateableNode of nodes) { | ||
const annotation = parseAnnotation(updateableNode.node.text); | ||
if (!annotation) continue; | ||
function* walkAnnotations(els: Generator<HTMLElement, void, void>): | ||
Generator<AnnotationEl, void, void> { | ||
for (const el of els) { | ||
// Root element has a `null` `tagName`. | ||
if (el.tagName?.toLowerCase() !== 'rules_prerender:annotation') continue; | ||
|
||
yield AnnotationNode.from(annotation, updateableNode); | ||
} | ||
} | ||
// Parse the annotation. | ||
const annotation = parseAnnotation(el.textContent); | ||
if (!annotation) throw new Error(`Failed to parse annotation:\n${el.outerHTML}`); | ||
|
||
/** | ||
* Filters the given {@link Generator} of {@link Node}s to limit to only | ||
* {@link CommentNode}s. | ||
*/ | ||
function* walkComments(nodes: Generator<UpdateableNode<Node>, void, void>): | ||
Generator<UpdateableNode<CommentNode>> { | ||
for (const updateableNode of nodes) { | ||
if (updateableNode.node instanceof CommentNode) { | ||
yield updateableNode; | ||
} | ||
yield { annotation, el }; | ||
} | ||
} | ||
|
||
/** | ||
* Returns a {@link Generator} of {@link UpdateableNode}s for each descendant of the | ||
* given root and their parent. The root node without a parent is dropped. | ||
* Returns a {@link Generator} of {@link UpdateableElement}s for each descendant | ||
* of the given root and their parent. The root node without a parent is | ||
* dropped. | ||
*/ | ||
function* walk(node: Node, parent?: HTMLElement): | ||
Generator<UpdateableNode<Node>, void, void> { | ||
if (parent) yield UpdateableNode.from({ node, parent }); | ||
function* walk(el: HTMLElement): Generator<HTMLElement, void, void> { | ||
yield el; | ||
|
||
if (node instanceof HTMLElement) { | ||
for (const child of node.childNodes) { | ||
yield* walk(child, node); | ||
for (const child of el.childNodes) { | ||
// Ignore `Nodes`, all annotations are `HTMLElements`. | ||
if (child instanceof HTMLElement) { | ||
yield* walk(child); | ||
} | ||
} | ||
} |
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
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
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
Oops, something went wrong.