Skip to content

Commit 8c70382

Browse files
committed
feat(extension): capture text variants and full computed styles in element serializer
1 parent 021f212 commit 8c70382

File tree

2 files changed

+146
-21
lines changed

2 files changed

+146
-21
lines changed

packages/chrome-extension/src/utils/element.ts

Lines changed: 140 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@
22
/* eslint-disable no-underscore-dangle */
33

44
import {
5-
ComponentInfo, CSSProperties, ElementPosition, TargetedElement,
5+
ComponentInfo,
6+
CSSDetailLevel,
7+
CSSProperties,
8+
DEFAULT_CSS_LEVEL,
9+
DEFAULT_TEXT_DETAIL,
10+
ElementPosition,
11+
TargetedElement,
12+
TextDetailLevel,
13+
TextSnapshots,
614
} from '@mcp-pointer/shared/types';
15+
import { CSS_LEVEL_FIELD_MAP } from '@mcp-pointer/shared/detail';
716
import logger from './logger';
817

918
export interface ReactSourceInfo {
@@ -12,6 +21,105 @@ export interface ReactSourceInfo {
1221
columnNumber?: number;
1322
}
1423

24+
export interface ElementSerializationOptions {
25+
textDetail?: TextDetailLevel;
26+
cssLevel?: CSSDetailLevel;
27+
}
28+
29+
function toKebabCase(property: string): string {
30+
return property
31+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
32+
.replace(/_/g, '-')
33+
.toLowerCase();
34+
}
35+
36+
function toCamelCase(property: string): string {
37+
return property
38+
.replace(/^-+/, '')
39+
.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());
40+
}
41+
42+
function getStyleValue(style: CSSStyleDeclaration, property: string): string | undefined {
43+
const camelValue = (style as any)[property];
44+
if (typeof camelValue === 'string' && camelValue.trim().length > 0) {
45+
return camelValue;
46+
}
47+
48+
const kebab = toKebabCase(property);
49+
const value = style.getPropertyValue(kebab);
50+
if (typeof value === 'string' && value.trim().length > 0) {
51+
return value;
52+
}
53+
54+
return undefined;
55+
}
56+
57+
function extractFullCSSProperties(style: CSSStyleDeclaration): Record<string, string> {
58+
const properties: Record<string, string> = {};
59+
60+
for (let i = 0; i < style.length; i += 1) {
61+
const property = style.item(i);
62+
63+
if (property && !property.startsWith('-')) {
64+
const value = style.getPropertyValue(property);
65+
if (typeof value === 'string' && value.trim().length > 0) {
66+
const camel = toCamelCase(property);
67+
properties[camel] = value;
68+
}
69+
}
70+
}
71+
72+
return properties;
73+
}
74+
75+
function getElementCSSProperties(
76+
style: CSSStyleDeclaration,
77+
cssLevel: CSSDetailLevel,
78+
fullCSS: Record<string, string>,
79+
): CSSProperties | undefined {
80+
if (cssLevel === 0) {
81+
return undefined;
82+
}
83+
84+
if (cssLevel === 3) {
85+
return fullCSS;
86+
}
87+
88+
const fields = CSS_LEVEL_FIELD_MAP[cssLevel];
89+
const properties: CSSProperties = {};
90+
91+
fields.forEach((property) => {
92+
const value = getStyleValue(style, property);
93+
if (value !== undefined) {
94+
properties[property] = value;
95+
}
96+
});
97+
98+
return properties;
99+
}
100+
101+
function collectTextVariants(element: HTMLElement): TextSnapshots {
102+
const visible = element.innerText || '';
103+
const full = element.textContent || visible;
104+
105+
return {
106+
visible,
107+
full,
108+
};
109+
}
110+
111+
function resolveTextByDetail(variants: TextSnapshots, detail: TextDetailLevel): string | undefined {
112+
if (detail === 'none') {
113+
return undefined;
114+
}
115+
116+
if (detail === 'visible') {
117+
return variants.visible;
118+
}
119+
120+
return variants.full || variants.visible;
121+
}
122+
15123
/**
16124
* Get source file information from a DOM element's React component
17125
*/
@@ -172,20 +280,6 @@ export function getElementPosition(element: HTMLElement): ElementPosition {
172280
};
173281
}
174282

175-
/**
176-
* Extract relevant CSS properties from an element
177-
*/
178-
export function getElementCSSProperties(element: HTMLElement): CSSProperties {
179-
const computedStyle = window.getComputedStyle(element);
180-
return {
181-
display: computedStyle.display,
182-
position: computedStyle.position,
183-
fontSize: computedStyle.fontSize,
184-
color: computedStyle.color,
185-
backgroundColor: computedStyle.backgroundColor,
186-
};
187-
}
188-
189283
/**
190284
* Extract CSS classes from an element as an array
191285
*/
@@ -197,18 +291,45 @@ export function getElementClasses(element: HTMLElement): string[] {
197291
return classNameStr.split(' ').filter((c: string) => c.trim());
198292
}
199293

200-
export function adaptTargetToElement(element: HTMLElement): TargetedElement {
201-
return {
294+
export function adaptTargetToElement(
295+
element: HTMLElement,
296+
options: ElementSerializationOptions = {},
297+
): TargetedElement {
298+
const textDetail = options.textDetail ?? DEFAULT_TEXT_DETAIL;
299+
const cssLevel = options.cssLevel ?? DEFAULT_CSS_LEVEL;
300+
301+
const textVariants = collectTextVariants(element);
302+
const resolvedText = resolveTextByDetail(textVariants, textDetail);
303+
304+
const computedStyle = window.getComputedStyle(element);
305+
const fullCSS = extractFullCSSProperties(computedStyle);
306+
const cssProperties = getElementCSSProperties(computedStyle, cssLevel, fullCSS);
307+
308+
const target: TargetedElement = {
202309
selector: generateSelector(element),
203310
tagName: element.tagName,
204311
id: element.id || undefined,
205312
classes: getElementClasses(element),
206-
innerText: element.innerText || element.textContent || '',
207313
attributes: getElementAttributes(element),
208314
position: getElementPosition(element),
209-
cssProperties: getElementCSSProperties(element),
315+
cssLevel,
316+
cssProperties,
317+
cssComputed: Object.keys(fullCSS).length > 0 ? fullCSS : undefined,
210318
componentInfo: getReactFiberInfo(element),
211319
timestamp: Date.now(),
212320
url: window.location.href,
321+
textDetail,
322+
textVariants,
323+
textContent: textVariants.full,
213324
};
325+
326+
if (resolvedText !== undefined) {
327+
target.innerText = resolvedText;
328+
}
329+
330+
if (!target.textContent && textVariants.visible) {
331+
target.textContent = textVariants.visible;
332+
}
333+
334+
return target;
214335
}

packages/chrome-extension/src/utils/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ export interface TargetedElement {
55
tagName: string;
66
id?: string;
77
classes: string[];
8-
innerText: string;
8+
innerText?: string;
9+
textContent?: string;
10+
textDetail?: 'full' | 'visible' | 'none';
911
attributes: Record<string, string>;
1012
position: ElementPosition;
11-
cssProperties: CSSProperties;
13+
cssLevel?: 0 | 1 | 2 | 3;
14+
cssProperties?: CSSProperties;
15+
cssComputed?: Record<string, string>;
1216
componentInfo?: ComponentInfo;
1317
timestamp: number;
1418
url: string;

0 commit comments

Comments
 (0)