Skip to content

Commit 8d4f81b

Browse files
committed
improve API, error handling, and more
1 parent 230781a commit 8d4f81b

File tree

6 files changed

+259
-101
lines changed

6 files changed

+259
-101
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ You can use this information to build your prompt for the vision-language model.
3232
Example prompt:
3333

3434
```javascript
35-
let markedElements = await mark();
35+
let markedElements = mark();
3636

3737
let prompt = `The following is a screenshot of a web page.
3838
@@ -148,7 +148,7 @@ Only mark elements that are visible in the current viewport.
148148
### Advanced example
149149

150150
```typescript
151-
const markedElements = await mark({
151+
const markedElements = mark({
152152
// Only mark buttons and inputs
153153
selector: "button, input",
154154
// Use test id attribute for marker labels

docs/pages/index.mdx

+4-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ You can use this information to build your prompt for the large language model.
4141
Example prompt:
4242

4343
```javascript
44-
let markedElements = await mark();
44+
let markedElements = mark();
4545

4646
let prompt = `The following is a screenshot of a web page.
4747
@@ -114,7 +114,7 @@ await page.evaluate(async () => await WebMarker.unmark());
114114
Marks the page and returns the marked elements.
115115

116116
```typescript
117-
const markedElements = await mark();
117+
const markedElements = mark();
118118

119119
// markedElements["0"].element returns the first marked element
120120
// markedElements["0"].markElement returns the label element for the first marked element
@@ -191,7 +191,7 @@ Only mark elements that are visible in the current viewport.
191191
#### Advanced example
192192

193193
```typescript
194-
const markedElements = await mark({
194+
const markedElements = mark({
195195
// Only mark buttons and inputs
196196
selector: "button, input",
197197
// Use test id attribute for marker labels
@@ -228,5 +228,5 @@ const isMarked = await isMarked();
228228
Unmark the page.
229229

230230
```typescript
231-
await unmark();
231+
unmark();
232232
```

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@mdx-js/react": "^3.0.1",
2727
"@next/mdx": "^14.2.3",
2828
"@playwright/test": "^1.46.1",
29+
"@types/jest": "^29.5.13",
2930
"@types/mdx": "^2.0.13",
3031
"@types/node": "^22.5.1",
3132
"autoprefixer": "^10.4.19",

src/index.ts

+114-78
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ type Placement =
1414
| "left-start"
1515
| "left-end";
1616

17-
type StyleFunction = (element: Element) => Partial<CSSStyleDeclaration>;
17+
type StyleFunction = (
18+
element: Element,
19+
index: number
20+
) => Partial<CSSStyleDeclaration>;
1821
type StyleObject = Partial<CSSStyleDeclaration>;
1922

2023
interface MarkOptions {
@@ -67,6 +70,14 @@ interface MarkOptions {
6770
* @default false
6871
*/
6972
viewPortOnly?: boolean;
73+
/**
74+
* Additional class to apply to the mark elements.
75+
*/
76+
markClass?: string;
77+
/**
78+
* Additional class to apply to the bounding box elements.
79+
*/
80+
boundingBoxClass?: string;
7081
}
7182

7283
interface MarkedElement {
@@ -77,134 +88,150 @@ interface MarkedElement {
7788

7889
let cleanupFns: (() => void)[] = [];
7990

80-
async function mark(
81-
options: MarkOptions = {}
82-
): Promise<Record<string, MarkedElement>> {
83-
const {
84-
selector = "button, input, a, select, textarea",
85-
getLabel = (_, index) => index.toString(),
86-
markAttribute = "data-mark-label",
87-
markPlacement = "top-start",
88-
markStyle = {
89-
backgroundColor: "red",
90-
color: "white",
91-
padding: "2px 4px",
92-
fontSize: "12px",
93-
fontWeight: "bold",
94-
},
95-
boundingBoxStyle = {
96-
outline: "2px dashed red",
97-
backgroundColor: "transparent",
98-
},
99-
showBoundingBoxes = true,
100-
containerElement = document.body,
101-
viewPortOnly = false,
102-
} = options;
103-
104-
const elements = Array.from(
105-
containerElement.querySelectorAll(selector)
106-
).filter(
107-
(el) => !viewPortOnly || el.getBoundingClientRect().top < window.innerHeight
108-
);
91+
function mark(options: MarkOptions = {}): Record<string, MarkedElement> {
92+
try {
93+
const {
94+
selector = "button, input, a, select, textarea",
95+
getLabel = (_, index) => index.toString(),
96+
markAttribute = "data-mark-label",
97+
markPlacement = "top-start",
98+
markStyle = {
99+
backgroundColor: "red",
100+
color: "white",
101+
padding: "2px 4px",
102+
fontSize: "12px",
103+
fontWeight: "bold",
104+
},
105+
boundingBoxStyle = {
106+
outline: "2px dashed red",
107+
backgroundColor: "transparent",
108+
},
109+
showBoundingBoxes = true,
110+
containerElement = document.body,
111+
viewPortOnly = false,
112+
markClass = "",
113+
boundingBoxClass = "",
114+
} = options;
115+
116+
const isInViewport = (el: Element) => {
117+
const rect = el.getBoundingClientRect();
118+
return rect.top < window.innerHeight && rect.bottom >= 0;
119+
};
120+
121+
const elements = Array.from(
122+
containerElement.querySelectorAll(selector)
123+
).filter((el) => !viewPortOnly || isInViewport(el));
109124

110-
const markedElements: Record<string, MarkedElement> = {};
125+
const markedElements: Record<string, MarkedElement> = {};
126+
const fragment = document.createDocumentFragment();
111127

112-
await Promise.all(
113-
elements.map(async (element, index) => {
128+
elements.forEach((element, index) => {
114129
const label = getLabel(element, index);
115-
const markElement = createMark(element, markStyle, label, markPlacement);
130+
const markElement = createMark(
131+
element,
132+
index,
133+
markStyle,
134+
label,
135+
markPlacement,
136+
markClass
137+
);
138+
fragment.appendChild(markElement);
116139

117140
const boundingBoxElement = showBoundingBoxes
118-
? createBoundingBox(element, boundingBoxStyle, label)
141+
? createBoundingBox(element, boundingBoxStyle, label, boundingBoxClass)
119142
: undefined;
143+
if (boundingBoxElement) {
144+
fragment.appendChild(boundingBoxElement);
145+
}
120146

121147
markedElements[label] = { element, markElement, boundingBoxElement };
122148
element.setAttribute(markAttribute, label);
123-
})
124-
);
149+
});
125150

126-
document.documentElement.setAttribute("data-marked", "true");
127-
return markedElements;
151+
document.body.appendChild(fragment);
152+
document.documentElement.setAttribute("data-marked", "true");
153+
return markedElements;
154+
} catch (error) {
155+
console.error("Error in mark function:", error);
156+
throw error;
157+
}
128158
}
129159

130160
function createMark(
131161
element: Element,
162+
index: number,
132163
style: StyleObject | StyleFunction,
133164
label: string,
134-
markPlacement: Placement = "top-start"
165+
markPlacement: Placement = "top-start",
166+
markClass: string
135167
): HTMLElement {
136168
const markElement = document.createElement("div");
137-
markElement.className = "webmarker";
169+
markElement.className = `webmarker ${markClass}`.trim();
138170
markElement.id = `webmarker-${label}`;
139171
markElement.textContent = label;
140-
document.body.appendChild(markElement);
141-
positionMark(markElement, element, markPlacement);
172+
markElement.setAttribute("aria-hidden", "true");
173+
positionElement(markElement, element, markPlacement, (x, y) => {
174+
Object.assign(markElement.style, {
175+
left: `${x}px`,
176+
top: `${y}px`,
177+
});
178+
});
142179
applyStyle(
143180
markElement,
144181
{
145182
zIndex: "999999999",
146183
position: "absolute",
147184
pointerEvents: "none",
148185
},
149-
typeof style === "function" ? style(element) : style
186+
typeof style === "function" ? style(element, index) : style
150187
);
151188
return markElement;
152189
}
153190

154191
function createBoundingBox(
155192
element: Element,
156193
style: StyleObject | StyleFunction,
157-
label: string
194+
label: string,
195+
boundingBoxClass: string
158196
): HTMLElement {
159197
const boundingBoxElement = document.createElement("div");
160-
boundingBoxElement.className = "webmarker-bounding-box";
198+
boundingBoxElement.className =
199+
`webmarker-bounding-box ${boundingBoxClass}`.trim();
161200
boundingBoxElement.id = `webmarker-bounding-box-${label}`;
162-
document.body.appendChild(boundingBoxElement);
163-
positionBoundingBox(boundingBoxElement, element);
201+
boundingBoxElement.setAttribute("aria-hidden", "true");
202+
positionElement(boundingBoxElement, element, "top-start", (x, y) => {
203+
const { width, height } = element.getBoundingClientRect();
204+
Object.assign(boundingBoxElement.style, {
205+
left: `${x}px`,
206+
top: `${y}px`,
207+
width: `${width}px`,
208+
height: `${height}px`,
209+
});
210+
});
164211
applyStyle(
165212
boundingBoxElement,
166213
{
167214
zIndex: "999999999",
168215
position: "absolute",
169216
pointerEvents: "none",
170217
},
171-
typeof style === "function" ? style(element) : style
218+
typeof style === "function" ? style(element, parseInt(label)) : style
172219
);
173220
return boundingBoxElement;
174221
}
175222

176-
function positionMark(
177-
markElement: HTMLElement,
178-
element: Element,
179-
markPlacement: Placement
223+
function positionElement(
224+
target: HTMLElement,
225+
anchor: Element,
226+
placement: Placement,
227+
updateCallback: (x: number, y: number) => void
180228
) {
181-
async function updatePosition() {
182-
const { x, y } = await computePosition(element, markElement, {
183-
placement: markPlacement,
184-
});
185-
Object.assign(markElement.style, {
186-
left: `${x}px`,
187-
top: `${y}px`,
229+
function updatePosition() {
230+
computePosition(anchor, target, { placement }).then(({ x, y }) => {
231+
updateCallback(x, y);
188232
});
189233
}
190-
191-
cleanupFns.push(autoUpdate(element, markElement, updatePosition));
192-
}
193-
194-
async function positionBoundingBox(boundingBox: HTMLElement, element: Element) {
195-
const { width, height } = element.getBoundingClientRect();
196-
async function updatePosition() {
197-
const { x, y } = await computePosition(element, boundingBox, {
198-
placement: "top-start",
199-
});
200-
Object.assign(boundingBox.style, {
201-
left: `${x}px`,
202-
top: `${y + height}px`,
203-
width: `${width}px`,
204-
height: `${height}px`,
205-
});
206-
}
207-
cleanupFns.push(autoUpdate(element, boundingBox, updatePosition));
234+
cleanupFns.push(autoUpdate(anchor, target, updatePosition));
208235
}
209236

210237
function applyStyle(
@@ -216,6 +243,15 @@ function applyStyle(
216243
}
217244

218245
function unmark(): void {
246+
const markAttribute = document
247+
.querySelector("[data-mark-label]")
248+
?.getAttribute("data-mark-label")
249+
? "data-mark-label"
250+
: "data-webmarker-label";
251+
252+
document.querySelectorAll(`[${markAttribute}]`).forEach((el) => {
253+
el.removeAttribute(markAttribute);
254+
});
219255
document
220256
.querySelectorAll(".webmarker, .webmarker-bounding-box")
221257
.forEach((el) => el.remove());

0 commit comments

Comments
 (0)