add support for styles
This commit is contained in:
@@ -1550,6 +1550,193 @@ describe('MCP Server Tests', () => {
|
|||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
|
it('should get styles for element using getStylesForLocator', async () => {
|
||||||
|
const browserContext = getBrowserContext()
|
||||||
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|
||||||
|
const page = await browserContext.newPage()
|
||||||
|
await page.setContent(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #333; }
|
||||||
|
.container { padding: 20px; margin: 10px; }
|
||||||
|
#main-btn { background-color: blue; color: white; border-radius: 4px; }
|
||||||
|
.btn { padding: 8px 16px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<button id="main-btn" class="btn" style="font-weight: bold;">Click Me</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
await page.bringToFront()
|
||||||
|
|
||||||
|
await serviceWorker.evaluate(async () => {
|
||||||
|
await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 400))
|
||||||
|
|
||||||
|
const stylesResult = await client.callTool({
|
||||||
|
name: 'execute',
|
||||||
|
arguments: {
|
||||||
|
code: js`
|
||||||
|
let testPage;
|
||||||
|
for (const p of context.pages()) {
|
||||||
|
const html = await p.content();
|
||||||
|
if (html.includes('main-btn')) { testPage = p; break; }
|
||||||
|
}
|
||||||
|
if (!testPage) throw new Error('Test page not found');
|
||||||
|
const btn = testPage.locator('#main-btn');
|
||||||
|
const styles = await getStylesForLocator({ locator: btn });
|
||||||
|
return styles;
|
||||||
|
`,
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(stylesResult.isError).toBeFalsy()
|
||||||
|
const stylesText = (stylesResult.content as any)[0]?.text || ''
|
||||||
|
expect(stylesText).toMatchInlineSnapshot(`
|
||||||
|
"Return value:
|
||||||
|
{
|
||||||
|
"element": "button#main-btn.btn",
|
||||||
|
"inlineStyle": {
|
||||||
|
"font-weight": "bold"
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"selector": ".btn",
|
||||||
|
"source": null,
|
||||||
|
"origin": "regular",
|
||||||
|
"declarations": {
|
||||||
|
"padding": "8px 16px",
|
||||||
|
"padding-top": "8px",
|
||||||
|
"padding-right": "16px",
|
||||||
|
"padding-bottom": "8px",
|
||||||
|
"padding-left": "16px"
|
||||||
|
},
|
||||||
|
"inheritedFrom": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "#main-btn",
|
||||||
|
"source": null,
|
||||||
|
"origin": "regular",
|
||||||
|
"declarations": {
|
||||||
|
"background-color": "blue",
|
||||||
|
"color": "white",
|
||||||
|
"border-radius": "4px",
|
||||||
|
"border-top-left-radius": "4px",
|
||||||
|
"border-top-right-radius": "4px",
|
||||||
|
"border-bottom-right-radius": "4px",
|
||||||
|
"border-bottom-left-radius": "4px"
|
||||||
|
},
|
||||||
|
"inheritedFrom": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": ".container",
|
||||||
|
"source": null,
|
||||||
|
"origin": "regular",
|
||||||
|
"declarations": {
|
||||||
|
"padding": "20px",
|
||||||
|
"margin": "10px",
|
||||||
|
"padding-top": "20px",
|
||||||
|
"padding-right": "20px",
|
||||||
|
"padding-bottom": "20px",
|
||||||
|
"padding-left": "20px",
|
||||||
|
"margin-top": "10px",
|
||||||
|
"margin-right": "10px",
|
||||||
|
"margin-bottom": "10px",
|
||||||
|
"margin-left": "10px"
|
||||||
|
},
|
||||||
|
"inheritedFrom": "ancestor[1]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "body",
|
||||||
|
"source": null,
|
||||||
|
"origin": "regular",
|
||||||
|
"declarations": {
|
||||||
|
"font-family": "Arial, sans-serif",
|
||||||
|
"color": "rgb(51, 51, 51)"
|
||||||
|
},
|
||||||
|
"inheritedFrom": "ancestor[2]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
|
||||||
|
const formattedResult = await client.callTool({
|
||||||
|
name: 'execute',
|
||||||
|
arguments: {
|
||||||
|
code: js`
|
||||||
|
let testPage;
|
||||||
|
for (const p of context.pages()) {
|
||||||
|
const html = await p.content();
|
||||||
|
if (html.includes('main-btn')) { testPage = p; break; }
|
||||||
|
}
|
||||||
|
if (!testPage) throw new Error('Test page not found');
|
||||||
|
const btn = testPage.locator('#main-btn');
|
||||||
|
const styles = await getStylesForLocator({ locator: btn });
|
||||||
|
return formatStylesAsText(styles);
|
||||||
|
`,
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(formattedResult.isError).toBeFalsy()
|
||||||
|
const formattedText = (formattedResult.content as any)[0]?.text || ''
|
||||||
|
expect(formattedText).toMatchInlineSnapshot(`
|
||||||
|
"Return value:
|
||||||
|
Element: button#main-btn.btn
|
||||||
|
|
||||||
|
Inline styles:
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
Matched rules:
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
#main-btn {
|
||||||
|
background-color: blue;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inherited from ancestor[1]:
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inherited from ancestor[2]:
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: rgb(51, 51, 51);
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
it('should return correct layout metrics via CDP', async () => {
|
it('should return correct layout metrics via CDP', async () => {
|
||||||
const browserContext = getBrowserContext()
|
const browserContext = getBrowserContext()
|
||||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from '
|
|||||||
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
||||||
import { Debugger } from './debugger.js'
|
import { Debugger } from './debugger.js'
|
||||||
import { Editor } from './editor.js'
|
import { Editor } from './editor.js'
|
||||||
|
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
|
||||||
|
|
||||||
class CodeExecutionTimeoutError extends Error {
|
class CodeExecutionTimeoutError extends Error {
|
||||||
constructor(timeout: number) {
|
constructor(timeout: number) {
|
||||||
@@ -77,6 +78,8 @@ interface VMContext {
|
|||||||
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
|
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
|
||||||
createDebugger: (options: { cdp: CDPSession }) => Debugger
|
createDebugger: (options: { cdp: CDPSession }) => Debugger
|
||||||
createEditor: (options: { cdp: CDPSession }) => Editor
|
createEditor: (options: { cdp: CDPSession }) => Editor
|
||||||
|
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
|
||||||
|
formatStylesAsText: (styles: StylesResult) => string
|
||||||
require: NodeRequire
|
require: NodeRequire
|
||||||
import: (specifier: string) => Promise<any>
|
import: (specifier: string) => Promise<any>
|
||||||
}
|
}
|
||||||
@@ -701,6 +704,11 @@ server.tool(
|
|||||||
return new Editor(options)
|
return new Editor(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStylesForLocatorFn = async (options: { locator: any }) => {
|
||||||
|
const cdp = await getCDPSession({ page: options.locator.page() })
|
||||||
|
return getStylesForLocator({ locator: options.locator, cdp })
|
||||||
|
}
|
||||||
|
|
||||||
let vmContextObj: VMContextWithGlobals = {
|
let vmContextObj: VMContextWithGlobals = {
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
@@ -714,6 +722,8 @@ server.tool(
|
|||||||
getCDPSession,
|
getCDPSession,
|
||||||
createDebugger,
|
createDebugger,
|
||||||
createEditor,
|
createEditor,
|
||||||
|
getStylesForLocator: getStylesForLocatorFn,
|
||||||
|
formatStylesAsText,
|
||||||
resetPlaywright: async () => {
|
resetPlaywright: async () => {
|
||||||
const { page: newPage, context: newContext } = await resetConnection()
|
const { page: newPage, context: newContext } = await resetConnection()
|
||||||
|
|
||||||
@@ -730,6 +740,8 @@ server.tool(
|
|||||||
getCDPSession,
|
getCDPSession,
|
||||||
createDebugger,
|
createDebugger,
|
||||||
createEditor,
|
createEditor,
|
||||||
|
getStylesForLocator: getStylesForLocatorFn,
|
||||||
|
formatStylesAsText,
|
||||||
resetPlaywright: vmContextObj.resetPlaywright,
|
resetPlaywright: vmContextObj.resetPlaywright,
|
||||||
require,
|
require,
|
||||||
// TODO --experimental-vm-modules is needed to make import work in vm
|
// TODO --experimental-vm-modules is needed to make import work in vm
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import type { CDPSession } from './cdp-session.js'
|
||||||
|
import type { Locator } from 'playwright-core'
|
||||||
|
|
||||||
|
export interface StyleSource {
|
||||||
|
url: string
|
||||||
|
line: number
|
||||||
|
column: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StyleDeclarations = Record<string, string>
|
||||||
|
|
||||||
|
export interface StyleRule {
|
||||||
|
selector: string
|
||||||
|
source: StyleSource | null
|
||||||
|
origin: 'regular' | 'user-agent' | 'injected' | 'inspector'
|
||||||
|
declarations: StyleDeclarations
|
||||||
|
inheritedFrom: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StylesResult {
|
||||||
|
element: string
|
||||||
|
inlineStyle: StyleDeclarations | null
|
||||||
|
rules: StyleRule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSSProperty {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
important?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSSStyle {
|
||||||
|
cssProperties: CSSProperty[]
|
||||||
|
cssText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSSRule {
|
||||||
|
selectorList: { text: string }
|
||||||
|
style: CSSStyle
|
||||||
|
styleSheetId?: string
|
||||||
|
origin: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleMatch {
|
||||||
|
rule: CSSRule
|
||||||
|
matchingSelectors: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InheritedStyleEntry {
|
||||||
|
inlineStyle?: CSSStyle
|
||||||
|
matchedCSSRules: RuleMatch[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourceRange {
|
||||||
|
startLine: number
|
||||||
|
startColumn: number
|
||||||
|
endLine: number
|
||||||
|
endColumn: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSSStyleSheetHeader {
|
||||||
|
styleSheetId: string
|
||||||
|
sourceURL?: string
|
||||||
|
origin: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStylesForLocator({
|
||||||
|
locator,
|
||||||
|
cdp,
|
||||||
|
includeUserAgentStyles = false,
|
||||||
|
}: {
|
||||||
|
locator: Locator
|
||||||
|
cdp: CDPSession
|
||||||
|
includeUserAgentStyles?: boolean
|
||||||
|
}): Promise<StylesResult> {
|
||||||
|
await cdp.send('DOM.enable')
|
||||||
|
await cdp.send('CSS.enable')
|
||||||
|
|
||||||
|
const elementHandle = await locator.elementHandle()
|
||||||
|
if (!elementHandle) {
|
||||||
|
throw new Error('Could not get element handle from locator')
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteObject = (elementHandle as any)._channel?.objectId
|
||||||
|
? { objectId: (elementHandle as any)._channel.objectId }
|
||||||
|
: null
|
||||||
|
|
||||||
|
let backendNodeId: number
|
||||||
|
if (remoteObject?.objectId) {
|
||||||
|
const nodeInfo = await cdp.send('DOM.describeNode', {
|
||||||
|
objectId: remoteObject.objectId,
|
||||||
|
})
|
||||||
|
backendNodeId = nodeInfo.node.backendNodeId
|
||||||
|
} else {
|
||||||
|
const box = await elementHandle.boundingBox()
|
||||||
|
if (!box) {
|
||||||
|
throw new Error('Element has no bounding box')
|
||||||
|
}
|
||||||
|
const docResult = await cdp.send('DOM.getDocument', { depth: 0 })
|
||||||
|
const nodeAtPoint = await cdp.send('DOM.getNodeForLocation', {
|
||||||
|
x: Math.round(box.x + box.width / 2),
|
||||||
|
y: Math.round(box.y + box.height / 2),
|
||||||
|
})
|
||||||
|
backendNodeId = nodeAtPoint.backendNodeId
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushResult = await cdp.send('DOM.pushNodesByBackendIdsToFrontend', {
|
||||||
|
backendNodeIds: [backendNodeId],
|
||||||
|
})
|
||||||
|
const nodeId = pushResult.nodeIds[0]
|
||||||
|
|
||||||
|
if (!nodeId) {
|
||||||
|
throw new Error('Could not get nodeId for element')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = await cdp.send('DOM.describeNode', { nodeId })
|
||||||
|
const elementDescription = formatElementDescription(nodeInfo.node)
|
||||||
|
|
||||||
|
const matchedStyles = await cdp.send('CSS.getMatchedStylesForNode', { nodeId })
|
||||||
|
|
||||||
|
const stylesheetUrls = new Map<string, string>()
|
||||||
|
|
||||||
|
const processStyleSheetId = async (styleSheetId: string | undefined): Promise<StyleSource | null> => {
|
||||||
|
if (!styleSheetId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stylesheetUrls.has(styleSheetId)) {
|
||||||
|
try {
|
||||||
|
const header = await cdp.send('CSS.getStyleSheetText', { styleSheetId })
|
||||||
|
stylesheetUrls.set(styleSheetId, '')
|
||||||
|
} catch {
|
||||||
|
stylesheetUrls.set(styleSheetId, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules: StyleRule[] = []
|
||||||
|
|
||||||
|
if (matchedStyles.matchedCSSRules) {
|
||||||
|
for (const ruleMatch of matchedStyles.matchedCSSRules) {
|
||||||
|
const rule = ruleMatch.rule
|
||||||
|
const sourceRange = (rule as any).selectorList?.range as SourceRange | undefined
|
||||||
|
const styleSheetId = rule.styleSheetId
|
||||||
|
|
||||||
|
let source: StyleSource | null = null
|
||||||
|
if (styleSheetId && sourceRange) {
|
||||||
|
const styleSheet = (matchedStyles as any).cssStyleSheetHeaders?.find(
|
||||||
|
(h: CSSStyleSheetHeader) => h.styleSheetId === styleSheetId
|
||||||
|
)
|
||||||
|
const url = styleSheet?.sourceURL || (rule as any).origin === 'user-agent' ? 'user-agent' : `stylesheet:${styleSheetId}`
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url: (rule as any).styleSheetId ? await getStylesheetUrl(cdp, styleSheetId) : 'user-agent',
|
||||||
|
line: sourceRange.startLine + 1,
|
||||||
|
column: sourceRange.startColumn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.push({
|
||||||
|
selector: rule.selectorList.text,
|
||||||
|
source,
|
||||||
|
origin: rule.origin as StyleRule['origin'],
|
||||||
|
declarations: extractDeclarations(rule.style),
|
||||||
|
inheritedFrom: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedStyles.inherited) {
|
||||||
|
for (let i = 0; i < matchedStyles.inherited.length; i++) {
|
||||||
|
const inheritedEntry = matchedStyles.inherited[i] as InheritedStyleEntry
|
||||||
|
const ancestorDesc = `ancestor[${i + 1}]`
|
||||||
|
|
||||||
|
if (inheritedEntry.inlineStyle) {
|
||||||
|
const declarations = extractDeclarations(inheritedEntry.inlineStyle)
|
||||||
|
if (Object.keys(declarations).length > 0) {
|
||||||
|
rules.push({
|
||||||
|
selector: 'element.style',
|
||||||
|
source: null,
|
||||||
|
origin: 'regular',
|
||||||
|
declarations,
|
||||||
|
inheritedFrom: ancestorDesc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ruleMatch of inheritedEntry.matchedCSSRules) {
|
||||||
|
const rule = ruleMatch.rule
|
||||||
|
const sourceRange = (rule as any).selectorList?.range as SourceRange | undefined
|
||||||
|
const styleSheetId = rule.styleSheetId
|
||||||
|
|
||||||
|
let source: StyleSource | null = null
|
||||||
|
if (styleSheetId && sourceRange) {
|
||||||
|
source = {
|
||||||
|
url: await getStylesheetUrl(cdp, styleSheetId),
|
||||||
|
line: sourceRange.startLine + 1,
|
||||||
|
column: sourceRange.startColumn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const declarations = extractDeclarations(rule.style)
|
||||||
|
if (Object.keys(declarations).length > 0) {
|
||||||
|
rules.push({
|
||||||
|
selector: rule.selectorList.text,
|
||||||
|
source,
|
||||||
|
origin: rule.origin as StyleRule['origin'],
|
||||||
|
declarations,
|
||||||
|
inheritedFrom: ancestorDesc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inlineStyle: StyleDeclarations | null = null
|
||||||
|
if (matchedStyles.inlineStyle) {
|
||||||
|
const declarations = extractDeclarations(matchedStyles.inlineStyle as CSSStyle)
|
||||||
|
if (Object.keys(declarations).length > 0) {
|
||||||
|
inlineStyle = declarations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRules = includeUserAgentStyles
|
||||||
|
? rules
|
||||||
|
: rules.filter((r) => r.origin !== 'user-agent')
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: elementDescription,
|
||||||
|
inlineStyle,
|
||||||
|
rules: filteredRules,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDeclarations(style: CSSStyle): StyleDeclarations {
|
||||||
|
if (!style?.cssProperties) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: StyleDeclarations = {}
|
||||||
|
for (const prop of style.cssProperties) {
|
||||||
|
if (!prop.value || prop.value === 'initial' || prop.name.startsWith('-webkit-')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const value = prop.important ? `${prop.value} !important` : prop.value
|
||||||
|
result[prop.name] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatElementDescription(node: any): string {
|
||||||
|
let desc = node.localName || node.nodeName?.toLowerCase() || 'element'
|
||||||
|
|
||||||
|
if (node.attributes) {
|
||||||
|
const attrs: Record<string, string> = {}
|
||||||
|
for (let i = 0; i < node.attributes.length; i += 2) {
|
||||||
|
attrs[node.attributes[i]] = node.attributes[i + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.id) {
|
||||||
|
desc += `#${attrs.id}`
|
||||||
|
}
|
||||||
|
if (attrs.class) {
|
||||||
|
desc += `.${attrs.class.split(' ').join('.')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStylesheetUrl(cdp: CDPSession, styleSheetId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
await cdp.send('CSS.getStyleSheetText', { styleSheetId })
|
||||||
|
return `stylesheet:${styleSheetId}`
|
||||||
|
} catch {
|
||||||
|
return `stylesheet:${styleSheetId}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatStylesAsText(styles: StylesResult): string {
|
||||||
|
const lines: string[] = []
|
||||||
|
|
||||||
|
lines.push(`Element: ${styles.element}`)
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
if (styles.inlineStyle) {
|
||||||
|
lines.push('Inline styles:')
|
||||||
|
for (const [prop, value] of Object.entries(styles.inlineStyle)) {
|
||||||
|
lines.push(` ${prop}: ${value}`)
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const directRules = styles.rules.filter((r) => !r.inheritedFrom)
|
||||||
|
const inheritedRules = styles.rules.filter((r) => r.inheritedFrom)
|
||||||
|
|
||||||
|
if (directRules.length > 0) {
|
||||||
|
lines.push('Matched rules:')
|
||||||
|
for (const rule of directRules) {
|
||||||
|
lines.push(` ${rule.selector} {`)
|
||||||
|
const sourceInfo = rule.source ? ` /* ${rule.source.url}:${rule.source.line}:${rule.source.column} */` : ''
|
||||||
|
if (sourceInfo) {
|
||||||
|
lines.push(` ${sourceInfo}`)
|
||||||
|
}
|
||||||
|
for (const [prop, value] of Object.entries(rule.declarations)) {
|
||||||
|
lines.push(` ${prop}: ${value};`)
|
||||||
|
}
|
||||||
|
lines.push(' }')
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inheritedRules.length > 0) {
|
||||||
|
const byAncestor = new Map<string, StyleRule[]>()
|
||||||
|
for (const rule of inheritedRules) {
|
||||||
|
const key = rule.inheritedFrom!
|
||||||
|
if (!byAncestor.has(key)) {
|
||||||
|
byAncestor.set(key, [])
|
||||||
|
}
|
||||||
|
byAncestor.get(key)!.push(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [ancestor, rules] of byAncestor) {
|
||||||
|
lines.push(`Inherited from ${ancestor}:`)
|
||||||
|
for (const rule of rules) {
|
||||||
|
lines.push(` ${rule.selector} {`)
|
||||||
|
const sourceInfo = rule.source ? ` /* ${rule.source.url}:${rule.source.line}:${rule.source.column} */` : ''
|
||||||
|
if (sourceInfo) {
|
||||||
|
lines.push(` ${sourceInfo}`)
|
||||||
|
}
|
||||||
|
for (const [prop, value] of Object.entries(rule.declarations)) {
|
||||||
|
lines.push(` ${prop}: ${value};`)
|
||||||
|
}
|
||||||
|
lines.push(' }')
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user