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 6 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.
44 changes: 38 additions & 6 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 {
// (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 {
// (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,6 +239,8 @@ export class Document extends Node<"document"> {

// @public (undocumented)
export namespace Document {
// (undocumented)
export function cloneDocument(document: Document, newElements: Element[], predicate: Predicate<Element>, device?: Device): Trampoline<Document>;
// @internal (undocumented)
export function fromDocument(json: JSON, device?: Device): Trampoline<Document>;
// (undocumented)
Expand Down Expand Up @@ -302,6 +308,8 @@ export class Element<N extends string = string> extends Node<"element"> implemen

// @public (undocumented)
export namespace Element {
// (undocumented)
export function cloneElement(element: Element, newElements: Element[], predicate: Predicate<Element>, device?: Device): 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 {
// (undocumented)
export function cloneFragment(fragment: Fragment, newElements: Element[], predicate: Predicate<Element>, device?: Device): Trampoline<Fragment>;
// @internal (undocumented)
export function fromFragment(json: JSON, device?: Device): Trampoline<Fragment>;
// (undocumented)
Expand Down Expand Up @@ -754,6 +764,27 @@ export interface Node {

// @public (undocumented)
export namespace Node {
// (undocumented)
export function clone(node: Element, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Element;
// (undocumented)
export function clone(node: Attribute, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Attribute;
// (undocumented)
export function clone(node: Text, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Text;
// (undocumented)
export function clone(node: Comment, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Comment;
// (undocumented)
export function clone(node: Document, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Document;
// (undocumented)
export function clone(node: Type, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Document;
const flatTree: Traversal;
const fullTree: Traversal;
const composedNested: Traversal;
// (undocumented)
export function clone(node: Fragment, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Fragment;
// (undocumented)
export function clone(node: Node, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Node;
// (undocumented)
export function cloneNode(node: Node, newElements?: Element[], predicate?: Predicate<Element>, device?: Device): Trampoline<Node>;
// (undocumented)
export interface EARL extends earl.EARL {
// (undocumented)
Expand Down Expand Up @@ -784,9 +815,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 +1170,8 @@ export class Text extends Node<"text"> implements Slotable {

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

// @public (undocumented)
export namespace Type {
// (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
102 changes: 102 additions & 0 deletions packages/alfa-dom/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,108 @@ export namespace Node {
}
}

export function clone(
node: Element,
newElements?: Element[],
rcj-siteimprove marked this conversation as resolved.
Show resolved Hide resolved
predicate?: Predicate<Element>,
device?: Device,
): Element;
rcj-siteimprove marked this conversation as resolved.
Show resolved Hide resolved

export function clone(
node: Attribute,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Attribute;

export function clone(
node: Text,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Text;

export function clone(
node: Comment,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Comment;

export function clone(
node: Document,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Document;

export function clone(
node: Type,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Document;

export function clone(
node: Fragment,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Fragment;

export function clone(
node: Node,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Node;

export function clone(
node: Node,
newElements?: Element[],
predicate?: Predicate<Element>,
device?: Device,
): Node {
return cloneNode(node, newElements, predicate, device).run();
}

export function cloneNode(
node: Node,
newElements: Element[] = [],
predicate: Predicate<Element> = () => false,
device?: Device,
): Trampoline<Node> {
if (Element.isElement(node)) {
rcj-siteimprove marked this conversation as resolved.
Show resolved Hide resolved
Jym77 marked this conversation as resolved.
Show resolved Hide resolved
return Element.cloneElement(node, newElements, predicate, device);
}

if (Attribute.isAttribute(node)) {
return Attribute.cloneAttribute(node);
}

if (Text.isText(node)) {
return Text.cloneText(node);
}

if (Comment.isComment(node)) {
return Comment.cloneComment(node);
}

if (Document.isDocument(node)) {
return Document.cloneDocument(node, newElements, predicate, device);
}

if (Type.isType(node)) {
return Type.cloneType(node);
}

if (Fragment.isFragment(node)) {
return Fragment.cloneFragment(node, newElements, predicate, device);
}

throw new Error(`Unexpected node of type: ${node.type}`);
}

export const { getNodesBetween } = traversal;

export const {
Expand Down
15 changes: 15 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,21 @@ export namespace Attribute {
);
}

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,
attribute.extraData,
),
);
}

/**
* 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
6 changes: 6 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,10 @@ export namespace Comment {
export function fromComment(json: JSON): Trampoline<Comment> {
return Trampoline.done(Comment.of(json.data));
}

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

export function cloneDocument(
document: Document,
newElements: Element[],
predicate: Predicate<Element>,
device?: Device,
): Trampoline<Document> {
return Trampoline.traverse(document.children(), (child) => {
if (Element.isElement(child) && predicate(child)) {
return Trampoline.done(newElements);
}

return Node.cloneNode(child, newElements, predicate, device).map(
(node) => [node],
);
Jym77 marked this conversation as resolved.
Show resolved Hide resolved
}).map((children) => {
return Document.of(
Iterable.flatten(children),
document.style,
document.externalId,
document.extraData,
);
});
}
}

function indent(input: string): string {
Expand Down
54 changes: 54 additions & 0 deletions packages/alfa-dom/src/node/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Shadow } from "./shadow";
import { Slot } from "./slot";
import { Slotable } from "./slotable";

import { Declaration } from "@siteimprove/alfa-dom";
import * as helpers from "./element/input-type";
import * as predicate from "./element/predicate";

Expand Down Expand Up @@ -447,6 +448,59 @@ export namespace Element {
});
}

export function cloneElement(
element: Element,
newElements: Element[],
predicate: Predicate<Element>,
device?: Device,
): Trampoline<Element> {
return Trampoline.traverse(element.children(), (child) => {
if (Element.isElement(child) && predicate(child)) {
return Trampoline.done(newElements);
}

return Node.cloneNode(child, newElements, predicate, device).map(
(node) => [node],
);
}).map((children) => {
const deviceOption = Option.from(device);
const clonedElement = Element.of(
element.namespace,
element.prefix,
element.name,
element.attributes.map((attribute) =>
Attribute.cloneAttribute(attribute).run(),
),
Iterable.flatten(children),
element.style.map((block) => {
return Block.of(
Iterable.map(block.declarations, (declaration) =>
Declaration.of(
declaration.name,
declaration.value,
declaration.important,
),
),
);
}),
deviceOption.flatMap((d) => element.getBoundingBox(d)),
deviceOption,
element.externalId,
element.extraData,
Jym77 marked this conversation as resolved.
Show resolved Hide resolved
);

if (element.shadow.isSome()) {
element._attachShadow(element.shadow.get());
}

if (element.content.isSome()) {
element._attachContent(element.content.get());
}
rcj-siteimprove marked this conversation as resolved.
Show resolved Hide resolved

return clonedElement;
});
}

export const {
hasAttribute,
hasBox,
Expand Down
Loading