Skip to content

Commit bd7b897

Browse files
author
Addon Stack
committed
feat(content): introduce marker-based anchor handling and cleanup resolvers
- Added `ContentScriptMarkerContract` for marker management. - Replaced `contentScriptAnchorAttribute` with marker attribute logic. - Refactored `Node` and introduced `MarkerNode` wrapping for marker operations. - Abstracted marker logic into `AbstractMarker` and `AttributeMarker`. - Updated related definitions and resolved configurations for marker integration.
1 parent 24a9395 commit bd7b897

File tree

9 files changed

+263
-77
lines changed

9 files changed

+263
-77
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {ContentScriptAnchor, ContentScriptMarkerContract, ContentScriptMarkerValue} from "@typing/content";
2+
3+
export default abstract class implements ContentScriptMarkerContract {
4+
public abstract marked(): Element[];
5+
6+
public abstract mark(element: Element, value: ContentScriptMarkerValue): boolean;
7+
8+
public abstract unmark(element: Element): boolean;
9+
10+
public abstract isMarked(element: Element): boolean;
11+
12+
public abstract value(element: Element): ContentScriptMarkerValue | undefined;
13+
14+
protected constructor(protected anchor: ContentScriptAnchor) {}
15+
16+
public for(anchor: ContentScriptAnchor): ContentScriptMarkerContract {
17+
this.anchor = anchor;
18+
19+
return this;
20+
}
21+
22+
public pending(): Element[] {
23+
const elements: Element[] = [];
24+
25+
let resolved = this.anchor;
26+
27+
if (resolved instanceof Element) {
28+
if (!this.isMarked(resolved)) {
29+
elements.push(resolved);
30+
}
31+
} else if (typeof resolved === "string") {
32+
if (resolved.startsWith("/")) {
33+
elements.push(...this.queryXpath(resolved));
34+
} else {
35+
elements.push(...this.querySelector(resolved));
36+
}
37+
}
38+
39+
return elements;
40+
}
41+
42+
public mount(element: Element): boolean {
43+
return this.mark(element, ContentScriptMarkerValue.Mounded);
44+
}
45+
46+
public unmount(element: Element): boolean {
47+
return this.mark(element, ContentScriptMarkerValue.Unmounted);
48+
}
49+
50+
public reset(): ContentScriptMarkerContract {
51+
for (const element of this.marked()) {
52+
this.unmark(element);
53+
}
54+
55+
return this;
56+
}
57+
58+
protected querySelector(selector: string): Element[] {
59+
return Array.from(document.querySelectorAll(selector));
60+
}
61+
62+
protected queryXpath(xpath: string): Element[] {
63+
const elements: Element[] = [];
64+
65+
const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
66+
67+
for (let i = 0; i < result.snapshotLength; i++) {
68+
elements.push(result.snapshotItem(i) as Element);
69+
}
70+
71+
return elements;
72+
}
73+
74+
protected isValidValue(value: any): value is ContentScriptMarkerValue {
75+
return [ContentScriptMarkerValue.Unmounted, ContentScriptMarkerValue.Mounded].includes(value);
76+
}
77+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {customAlphabet} from "nanoid";
2+
3+
import AbstractMarker from "./AbstractMarker";
4+
5+
import {ContentScriptAnchor, ContentScriptMarkerValue} from "@typing/content";
6+
7+
export default class extends AbstractMarker {
8+
protected readonly attr: string;
9+
10+
constructor(anchor?: ContentScriptAnchor) {
11+
super(anchor);
12+
13+
const generateId = customAlphabet("abcdefghijklmnopqrstuvwxyz", 7);
14+
15+
this.attr = `data-${generateId()}`;
16+
}
17+
18+
public isMarked(element: Element): boolean {
19+
return element.hasAttribute(this.attr);
20+
}
21+
22+
public mark(element: Element, value: ContentScriptMarkerValue): boolean {
23+
element.setAttribute(this.attr, value);
24+
25+
return true;
26+
}
27+
28+
public marked(): Element[] {
29+
const anchor = this.anchor;
30+
31+
if (anchor instanceof Element) {
32+
if (this.isMarked(anchor)) {
33+
return [anchor];
34+
}
35+
36+
return [];
37+
}
38+
39+
return Array.from(document.querySelectorAll(`${anchor}[${this.attr}]`));
40+
}
41+
42+
public unmark(element: Element): boolean {
43+
element.removeAttribute(this.attr);
44+
45+
return true;
46+
}
47+
48+
public value(element: Element): ContentScriptMarkerValue | undefined {
49+
const value = element.getAttribute(this.attr);
50+
51+
return this.isValidValue(value) ? value : undefined;
52+
}
53+
54+
protected querySelector(selector: string): Element[] {
55+
return super.querySelector(`${selector}:not([${this.attr}])`);
56+
}
57+
58+
protected queryXpath(xpath: string): Element[] {
59+
return super.queryXpath(`(${xpath})[not(@${this.attr})]`);
60+
}
61+
}

src/entry/content/core/Builder.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,26 @@ import {
1414

1515
import ManagedContext from "./ManagedContext";
1616
import EventEmitter from "./EventEmitter";
17+
import AttributeMarker from "./AttributeMarker";
1718

1819
import {
1920
ContentScriptAnchor,
2021
ContentScriptAnchorGetter,
21-
ContentScriptAnchorResolver,
2222
ContentScriptBuilder,
2323
ContentScriptContainerCreator,
2424
ContentScriptContainerFactory,
2525
ContentScriptContainerOptions,
2626
ContentScriptContainerTag,
2727
ContentScriptContext,
2828
ContentScriptDefinition,
29+
ContentScriptMarker,
30+
ContentScriptMarkerContract,
31+
ContentScriptMarkerGetter,
32+
ContentScriptMarkerResolver,
33+
ContentScriptMarkerType,
2934
ContentScriptMountFunction,
3035
ContentScriptNode,
36+
ContentScriptOptions,
3137
ContentScriptRenderHandler,
3238
ContentScriptRenderValue,
3339
ContentScriptResolvedDefinition,
@@ -45,6 +51,8 @@ export default abstract class extends Builder implements ContentScriptBuilder {
4551

4652
protected readonly context = new ManagedContext(this.emitter);
4753

54+
protected marker: ContentScriptMarkerContract = new AttributeMarker();
55+
4856
protected unwatch?: () => void;
4957

5058
protected abstract createNode(anchor: Element): Promise<ContentScriptNode>;
@@ -56,6 +64,7 @@ export default abstract class extends Builder implements ContentScriptBuilder {
5664

5765
this.definition = {
5866
...definition,
67+
marker: this.resolveMarker(definition.marker),
5968
anchor: this.resolveAnchor(definition.anchor),
6069
mount: this.resolveMount(definition.mount),
6170
container: this.resolveContainer(definition.container),
@@ -64,7 +73,25 @@ export default abstract class extends Builder implements ContentScriptBuilder {
6473
};
6574
}
6675

67-
protected resolveAnchor(anchor?: ContentScriptAnchor | ContentScriptAnchorGetter): ContentScriptAnchorResolver {
76+
protected resolveMarker(marker: ContentScriptMarkerType | ContentScriptMarkerGetter): ContentScriptMarkerResolver {
77+
return async (options: ContentScriptOptions) => {
78+
if (typeof marker === "function") {
79+
marker = await marker(options);
80+
}
81+
82+
if (!marker) {
83+
marker = ContentScriptMarker.Attribute;
84+
}
85+
86+
if (typeof marker === "string") {
87+
return new AttributeMarker();
88+
}
89+
90+
return marker;
91+
};
92+
}
93+
94+
protected resolveAnchor(anchor?: ContentScriptAnchor | ContentScriptAnchorGetter): ContentScriptAnchorGetter {
6895
return contentScriptAnchorResolver(anchor);
6996
}
7097

@@ -105,7 +132,9 @@ export default abstract class extends Builder implements ContentScriptBuilder {
105132
public async build(): Promise<void> {
106133
await this.destroy();
107134

108-
const {render, main, anchor, container, watch, mount, ...options} = this.definition;
135+
const {render, main, anchor, marker, container, watch, mount, ...options} = this.definition;
136+
137+
this.marker = await marker(options);
109138

110139
await main?.(this.context, options);
111140

@@ -123,13 +152,17 @@ export default abstract class extends Builder implements ContentScriptBuilder {
123152
public async destroy(): Promise<void> {
124153
this.unwatch?.();
125154
this.unwatch = undefined;
155+
156+
this.marker.reset();
126157
}
127158

128159
protected async processing(): Promise<void> {
129160
await this.lock.acquireAsync();
130161

131162
try {
132-
const anchors = await this.definition.anchor();
163+
const anchor = await this.definition.anchor();
164+
165+
const anchors = this.marker.for(anchor).pending();
133166

134167
await Promise.allSettled(anchors.map(this.processAnchor.bind(this)));
135168
} finally {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {ContentScriptMarkerContract, ContentScriptNode} from "@typing/content";
2+
3+
export default class implements ContentScriptNode {
4+
constructor(
5+
protected readonly node: ContentScriptNode,
6+
protected readonly marker: ContentScriptMarkerContract
7+
) {}
8+
9+
public get anchor(): Element {
10+
return this.node.anchor;
11+
}
12+
13+
public get container(): Element | undefined {
14+
return this.node.container;
15+
}
16+
17+
public mount(): boolean | undefined | void {
18+
this.marker.mount(this.anchor);
19+
20+
return this.node.mount();
21+
}
22+
23+
public unmount(): boolean | undefined | void {
24+
this.marker.unmount(this.anchor);
25+
26+
return this.node.unmount();
27+
}
28+
}

src/entry/content/core/MountBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Builder from "./Builder";
22
import Node from "./Node";
33
import MountNode from "./MountNode";
4+
import MarkerNode from "./MarkerNode";
45

56
import {ContentScriptNode, ContentScriptProps, ContentScriptRenderValue} from "@typing/content";
67

@@ -42,7 +43,7 @@ export default abstract class extends Builder {
4243
container = (await this.definition.container(this.getProps(anchor))) as Element | undefined;
4344
}
4445

45-
return new MountNode(new Node(anchor, container), this.definition.mount);
46+
return new MountNode(new MarkerNode(new Node(anchor, container), this.marker), this.definition.mount);
4647
}
4748

4849
protected cleanupNode(anchor: Element): void {

src/entry/content/core/Node.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
1-
import {contentScriptAnchorAttribute} from "./resolvers";
2-
31
import {ContentScriptNode} from "@typing/content";
42

5-
enum NodeMark {
6-
Mounded = "1",
7-
Unmounted = "0",
8-
}
9-
103
export default class implements ContentScriptNode {
114
private readonly _container?: Element;
125

13-
private readonly attr = contentScriptAnchorAttribute;
14-
156
constructor(
167
public readonly anchor: Element,
178
public container?: Element
@@ -22,8 +13,6 @@ export default class implements ContentScriptNode {
2213
}
2314

2415
public mount(): boolean {
25-
this.mark();
26-
2716
if (!this.container && this._container) {
2817
this.container = this._container.cloneNode(false) as Element;
2918

@@ -34,8 +23,6 @@ export default class implements ContentScriptNode {
3423
}
3524

3625
public unmount(): boolean {
37-
this.unmark();
38-
3926
if (this.container) {
4027
this.container.remove();
4128
this.container = undefined;
@@ -45,18 +32,4 @@ export default class implements ContentScriptNode {
4532

4633
return false;
4734
}
48-
49-
protected mark(): this {
50-
let id = this.anchor.getAttribute(this.attr);
51-
52-
if (typeof id !== "string" || id.length === 0 || id === NodeMark.Unmounted) {
53-
this.anchor.setAttribute(this.attr, NodeMark.Mounded);
54-
}
55-
56-
return this;
57-
}
58-
59-
protected unmark(): void {
60-
this.anchor.setAttribute(this.attr, NodeMark.Unmounted);
61-
}
6235
}

0 commit comments

Comments
 (0)