Skip to content
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

Clone-replace for alfa-dom Nodes #1523

Merged
merged 24 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aea426c
Add node cloning functions with replace capability
rcj-siteimprove Dec 1, 2023
0253c6f
Add changeset
rcj-siteimprove Dec 1, 2023
e1b2906
Clone attributes
rcj-siteimprove Dec 4, 2023
55d195c
Clean up
rcj-siteimprove Dec 4, 2023
6299b92
Merge branch 'main' into node-cloning
rcj-siteimprove Dec 4, 2023
1ce9c2d
Extract API
github-actions[bot] Dec 4, 2023
e6bc527
Update clone functions to accept `Iterable<Element>`
rcj-siteimprove Dec 6, 2023
07dc6c9
Add interface for gathering replace predicate and elements to replace
rcj-siteimprove Dec 6, 2023
e958464
Use `Selective` and currify functions
rcj-siteimprove Dec 6, 2023
7bb415a
Fix shadow and content not being cloned and add tests for that
rcj-siteimprove Dec 6, 2023
0c0f376
Merge remote-tracking branch 'refs/remotes/origin/node-cloning' into …
rcj-siteimprove Dec 6, 2023
619df86
Extract API
github-actions[bot] Dec 7, 2023
8f385d4
Update cloning functions to discard `extraData` and add doc strings
rcj-siteimprove Dec 7, 2023
8c64e79
Update doc strings
rcj-siteimprove Dec 7, 2023
5b30d0e
Add remark about externalId to doc strings
rcj-siteimprove Dec 7, 2023
9e292b9
Extract API
github-actions[bot] Dec 7, 2023
a4352ec
Merge branch 'main' into node-cloning
rcj-siteimprove Dec 7, 2023
392632e
Update dependencies
rcj-siteimprove Dec 7, 2023
987de4d
Merge remote-tracking branch 'refs/remotes/origin/node-cloning' into …
rcj-siteimprove Dec 7, 2023
e1fbdf3
Extract shadow cloning and simplify tests
rcj-siteimprove Dec 7, 2023
cec6b70
Merge branch 'main' into node-cloning
rcj-siteimprove Dec 7, 2023
1280281
Extract API
github-actions[bot] Dec 7, 2023
adc173a
Clean up
rcj-siteimprove Dec 7, 2023
e8a2344
Merge remote-tracking branch 'refs/remotes/origin/node-cloning' into …
rcj-siteimprove Dec 7, 2023
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 .changeset/twelve-shrimps-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-dom": minor
---

**Added:** New functions `Node.clone` for cloning nodes and optionally replacing child elements based on a predicate.
45 changes: 38 additions & 7 deletions docs/review/api/alfa-dom.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class Attribute<N extends string = string> extends Node<"attribute"> {

// @public (undocumented)
export namespace Attribute {
// @internal (undocumented)
export function cloneAttribute<N extends string = string>(attribute: Attribute<N>): Trampoline<Attribute<N | Lowercase<N>>>;
// @internal
export function foldCase<N extends string = string>(name: N, owner: Option<Element>): N | Lowercase<N>;
// @internal (undocumented)
Expand Down Expand Up @@ -132,6 +134,8 @@ export class Comment extends Node<"comment"> {

// @public (undocumented)
export namespace Comment {
// @internal (undocumented)
export function cloneComment(comment: Comment): Trampoline<Comment>;
// @internal (undocumented)
export function fromComment(json: JSON): Trampoline<Comment>;
// (undocumented)
Expand Down Expand Up @@ -222,11 +226,11 @@ export class Document extends Node<"document"> {
// @internal (undocumented)
protected _internalPath(options?: Node.Traversal): string;
// (undocumented)
static of(children: Iterable<Node>, style?: Iterable<Sheet>, externalId?: string, extraData?: any): Document;
static of(children: Iterable_2<Node>, style?: Iterable_2<Sheet>, externalId?: string, extraData?: any): Document;
// (undocumented)
parent(options?: Node.Traversal): Option<Node>;
// (undocumented)
get style(): Iterable<Sheet>;
get style(): Iterable_2<Sheet>;
// (undocumented)
toJSON(options?: Node.SerializationOptions): Document.JSON;
// (undocumented)
Expand All @@ -235,14 +239,16 @@ export class Document extends Node<"document"> {

// @public (undocumented)
export namespace Document {
// @internal (undocumented)
export function cloneDocument(options: Node.ElementReplacementOptions, device?: Device): (document: Document) => Trampoline<Document>;
// @internal (undocumented)
export function fromDocument(json: JSON, device?: Device): Trampoline<Document>;
// (undocumented)
export function isDocument(value: unknown): value is Document;
// (undocumented)
export interface JSON extends Node.JSON<"document"> {
// (undocumented)
style: Array<Sheet.JSON>;
style: Array_2<Sheet.JSON>;
}
}

Expand Down Expand Up @@ -302,6 +308,8 @@ export class Element<N extends string = string> extends Node<"element"> implemen

// @public (undocumented)
export namespace Element {
// @internal (undocumented)
export function cloneElement(options: Node.ElementReplacementOptions, device?: Device): (element: Element) => Trampoline<Element>;
// @internal (undocumented)
export function fromElement<N extends string = string>(json: JSON<N>, device?: Device): Trampoline<Element<N>>;
// (undocumented)
Expand Down Expand Up @@ -391,13 +399,15 @@ export class Fragment extends Node<"fragment"> {
// @internal (undocumented)
protected _internalPath(): string;
// (undocumented)
static of(children: Iterable<Node>, externalId?: string, extraData?: any): Fragment;
static of(children: Iterable_2<Node>, externalId?: string, extraData?: any): Fragment;
// (undocumented)
toString(): string;
}

// @public (undocumented)
export namespace Fragment {
// @internal (undocumented)
export function cloneFragment(options: Node.ElementReplacementOptions, device?: Device): (fragment: Fragment) => Trampoline<Fragment>;
// @internal (undocumented)
export function fromFragment(json: JSON, device?: Device): Trampoline<Fragment>;
// (undocumented)
Expand Down Expand Up @@ -754,6 +764,19 @@ export interface Node {

// @public (undocumented)
export namespace Node {
export function clone(node: Element, options?: ElementReplacementOptions, device?: Device): Element;
export function clone(node: Attribute, options?: ElementReplacementOptions, device?: Device): Attribute;
export function clone(node: Text, options?: ElementReplacementOptions, device?: Device): Text;
export function clone(node: Comment, options?: ElementReplacementOptions, device?: Device): Comment;
export function clone(node: Document, options?: ElementReplacementOptions, device?: Device): Document;
export function clone(node: Type, options?: ElementReplacementOptions, device?: Device): Document;
const flatTree: Traversal;
const fullTree: Traversal;
const composedNested: Traversal;
export function clone(node: Fragment, options?: ElementReplacementOptions, device?: Device): Fragment;
export function clone(node: Node, options?: ElementReplacementOptions, device?: Device): Node;
// @internal (undocumented)
export function cloneNode(node: Node, options?: ElementReplacementOptions, device?: Device): Trampoline<Node>;
// (undocumented)
export interface EARL extends earl.EARL {
// (undocumented)
Expand All @@ -775,6 +798,13 @@ export namespace Node {
};
}
// (undocumented)
export interface ElementReplacementOptions {
// (undocumented)
newElements: Iterable<Element>;
// (undocumented)
predicate: Predicate<Element>;
}
// (undocumented)
export function from(json: Element.JSON, device?: Device): Element;
// (undocumented)
export function from(json: Attribute.JSON, device?: Device): Attribute;
Expand All @@ -784,9 +814,6 @@ export namespace Node {
export function from(json: Comment.JSON, device?: Device): Comment;
// (undocumented)
export function from(json: Document.JSON, device?: Device): Document;
const flatTree: Traversal;
const fullTree: Traversal;
const composedNested: Traversal;
// (undocumented)
export function from(json: Type.JSON, device?: Device): Document;
// (undocumented)
Expand Down Expand Up @@ -1142,6 +1169,8 @@ export class Text extends Node<"text"> implements Slotable {

// @public (undocumented)
export namespace Text {
// @internal (undocumented)
export function cloneText(text: Text): Trampoline<Text>;
// @internal (undocumented)
export function fromText(json: JSON): Trampoline<Text>;
// (undocumented)
Expand Down Expand Up @@ -1173,6 +1202,8 @@ export class Type<N extends string = string> extends Node<"type"> {

// @public (undocumented)
export namespace Type {
// @internal (undocumented)
export function cloneType<N extends string = string>(type: Type<N>): Trampoline<Type<N>>;
// @internal (undocumented)
export function fromType<N extends string = string>(json: JSON<N>): Trampoline<Type<N>>;
// (undocumented)
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@siteimprove/alfa-rectangle": "workspace:^0.69.0",
"@siteimprove/alfa-refinement": "workspace:^0.69.0",
"@siteimprove/alfa-sarif": "workspace:^0.69.0",
"@siteimprove/alfa-selective": "workspace:^0.69.0",
"@siteimprove/alfa-sequence": "workspace:^0.69.0",
"@siteimprove/alfa-trampoline": "workspace:^0.69.0",
"@siteimprove/alfa-tree": "workspace:^0.69.0"
Expand Down
149 changes: 148 additions & 1 deletion packages/alfa-dom/src/node.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Device } from "@siteimprove/alfa-device";
import { Flags } from "@siteimprove/alfa-flags";
import { Lazy } from "@siteimprove/alfa-lazy";
import { Option } from "@siteimprove/alfa-option";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Refinement } from "@siteimprove/alfa-refinement";
import { Selective } from "@siteimprove/alfa-selective";
import { Sequence } from "@siteimprove/alfa-sequence";
import { Trampoline } from "@siteimprove/alfa-trampoline";

Expand All @@ -23,7 +25,6 @@ import {
Type,
} from ".";

import { Device } from "@siteimprove/alfa-device";
import * as predicate from "./node/predicate";
import * as traversal from "./node/traversal";

Expand Down Expand Up @@ -380,6 +381,152 @@ export namespace Node {
}
}

export interface ElementReplacementOptions {
predicate: Predicate<Element>;
newElements: Iterable<Element>;
}

/**
* Creates a new `Element` instance with the same value as the original and deeply referentially non-equal.
* Optionally replaces child elements based on a predicate.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Element,
options?: ElementReplacementOptions,
device?: Device,
): Element;

/**
* Creates a new `Attribute` instance with the same value as the original and referentially non-equal.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Attribute,
options?: ElementReplacementOptions,
device?: Device,
): Attribute;

/**
* Creates a new `Text` instance with the same value as the original and referentially non-equal.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Text,
options?: ElementReplacementOptions,
device?: Device,
): Text;

/**
* Creates a new `Comment` instance with the same value as the original and referentially non-equal.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Comment,
options?: ElementReplacementOptions,
device?: Device,
): Comment;

/**
* Creates a new `Document` instance with the same value as the original and deeply referentially non-equal.
* Optionally replaces child elements based on a predicate.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Document,
options?: ElementReplacementOptions,
device?: Device,
): Document;

/**
* Creates a new `Type` instance with the same value as the original and referentially non-equal.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Type,
options?: ElementReplacementOptions,
device?: Device,
): Document;

/**
* Creates a new `Fragment` instance with the same value as the original and deeply referentially non-equal.
* Optionally replaces child elements based on a predicate.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Fragment,
options?: ElementReplacementOptions,
device?: Device,
): Fragment;

/**
* Creates a new `Node` instance with the same value as the original and deeply referentially non-equal.
* Optionally replaces child elements based on a predicate.
*
* @remarks
* The clone will have the same `externalId` as the original.
* The clone will *not* get `extraData` from the original, instead it will be `undefined`.
*/
export function clone(
node: Node,
options?: ElementReplacementOptions,
device?: Device,
): Node;

export function clone(
node: Node,
options?: ElementReplacementOptions,
device?: Device,
): Node {
return cloneNode(node, options, device).run();
}

/**
* @internal
*/
export function cloneNode(
node: Node,
options: ElementReplacementOptions = {
predicate: () => false,
newElements: [],
},
device?: Device,
): Trampoline<Node> {
return Selective.of(node)
.if(Element.isElement, Element.cloneElement(options, device))
.if(Attribute.isAttribute, Attribute.cloneAttribute)
.if(Text.isText, Text.cloneText)
.if(Comment.isComment, Comment.cloneComment)
.if(Document.isDocument, Document.cloneDocument(options, device))
.if(Type.isType, Type.cloneType)
.if(Fragment.isFragment, Fragment.cloneFragment(options, device))
.else(() => {
throw new Error(`Unexpected node of type: ${node.type}`);
})
.get();
}

export const { getNodesBetween } = traversal;

export const {
Expand Down
17 changes: 17 additions & 0 deletions packages/alfa-dom/src/node/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,23 @@ export namespace Attribute {
);
}

/**
* @internal
*/
export function cloneAttribute<N extends string = string>(
attribute: Attribute<N>,
): Trampoline<Attribute<N | Lowercase<N>>> {
return Trampoline.done(
Attribute.of(
attribute.namespace,
attribute.prefix,
attribute.name,
attribute.value,
attribute.externalId,
),
);
}

/**
* Conditionally fold the case of an attribute name based on its owner; HTML
* attributes are case insensitive while attributes in other namespaces aren't.
Expand Down
7 changes: 7 additions & 0 deletions packages/alfa-dom/src/node/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,11 @@ export namespace Comment {
export function fromComment(json: JSON): Trampoline<Comment> {
return Trampoline.done(Comment.of(json.data));
}

/**
* @internal
*/
export function cloneComment(comment: Comment): Trampoline<Comment> {
return Trampoline.done(Comment.of(comment.data, comment.externalId));
}
}
25 changes: 25 additions & 0 deletions packages/alfa-dom/src/node/document.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Array } from "@siteimprove/alfa-array";
import { Device } from "@siteimprove/alfa-device";
import { Iterable } from "@siteimprove/alfa-iterable";
import { None, Option } from "@siteimprove/alfa-option";
import { Trampoline } from "@siteimprove/alfa-trampoline";
import { Node } from "../node";
Expand Down Expand Up @@ -125,6 +127,29 @@ export namespace Document {
Node.fromNode(child, device),
).map((children) => Document.of(children, json.style.map(Sheet.from)));
}

/**
* @internal
*/
export function cloneDocument(
options: Node.ElementReplacementOptions,
device?: Device,
): (document: Document) => Trampoline<Document> {
return (document) =>
Trampoline.traverse(document.children(), (child) => {
if (Element.isElement(child) && options.predicate(child)) {
return Trampoline.done(Array.from(options.newElements));
}

return Node.cloneNode(child, options, device).map((node) => [node]);
}).map((children) => {
return Document.of(
Iterable.flatten(children),
document.style,
document.externalId,
);
});
}
}

function indent(input: string): string {
Expand Down
Loading