cssprobe-cli 0.2.0 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/browser/launcher.d.ts +5 -0
- package/dist/browser/launcher.js +4 -2
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.js +3 -1
- package/dist/cli/program.js +6 -9
- package/dist/config/config.d.ts +5 -0
- package/dist/config/config.js +14 -0
- package/dist/engine/renderer.js +12 -270
- package/dist/engine/renderers/ancestors.d.ts +2 -0
- package/dist/engine/renderers/ancestors.js +17 -0
- package/dist/engine/renderers/dom-tree.d.ts +2 -0
- package/dist/engine/renderers/dom-tree.js +43 -0
- package/dist/engine/renderers/findings.d.ts +3 -0
- package/dist/engine/renderers/findings.js +44 -0
- package/dist/engine/renderers/layout.d.ts +2 -0
- package/dist/engine/renderers/layout.js +203 -0
- package/dist/engine/renderers/sketch.d.ts +2 -0
- package/dist/engine/renderers/sketch.js +78 -0
- package/dist/engine/renderers/utils.d.ts +16 -0
- package/dist/engine/renderers/utils.js +54 -0
- package/dist/help.json +7 -5
- package/docs/guide/installation.md +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Options:
|
|
|
142
142
|
--brief Compact output: tree sketch + warnings/errors only
|
|
143
143
|
--layout ASCII layout diagram showing element positions and sizes
|
|
144
144
|
--wait Wait for user interaction before collecting (implies --headed)
|
|
145
|
+
--viewport <WxH> Viewport size (e.g. 375x812, default 1280x720)
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
---
|
|
@@ -189,6 +190,7 @@ The report header shows: `confidence: DEFINITE 8 | INDEFINITE 0 | UNVERIFIABLE 1
|
|
|
189
190
|
# Set values
|
|
190
191
|
cssprobe-cli config-set browser chromium
|
|
191
192
|
cssprobe-cli config-set depth 8
|
|
193
|
+
cssprobe-cli config-set viewport 375x812
|
|
192
194
|
|
|
193
195
|
# Show current config
|
|
194
196
|
cssprobe-cli config-show
|
|
@@ -206,6 +208,7 @@ Priority: CLI flags > Environment variables > Config file
|
|
|
206
208
|
--browser CSSPROBE_BROWSER
|
|
207
209
|
--depth CSSPROBE_DEPTH
|
|
208
210
|
--headed CSSPROBE_HEADED
|
|
211
|
+
--viewport CSSPROBE_VIEWPORT
|
|
209
212
|
-p, --profile CSSPROBE_PROFILE
|
|
210
213
|
```
|
|
211
214
|
|
|
@@ -4,6 +4,11 @@ export interface LauncherOptions {
|
|
|
4
4
|
headed?: boolean;
|
|
5
5
|
/** Path to a Playwright storage state file (cookies + localStorage). */
|
|
6
6
|
state?: string;
|
|
7
|
+
/** Custom viewport size. Defaults to 1280×720. */
|
|
8
|
+
viewport?: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
7
12
|
}
|
|
8
13
|
export declare class BrowserLauncher {
|
|
9
14
|
private options;
|
package/dist/browser/launcher.js
CHANGED
|
@@ -75,7 +75,8 @@ class BrowserLauncher {
|
|
|
75
75
|
}
|
|
76
76
|
this.browser = await factory.launch(launchOptions);
|
|
77
77
|
// Load saved state if provided
|
|
78
|
-
const
|
|
78
|
+
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
79
|
+
const contextOptions = { viewport: vp };
|
|
79
80
|
if (this.options.state && fs_1.default.existsSync(this.options.state)) {
|
|
80
81
|
contextOptions.storageState = this.options.state;
|
|
81
82
|
}
|
|
@@ -212,7 +213,8 @@ class BrowserLauncher {
|
|
|
212
213
|
if (execPath)
|
|
213
214
|
launchOptions.executablePath = execPath;
|
|
214
215
|
this.browser = await factory.launch(launchOptions);
|
|
215
|
-
|
|
216
|
+
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
217
|
+
this.context = await this.browser.newContext({ viewport: vp });
|
|
216
218
|
this.page = await this.context.newPage();
|
|
217
219
|
// Navigate to the target URL
|
|
218
220
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
24
24
|
brief: z.ZodOptional<z.ZodBoolean>;
|
|
25
25
|
layout: z.ZodOptional<z.ZodBoolean>;
|
|
26
26
|
wait: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
viewport: z.ZodOptional<z.ZodString>;
|
|
27
28
|
}, "strip", z.ZodTypeAny, {
|
|
28
29
|
json?: boolean | undefined;
|
|
29
30
|
headed?: boolean | undefined;
|
|
@@ -36,6 +37,7 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
36
37
|
brief?: boolean | undefined;
|
|
37
38
|
layout?: boolean | undefined;
|
|
38
39
|
wait?: boolean | undefined;
|
|
40
|
+
viewport?: string | undefined;
|
|
39
41
|
}, {
|
|
40
42
|
json?: boolean | undefined;
|
|
41
43
|
headed?: boolean | undefined;
|
|
@@ -48,4 +50,5 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
48
50
|
brief?: boolean | undefined;
|
|
49
51
|
layout?: boolean | undefined;
|
|
50
52
|
wait?: boolean | undefined;
|
|
53
|
+
viewport?: string | undefined;
|
|
51
54
|
}>>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -65,6 +65,7 @@ const inspect = (0, command_1.declareCommand)({
|
|
|
65
65
|
brief: z.boolean().optional().describe('compact output: tree sketch + warnings/errors only'),
|
|
66
66
|
layout: z.boolean().optional().describe('ASCII layout diagram showing element positions and sizes'),
|
|
67
67
|
wait: z.boolean().optional().describe('wait for user interaction before collecting (implies --headed)'),
|
|
68
|
+
viewport: z.string().optional().describe('viewport size as WxH (e.g. 375x812, default 1280x720)'),
|
|
68
69
|
}),
|
|
69
70
|
});
|
|
70
71
|
const login = (0, command_1.declareCommand)({
|
|
@@ -93,7 +94,7 @@ const configSet = (0, command_1.declareCommand)({
|
|
|
93
94
|
category: 'config',
|
|
94
95
|
description: 'Set a configuration value',
|
|
95
96
|
args: z.object({
|
|
96
|
-
key: z.string().describe('Config key (browser, depth, headed)'),
|
|
97
|
+
key: z.string().describe('Config key (browser, depth, headed, viewport)'),
|
|
97
98
|
value: z.string().describe('Config value'),
|
|
98
99
|
}),
|
|
99
100
|
options: z.object({
|
|
@@ -170,6 +171,7 @@ const interactive = (0, command_1.declareCommand)({
|
|
|
170
171
|
browser: z.enum(['chromium', 'firefox', 'webkit']).optional().describe('browser engine (default chromium)'),
|
|
171
172
|
state: z.string().optional().describe('path to saved state file (cookies + localStorage)'),
|
|
172
173
|
depth: numberArg.optional().describe('DOM tree depth (default: auto, max 20)'),
|
|
174
|
+
viewport: z.string().optional().describe('viewport size as WxH (e.g. 375x812, default 1280x720)'),
|
|
173
175
|
}),
|
|
174
176
|
});
|
|
175
177
|
const commandsArray = [
|
package/dist/cli/program.js
CHANGED
|
@@ -114,22 +114,18 @@ async function handleInspect(command, cmdEntry, args, output) {
|
|
|
114
114
|
}
|
|
115
115
|
const maxNodes = parsed['max-nodes'] || 60;
|
|
116
116
|
const upTo = parsed['up-to'] || 'html';
|
|
117
|
-
const
|
|
117
|
+
const wait = !!parsed.wait;
|
|
118
|
+
const headed = !!parsed.headed || config.headed || wait;
|
|
118
119
|
const browser = parsed.browser || config.browser;
|
|
119
120
|
const state = parsed.state || undefined;
|
|
120
121
|
const brief = !!parsed.brief;
|
|
121
122
|
const layout = !!parsed.layout;
|
|
122
|
-
const wait = !!parsed.wait;
|
|
123
|
-
// --wait implies --headed
|
|
124
|
-
if (wait) {
|
|
125
|
-
args.headed = true;
|
|
126
|
-
}
|
|
127
123
|
if (!url)
|
|
128
124
|
output.error('URL is required. Usage: cssprobe-cli inspect <url> [selector]');
|
|
129
125
|
// Auto-detect root selector if not provided
|
|
130
126
|
let rootSelector = selector;
|
|
131
127
|
if (!rootSelector) {
|
|
132
|
-
const detected = await autoDetectRoot(url, { browser, headed, state });
|
|
128
|
+
const detected = await autoDetectRoot(url, { browser, headed, state, viewport: config.viewport });
|
|
133
129
|
if (!detected) {
|
|
134
130
|
output.error('Could not auto-detect a root element. Please provide a selector: cssprobe-cli inspect <url> <selector>');
|
|
135
131
|
return;
|
|
@@ -137,7 +133,7 @@ async function handleInspect(command, cmdEntry, args, output) {
|
|
|
137
133
|
rootSelector = detected;
|
|
138
134
|
console.error(`Auto-detected root: ${rootSelector}`);
|
|
139
135
|
}
|
|
140
|
-
const launcher = new launcher_1.BrowserLauncher({ browser, headed, state });
|
|
136
|
+
const launcher = new launcher_1.BrowserLauncher({ browser, headed, state, viewport: config.viewport });
|
|
141
137
|
await launcher.open(url);
|
|
142
138
|
// Wait for user interaction if --wait
|
|
143
139
|
if (wait) {
|
|
@@ -431,7 +427,8 @@ async function handleInteractive(command, cmdEntry, args, _output) {
|
|
|
431
427
|
console.error('Error: URL is required. Usage: cssprobe-cli interactive <url>');
|
|
432
428
|
process.exit(1);
|
|
433
429
|
}
|
|
434
|
-
const
|
|
430
|
+
const config = (0, config_1.loadConfig)(args);
|
|
431
|
+
const launcher = new launcher_1.BrowserLauncher({ browser, headed: true, state, viewport: config.viewport });
|
|
435
432
|
await launcher.open(url);
|
|
436
433
|
let currentUrl = url;
|
|
437
434
|
// Resolve auto depth
|
package/dist/config/config.d.ts
CHANGED
|
@@ -3,11 +3,16 @@ export interface ProfileConfig {
|
|
|
3
3
|
browser?: string;
|
|
4
4
|
depth?: string;
|
|
5
5
|
headed?: string;
|
|
6
|
+
viewport?: string;
|
|
6
7
|
}
|
|
7
8
|
export interface Config {
|
|
8
9
|
browser: string;
|
|
9
10
|
depth: number;
|
|
10
11
|
headed: boolean;
|
|
12
|
+
viewport?: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
11
16
|
output: 'text' | 'json';
|
|
12
17
|
raw: boolean;
|
|
13
18
|
}
|
package/dist/config/config.js
CHANGED
|
@@ -77,16 +77,29 @@ function resolveProfile(args) {
|
|
|
77
77
|
const rc = readRcFile();
|
|
78
78
|
return rc.profiles[name] || {};
|
|
79
79
|
}
|
|
80
|
+
function parseViewport(raw) {
|
|
81
|
+
const match = raw.match(/^(\d+)\s*[xX*](\d+)$/);
|
|
82
|
+
if (!match)
|
|
83
|
+
return undefined;
|
|
84
|
+
const width = Number(match[1]);
|
|
85
|
+
const height = Number(match[2]);
|
|
86
|
+
if (width > 0 && height > 0)
|
|
87
|
+
return { width, height };
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
80
90
|
function loadConfig(args) {
|
|
81
91
|
const profile = resolveProfile(args);
|
|
82
92
|
const browser = args.browser || process.env.CSSPROBE_BROWSER || profile.browser || 'chromium';
|
|
83
93
|
const depthRaw = args.depth ?? process.env.CSSPROBE_DEPTH ?? profile.depth;
|
|
84
94
|
const depth = depthRaw ? Number(depthRaw) : 6;
|
|
85
95
|
const headed = !!(args.headed || process.env.CSSPROBE_HEADED === 'true' || profile.headed === 'true');
|
|
96
|
+
const viewportRaw = args.viewport || process.env.CSSPROBE_VIEWPORT || profile.viewport;
|
|
97
|
+
const viewport = viewportRaw ? parseViewport(viewportRaw) : undefined;
|
|
86
98
|
return {
|
|
87
99
|
browser,
|
|
88
100
|
depth: Number.isNaN(depth) ? 6 : depth,
|
|
89
101
|
headed,
|
|
102
|
+
viewport,
|
|
90
103
|
output: args.json ? 'json' : 'text',
|
|
91
104
|
raw: !!args.raw,
|
|
92
105
|
};
|
|
@@ -96,6 +109,7 @@ function maskConfig(config) {
|
|
|
96
109
|
browser: config.browser,
|
|
97
110
|
depth: String(config.depth),
|
|
98
111
|
headed: String(config.headed),
|
|
112
|
+
...(config.viewport ? { viewport: `${config.viewport.width}x${config.viewport.height}` } : {}),
|
|
99
113
|
};
|
|
100
114
|
}
|
|
101
115
|
function writeRcConfig(key, value, profileName) {
|
package/dist/engine/renderer.js
CHANGED
|
@@ -4,277 +4,19 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.renderMarkdown = renderMarkdown;
|
|
6
6
|
exports.renderJSON = renderJSON;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// ─── Node rendering (DOM tree) ───
|
|
14
|
-
function renderNode(node, indent) {
|
|
15
|
-
const lines = [];
|
|
16
|
-
if (!node)
|
|
17
|
-
return lines;
|
|
18
|
-
const cls = node.classes.length ? `.${node.classes.join('.')}` : '';
|
|
19
|
-
const id = node.id ? `#${node.id}` : '';
|
|
20
|
-
const repeat = node.repeat ? ` \u00D7${node.repeat}` : '';
|
|
21
|
-
const m = node.metrics;
|
|
22
|
-
const s = node.shape || {};
|
|
23
|
-
const flag = [];
|
|
24
|
-
if (node.flags.overflowsViewport)
|
|
25
|
-
flag.push('\u26A0viewport-overflow');
|
|
26
|
-
if (node.flags.overflowsParent)
|
|
27
|
-
flag.push('\u26A0parent-overflow');
|
|
28
|
-
if (m.clientHeight === 0 && m.offsetHeight === 0)
|
|
29
|
-
flag.push('\u26A0collapsed(0px)');
|
|
30
|
-
if (node.flags.scrollable)
|
|
31
|
-
flag.push('\u2714scrollable');
|
|
32
|
-
if (node.flags.hasScrollY && !node.flags.scrollable) {
|
|
33
|
-
flag.push(`\u26A0overflow(${m.scrollHeight}>${m.clientHeight})but-overflow=${node.props.overflowY}`);
|
|
34
|
-
}
|
|
35
|
-
if (node.containingBlockModifiers.length) {
|
|
36
|
-
flag.push(`CB:${node.containingBlockModifiers.join(' ')}`);
|
|
37
|
-
}
|
|
38
|
-
const styleHint = node.inlineStyle ? ` style="${node.inlineStyle}"` : '';
|
|
39
|
-
const h = node.props.height;
|
|
40
|
-
const heightStr = h === 'auto' ? `auto\u2192${m.offsetHeight}px` : `${h}`;
|
|
41
|
-
const w = node.props.width;
|
|
42
|
-
const widthStr = w === 'auto' ? `auto\u2192${m.clientWidth}px` : `${w}`;
|
|
43
|
-
const mhStr = node.declared && node.declared['max-height'] ? ` maxH:${node.declared['max-height'].map(x => x.value).join(';')}` : '';
|
|
44
|
-
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? ' ' + s.scrollTag : ''} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
45
|
-
lines.push(`${' '.repeat(indent)}<${node.tag}${id}${cls}${styleHint}${repeat}> ${shapeStr} [${node.props.position},${node.props.display},h:${heightStr},w:${widthStr}${mhStr}] rect(${m.rect.width}\u00D7${m.rect.height}) bottom=${m.rectBottom} scroll=${m.clientHeight}/${m.scrollHeight}` +
|
|
46
|
-
(flag.length ? ` ${flag.join(' ')}` : '') +
|
|
47
|
-
(node.text ? ` \u300C${node.text.slice(0, 30)}\u300D` : ''));
|
|
48
|
-
for (const child of node.children) {
|
|
49
|
-
lines.push(...renderNode(child, indent + 1));
|
|
50
|
-
}
|
|
51
|
-
return lines;
|
|
52
|
-
}
|
|
53
|
-
function renderAncestors(chain) {
|
|
54
|
-
return chain.map(a => {
|
|
55
|
-
const dh = a.declared && a.declared.height ? a.declared.height.map(x => `${x.selector}\u2192${x.value}`).join('; ') : 'auto';
|
|
56
|
-
const dw = a.declared && a.declared.width ? a.declared.width.map(x => `${x.selector}\u2192${x.value}`).join('; ') : 'auto';
|
|
57
|
-
const mh = a.declared && a.declared['max-height'] ? a.declared['max-height'].map(x => `${x.selector}\u2192${x.value}`).join('; ') : null;
|
|
58
|
-
const maxPart = mh ? ` max-height:${mh}` : '';
|
|
59
|
-
const s = a.shape || {};
|
|
60
|
-
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? ' ' + s.scrollTag : ''} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
61
|
-
return `${a.label} ${shapeStr} [${a.props.position},${a.props.display}] h:${dh}${maxPart} w:${dw} \u2192 ${a.metrics.clientWidth}\u00D7${a.metrics.offsetHeight}` +
|
|
62
|
-
(a.containingBlockModifiers.length ? ` CB:${a.containingBlockModifiers.join(' ')}` : '') +
|
|
63
|
-
(a.inlineStyle ? ` style="${a.inlineStyle}"` : '');
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function buildSketchTree(node, issueLabels) {
|
|
67
|
-
if (!node)
|
|
68
|
-
return null;
|
|
69
|
-
const label = nodeLabel(node);
|
|
70
|
-
const s = node.shape || {};
|
|
71
|
-
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
72
|
-
const flags = [];
|
|
73
|
-
if (node.flags.overflowsParent)
|
|
74
|
-
flags.push('\u26A0parent-overflow');
|
|
75
|
-
if (node.flags.overflowsViewport)
|
|
76
|
-
flags.push('\u26A0viewport-overflow');
|
|
77
|
-
if (node.metrics.clientHeight === 0 && node.metrics.offsetHeight === 0)
|
|
78
|
-
flags.push('\u26A0collapsed');
|
|
79
|
-
if (node.flags.scrollable)
|
|
80
|
-
flags.push('\u2714scrollable');
|
|
81
|
-
if (node.containingBlockModifiers.length)
|
|
82
|
-
flags.push(`CB:${node.containingBlockModifiers.join(' ')}`);
|
|
83
|
-
const hasIssue = issueLabels.has(label) || flags.length > 0;
|
|
84
|
-
const children = [];
|
|
85
|
-
let omitted = 0;
|
|
86
|
-
for (const child of node.children) {
|
|
87
|
-
const sketchChild = buildSketchTree(child, issueLabels);
|
|
88
|
-
if (!sketchChild)
|
|
89
|
-
continue;
|
|
90
|
-
if (sketchChild.hasIssue || hasDescendantIssue(sketchChild)) {
|
|
91
|
-
children.push(sketchChild);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
omitted++;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { label, shape: shapeStr, flags, children, hasIssue, omitted };
|
|
98
|
-
}
|
|
99
|
-
function hasDescendantIssue(node) {
|
|
100
|
-
if (node.hasIssue)
|
|
101
|
-
return true;
|
|
102
|
-
return node.children.some(c => hasDescendantIssue(c));
|
|
103
|
-
}
|
|
104
|
-
function renderSketchNode(node, prefix, isLast) {
|
|
105
|
-
const lines = [];
|
|
106
|
-
const connector = isLast ? '\u2514\u2500 ' : '\u251C\u2500 ';
|
|
107
|
-
const flagStr = node.flags.length ? ` ${node.flags.join(' ')}` : '';
|
|
108
|
-
const issueMark = node.hasIssue && node.flags.length === 0 ? ' \u26A0' : '';
|
|
109
|
-
lines.push(`${prefix}${connector}${node.label} ${node.shape}${flagStr}${issueMark}`);
|
|
110
|
-
const childPrefix = prefix + (isLast ? ' ' : '\u2502 ');
|
|
111
|
-
const totalChildren = node.children.length + (node.omitted > 0 ? 1 : 0);
|
|
112
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
113
|
-
lines.push(...renderSketchNode(node.children[i], childPrefix, i === totalChildren - 1));
|
|
114
|
-
}
|
|
115
|
-
if (node.omitted > 0) {
|
|
116
|
-
const omConnector = '\u2514\u2500 ';
|
|
117
|
-
lines.push(`${childPrefix}${omConnector}... (${node.omitted} subtrees omitted)`);
|
|
118
|
-
}
|
|
119
|
-
return lines;
|
|
120
|
-
}
|
|
121
|
-
function renderSketch(tree, findings) {
|
|
122
|
-
if (!tree)
|
|
123
|
-
return ['(empty tree)'];
|
|
124
|
-
const issueLabels = new Set();
|
|
125
|
-
for (const f of findings) {
|
|
126
|
-
if (f.level === 'warning' || f.level === 'error') {
|
|
127
|
-
if (f.location)
|
|
128
|
-
issueLabels.add(f.location);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const sketchRoot = buildSketchTree(tree, issueLabels);
|
|
132
|
-
if (!sketchRoot)
|
|
133
|
-
return ['(empty tree)'];
|
|
134
|
-
if (!hasDescendantIssue(sketchRoot) && !sketchRoot.hasIssue) {
|
|
135
|
-
return [`${sketchRoot.label} ${sketchRoot.shape} (no issues found in tree)`];
|
|
136
|
-
}
|
|
137
|
-
return renderSketchNode(sketchRoot, '', true);
|
|
138
|
-
}
|
|
139
|
-
// ─── Layout diagram (ASCII box layout) ───
|
|
140
|
-
const BOX = { tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' };
|
|
141
|
-
function renderLayout(snapshot, findings) {
|
|
142
|
-
if (!snapshot.tree)
|
|
143
|
-
return ['(empty tree)'];
|
|
144
|
-
const vpW = snapshot.viewport.width;
|
|
145
|
-
const termWidth = Math.min(Math.max(process.stdout?.columns || 80, 60), 120);
|
|
146
|
-
const indentUnit = 2;
|
|
147
|
-
const maxDepth = 8;
|
|
148
|
-
const boxPad = 4; // '│ ' prefix per depth level
|
|
149
|
-
const issueLocations = new Set();
|
|
150
|
-
for (const f of findings) {
|
|
151
|
-
if (f.location && (f.level === 'warning' || f.level === 'error'))
|
|
152
|
-
issueLocations.add(f.location);
|
|
153
|
-
}
|
|
154
|
-
function boxCharW(nodeW, depth) {
|
|
155
|
-
const usedIndent = depth * indentUnit;
|
|
156
|
-
const available = termWidth - usedIndent - boxPad;
|
|
157
|
-
const ratio = nodeW / vpW;
|
|
158
|
-
return Math.max(Math.round(available * ratio), 14);
|
|
159
|
-
}
|
|
160
|
-
const lines = [];
|
|
161
|
-
function renderEl(node, depth, parentW) {
|
|
162
|
-
if (depth > maxDepth) {
|
|
163
|
-
lines.push(' '.repeat(depth) + '... (depth limit)');
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const m = node.metrics.rect;
|
|
167
|
-
// Skip zero-size elements (SVG symbols, hidden elements)
|
|
168
|
-
if (m.width < 1 && m.height < 1 && node.children.length > 0) {
|
|
169
|
-
for (const child of node.children)
|
|
170
|
-
renderEl(child, depth, parentW);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (m.width < 1 && m.height < 1)
|
|
174
|
-
return;
|
|
175
|
-
const label = nodeLabel(node);
|
|
176
|
-
const hasIssue = issueLocations.has(label);
|
|
177
|
-
const dimStr = `${Math.round(m.width)}\u00D7${Math.round(m.height)}`;
|
|
178
|
-
const charW = boxCharW(m.width, depth);
|
|
179
|
-
const innerW = charW - 2;
|
|
180
|
-
const indent = ' '.repeat(depth);
|
|
181
|
-
const issueMark = hasIssue ? ' \u26A0' : '';
|
|
182
|
-
if (innerW < 3) {
|
|
183
|
-
// Too narrow for a proper box
|
|
184
|
-
lines.push(`${indent}${BOX.v} ${label.slice(0, charW - 3)}... ${dimStr}`);
|
|
185
|
-
for (const child of node.children)
|
|
186
|
-
renderEl(child, depth + 1, m.width);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
// Top border: ┌─ label ────────┐ WxH
|
|
190
|
-
const labelPart = ` ${label} `;
|
|
191
|
-
const dimPart = ` ${dimStr} `;
|
|
192
|
-
const topFillLen = Math.max(0, innerW - labelPart.length);
|
|
193
|
-
lines.push(`${indent}${BOX.tl}${BOX.h}${labelPart}${BOX.h.repeat(topFillLen)}${BOX.tr}${dimPart}${issueMark}`);
|
|
194
|
-
// Content
|
|
195
|
-
if (node.children.length === 0) {
|
|
196
|
-
// Empty box
|
|
197
|
-
lines.push(`${indent}${BOX.v}${' '.repeat(innerW)}${BOX.v}`);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
for (const child of node.children)
|
|
201
|
-
renderEl(child, depth + 1, m.width);
|
|
202
|
-
}
|
|
203
|
-
// Bottom border: └───────────────┘
|
|
204
|
-
lines.push(`${indent}${BOX.bl}${BOX.h.repeat(innerW)}${BOX.br}`);
|
|
205
|
-
}
|
|
206
|
-
renderEl(snapshot.tree, 0, vpW);
|
|
207
|
-
const scaleStr = `${Math.round(vpW / termWidth)}px/char`;
|
|
208
|
-
const result = [];
|
|
209
|
-
result.push(`## Layout (${vpW}\u00D7${snapshot.viewport.height} viewport, scale: ${scaleStr})`);
|
|
210
|
-
result.push('```');
|
|
211
|
-
result.push(...lines);
|
|
212
|
-
result.push('```');
|
|
213
|
-
return result;
|
|
214
|
-
}
|
|
215
|
-
// ─── Findings rendering ───
|
|
216
|
-
const CONF_BADGE = {
|
|
217
|
-
DEFINITE: 'DEFINITE',
|
|
218
|
-
INDEFINITE: 'INDEFINITE',
|
|
219
|
-
UNVERIFIABLE: 'UNVERIFIABLE',
|
|
220
|
-
};
|
|
221
|
-
function renderFindings(findings) {
|
|
222
|
-
const lines = [];
|
|
223
|
-
lines.push('## Findings');
|
|
224
|
-
if (findings.length === 0) {
|
|
225
|
-
lines.push('- 无异常发现');
|
|
226
|
-
return lines;
|
|
227
|
-
}
|
|
228
|
-
for (const f of findings) {
|
|
229
|
-
const badge = `[${CONF_BADGE[f.confidence]}]`;
|
|
230
|
-
const loc = f.location ? ` @ ${f.location}` : '';
|
|
231
|
-
lines.push(`- ${f.level === 'error' ? '\u26A0' : f.level === 'warning' ? '\u26A0' : '\u2714'} ${f.message} ${badge}${loc}`);
|
|
232
|
-
for (const ev of f.evidence.slice(0, 3)) {
|
|
233
|
-
if (ev.type === 'computed') {
|
|
234
|
-
lines.push(` - computed ${ev.property}: ${ev.value}`);
|
|
235
|
-
}
|
|
236
|
-
else if (ev.type === 'declared') {
|
|
237
|
-
lines.push(` - declared ${ev.property}: ${ev.value} (${ev.source || '?'})${ev.accessible ? '' : ' [inaccessible]'}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return lines;
|
|
242
|
-
}
|
|
243
|
-
function renderBriefFindings(findings) {
|
|
244
|
-
const lines = [];
|
|
245
|
-
lines.push('## Findings');
|
|
246
|
-
const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
|
|
247
|
-
if (important.length === 0) {
|
|
248
|
-
lines.push('- No warnings or errors');
|
|
249
|
-
return lines;
|
|
250
|
-
}
|
|
251
|
-
for (const f of important) {
|
|
252
|
-
const badge = `[${CONF_BADGE[f.confidence]}]`;
|
|
253
|
-
const loc = f.location ? ` @ ${f.location}` : '';
|
|
254
|
-
const icon = f.level === 'error' ? '\u26A0' : '\u26A0';
|
|
255
|
-
lines.push(`- ${icon} ${f.message} ${badge}${loc}`);
|
|
256
|
-
}
|
|
257
|
-
return lines;
|
|
258
|
-
}
|
|
259
|
-
function confidenceSummary(findings) {
|
|
260
|
-
const counts = findings.reduce((acc, f) => {
|
|
261
|
-
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
262
|
-
return acc;
|
|
263
|
-
}, {});
|
|
264
|
-
const parts = [
|
|
265
|
-
`DEFINITE ${counts.DEFINITE || 0}`,
|
|
266
|
-
`INDEFINITE ${counts.INDEFINITE || 0}`,
|
|
267
|
-
`UNVERIFIABLE ${counts.UNVERIFIABLE || 0}`,
|
|
268
|
-
];
|
|
269
|
-
return parts.join(' | ');
|
|
270
|
-
}
|
|
7
|
+
const utils_1 = require("./renderers/utils");
|
|
8
|
+
const dom_tree_1 = require("./renderers/dom-tree");
|
|
9
|
+
const ancestors_1 = require("./renderers/ancestors");
|
|
10
|
+
const sketch_1 = require("./renderers/sketch");
|
|
11
|
+
const layout_1 = require("./renderers/layout");
|
|
12
|
+
const findings_1 = require("./renderers/findings");
|
|
271
13
|
// ─── Markdown ───
|
|
272
14
|
function renderMarkdown(snapshot, findings, brief = false, layout = false) {
|
|
273
15
|
const lines = [];
|
|
274
16
|
const vp = snapshot.viewport ? `${snapshot.viewport.width}\u00D7${snapshot.viewport.height}` : 'unknown';
|
|
275
17
|
lines.push('# cssprobe-cli report');
|
|
276
18
|
lines.push(`viewport: ${vp} | root: ${snapshot.rootSelector} | nodes: ${snapshot.nodeCount}`);
|
|
277
|
-
lines.push(`confidence: ${confidenceSummary(findings)}`);
|
|
19
|
+
lines.push(`confidence: ${(0, utils_1.confidenceSummary)(findings)}`);
|
|
278
20
|
if (snapshot.error) {
|
|
279
21
|
lines.push('');
|
|
280
22
|
lines.push(`\u26A0 ${snapshot.error}`);
|
|
@@ -286,28 +28,28 @@ function renderMarkdown(snapshot, findings, brief = false, layout = false) {
|
|
|
286
28
|
lines.push('');
|
|
287
29
|
lines.push(`## Ancestor chain (root\u2192${snapshot.upTo})`);
|
|
288
30
|
lines.push('```');
|
|
289
|
-
lines.push(...renderAncestors(snapshot.ancestors));
|
|
31
|
+
lines.push(...(0, ancestors_1.renderAncestors)(snapshot.ancestors));
|
|
290
32
|
lines.push('```');
|
|
291
33
|
if (layout) {
|
|
292
34
|
lines.push('');
|
|
293
|
-
lines.push(...renderLayout(snapshot, findings));
|
|
35
|
+
lines.push(...(0, layout_1.renderLayout)(snapshot, findings));
|
|
294
36
|
}
|
|
295
37
|
if (brief) {
|
|
296
38
|
lines.push('');
|
|
297
39
|
lines.push('## Tree Sketch (issues only)');
|
|
298
40
|
lines.push('```');
|
|
299
|
-
lines.push(...renderSketch(snapshot.tree, findings));
|
|
41
|
+
lines.push(...(0, sketch_1.renderSketch)(snapshot.tree, findings));
|
|
300
42
|
lines.push('```');
|
|
301
43
|
}
|
|
302
44
|
else if (!layout) {
|
|
303
45
|
lines.push('');
|
|
304
46
|
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
305
47
|
lines.push('```');
|
|
306
|
-
lines.push(...renderNode(snapshot.tree, 0));
|
|
48
|
+
lines.push(...(0, dom_tree_1.renderNode)(snapshot.tree, 0));
|
|
307
49
|
lines.push('```');
|
|
308
50
|
}
|
|
309
51
|
lines.push('');
|
|
310
|
-
lines.push(...(brief ? renderBriefFindings(findings) : renderFindings(findings)));
|
|
52
|
+
lines.push(...(brief ? (0, findings_1.renderBriefFindings)(findings) : (0, findings_1.renderFindings)(findings)));
|
|
311
53
|
if (snapshot.crossOriginBlocked && snapshot.crossOriginBlocked > 0) {
|
|
312
54
|
lines.push('');
|
|
313
55
|
lines.push('## Cross-origin limitation');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderAncestors = renderAncestors;
|
|
4
|
+
function renderAncestors(chain) {
|
|
5
|
+
return chain.map(a => {
|
|
6
|
+
const dh = a.declared && a.declared.height ? a.declared.height.map(x => `${x.selector}\u2192${x.value}`).join('; ') : 'auto';
|
|
7
|
+
const dw = a.declared && a.declared.width ? a.declared.width.map(x => `${x.selector}\u2192${x.value}`).join('; ') : 'auto';
|
|
8
|
+
const mh = a.declared && a.declared['max-height'] ? a.declared['max-height'].map(x => `${x.selector}\u2192${x.value}`).join('; ') : null;
|
|
9
|
+
const maxPart = mh ? ` max-height:${mh}` : '';
|
|
10
|
+
const s = a.shape || {};
|
|
11
|
+
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? ' ' + s.scrollTag : ''} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
12
|
+
return `${a.label} ${shapeStr} [${a.props.position},${a.props.display}] h:${dh}${maxPart} w:${dw} \u2192 ${a.metrics.clientWidth}\u00D7${a.metrics.offsetHeight}` +
|
|
13
|
+
(a.containingBlockModifiers.length ? ` CB:${a.containingBlockModifiers.join(' ')}` : '') +
|
|
14
|
+
(a.inlineStyle ? ` style="${a.inlineStyle}"` : '');
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ancestors.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderNode = renderNode;
|
|
4
|
+
function renderNode(node, indent) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
if (!node)
|
|
7
|
+
return lines;
|
|
8
|
+
const cls = node.classes.length ? `.${node.classes.join('.')}` : '';
|
|
9
|
+
const id = node.id ? `#${node.id}` : '';
|
|
10
|
+
const repeat = node.repeat ? ` \u00D7${node.repeat}` : '';
|
|
11
|
+
const m = node.metrics;
|
|
12
|
+
const s = node.shape || {};
|
|
13
|
+
const flag = [];
|
|
14
|
+
if (node.flags.overflowsViewport)
|
|
15
|
+
flag.push('\u26A0viewport-overflow');
|
|
16
|
+
if (node.flags.overflowsParent)
|
|
17
|
+
flag.push('\u26A0parent-overflow');
|
|
18
|
+
if (m.clientHeight === 0 && m.offsetHeight === 0)
|
|
19
|
+
flag.push('\u26A0collapsed(0px)');
|
|
20
|
+
if (node.flags.scrollable)
|
|
21
|
+
flag.push('\u2714scrollable');
|
|
22
|
+
if (node.flags.hasScrollY && !node.flags.scrollable) {
|
|
23
|
+
flag.push(`\u26A0overflow(${m.scrollHeight}>${m.clientHeight})but-overflow=${node.props.overflowY}`);
|
|
24
|
+
}
|
|
25
|
+
if (node.containingBlockModifiers.length) {
|
|
26
|
+
flag.push(`CB:${node.containingBlockModifiers.join(' ')}`);
|
|
27
|
+
}
|
|
28
|
+
const styleHint = node.inlineStyle ? ` style="${node.inlineStyle}"` : '';
|
|
29
|
+
const h = node.props.height;
|
|
30
|
+
const heightStr = h === 'auto' ? `auto\u2192${m.offsetHeight}px` : `${h}`;
|
|
31
|
+
const w = node.props.width;
|
|
32
|
+
const widthStr = w === 'auto' ? `auto\u2192${m.clientWidth}px` : `${w}`;
|
|
33
|
+
const mhStr = node.declared && node.declared['max-height'] ? ` maxH:${node.declared['max-height'].map(x => x.value).join(';')}` : '';
|
|
34
|
+
const shapeStr = s.role ? `[${s.role}${s.scrollTag ? ' ' + s.scrollTag : ''} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
35
|
+
lines.push(`${' '.repeat(indent)}<${node.tag}${id}${cls}${styleHint}${repeat}> ${shapeStr} [${node.props.position},${node.props.display},h:${heightStr},w:${widthStr}${mhStr}] rect(${m.rect.width}\u00D7${m.rect.height}) bottom=${m.rectBottom} scroll=${m.clientHeight}/${m.scrollHeight}` +
|
|
36
|
+
(flag.length ? ` ${flag.join(' ')}` : '') +
|
|
37
|
+
(node.text ? ` \u300C${node.text.slice(0, 30)}\u300D` : ''));
|
|
38
|
+
for (const child of node.children) {
|
|
39
|
+
lines.push(...renderNode(child, indent + 1));
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=dom-tree.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderFindings = renderFindings;
|
|
4
|
+
exports.renderBriefFindings = renderBriefFindings;
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
function renderFindings(findings) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push('## Findings');
|
|
9
|
+
if (findings.length === 0) {
|
|
10
|
+
lines.push('- 无异常发现');
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
for (const f of findings) {
|
|
14
|
+
const badge = `[${utils_1.CONF_BADGE[f.confidence]}]`;
|
|
15
|
+
const loc = f.location ? ` @ ${f.location}` : '';
|
|
16
|
+
lines.push(`- ${f.level === 'error' ? '\u26A0' : f.level === 'warning' ? '\u26A0' : '\u2714'} ${f.message} ${badge}${loc}`);
|
|
17
|
+
for (const ev of f.evidence.slice(0, 3)) {
|
|
18
|
+
if (ev.type === 'computed') {
|
|
19
|
+
lines.push(` - computed ${ev.property}: ${ev.value}`);
|
|
20
|
+
}
|
|
21
|
+
else if (ev.type === 'declared') {
|
|
22
|
+
lines.push(` - declared ${ev.property}: ${ev.value} (${ev.source || '?'})${ev.accessible ? '' : ' [inaccessible]'}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
function renderBriefFindings(findings) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
lines.push('## Findings');
|
|
31
|
+
const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
|
|
32
|
+
if (important.length === 0) {
|
|
33
|
+
lines.push('- No warnings or errors');
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
for (const f of important) {
|
|
37
|
+
const badge = `[${utils_1.CONF_BADGE[f.confidence]}]`;
|
|
38
|
+
const loc = f.location ? ` @ ${f.location}` : '';
|
|
39
|
+
const icon = f.level === 'error' ? '\u26A0' : '\u26A0';
|
|
40
|
+
lines.push(`- ${icon} ${f.message} ${badge}${loc}`);
|
|
41
|
+
}
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=findings.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderLayout = renderLayout;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function renderLayout(snapshot, findings) {
|
|
6
|
+
if (!snapshot.tree)
|
|
7
|
+
return ['(empty tree)'];
|
|
8
|
+
const vpW = snapshot.viewport.width;
|
|
9
|
+
const termWidth = Math.min(Math.max(process.stdout?.columns || 80, 60), 120);
|
|
10
|
+
const indentUnit = 2;
|
|
11
|
+
const maxDepth = 8;
|
|
12
|
+
const boxPad = 4;
|
|
13
|
+
const issueLocations = new Set();
|
|
14
|
+
for (const f of findings) {
|
|
15
|
+
if (f.location && (f.level === 'warning' || f.level === 'error'))
|
|
16
|
+
issueLocations.add(f.location);
|
|
17
|
+
}
|
|
18
|
+
function boxCharW(nodeW, depth) {
|
|
19
|
+
const usedIndent = depth * indentUnit;
|
|
20
|
+
const available = termWidth - usedIndent - boxPad;
|
|
21
|
+
const ratio = nodeW / vpW;
|
|
22
|
+
return Math.max(Math.round(available * ratio), 14);
|
|
23
|
+
}
|
|
24
|
+
const lines = [];
|
|
25
|
+
function renderBox(node, charW, depth, flat = false) {
|
|
26
|
+
const m = node.metrics.rect;
|
|
27
|
+
const dimStr = `${Math.round(m.width)}\u00D7${Math.round(m.height)}`;
|
|
28
|
+
const innerW = Math.max(charW - 2, 1);
|
|
29
|
+
const label = (0, utils_1.smartLabel)(node, innerW - 4);
|
|
30
|
+
const hasIssue = issueLocations.has(label);
|
|
31
|
+
const issueMark = hasIssue ? ' \u26A0' : '';
|
|
32
|
+
const repeat = node.repeat && node.repeat > 1 ? node.repeat : 1;
|
|
33
|
+
const boxLines = [];
|
|
34
|
+
if (innerW < 3) {
|
|
35
|
+
boxLines.push(`${utils_1.BOX.v} ${label.slice(0, Math.max(charW - 3, 0))}... ${dimStr}`);
|
|
36
|
+
return boxLines;
|
|
37
|
+
}
|
|
38
|
+
const labelPart = ` ${label}${repeat > 1 ? ` \u00D7${repeat}` : ''} `;
|
|
39
|
+
const dimPart = ` ${dimStr} `;
|
|
40
|
+
const topFillLen = Math.max(0, innerW - labelPart.length - dimPart.length);
|
|
41
|
+
boxLines.push(`${utils_1.BOX.tl}${utils_1.BOX.h}${labelPart}${utils_1.BOX.h.repeat(topFillLen)}${utils_1.BOX.tr}${dimPart}${issueMark}`);
|
|
42
|
+
// Children
|
|
43
|
+
const children = node.children.filter(c => !(c.metrics.rect.width < 1 && c.metrics.rect.height < 1));
|
|
44
|
+
if (children.length === 0) {
|
|
45
|
+
boxLines.push(`${utils_1.BOX.v}${' '.repeat(innerW)}${utils_1.BOX.v}`);
|
|
46
|
+
}
|
|
47
|
+
else if (flat) {
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
const childLabel = (0, utils_1.nodeLabel)(child);
|
|
50
|
+
const childRepeat = child.repeat && child.repeat > 1 ? ` \u00D7${child.repeat}` : '';
|
|
51
|
+
const childDim = `${Math.round(child.metrics.rect.width)}\u00D7${Math.round(child.metrics.rect.height)}`;
|
|
52
|
+
const childLine = ` ${childLabel}${childRepeat} ${childDim} `;
|
|
53
|
+
if (childLine.length <= innerW) {
|
|
54
|
+
boxLines.push(`${utils_1.BOX.v}${childLine}${' '.repeat(innerW - childLine.length)}${utils_1.BOX.v}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
boxLines.push(`${utils_1.BOX.v}${childLine.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if ((0, utils_1.isMultiColumn)(node)) {
|
|
62
|
+
// Calculate expanded count (considering repeat)
|
|
63
|
+
const expandedCount = children.reduce((sum, c) => sum + (c.repeat && c.repeat > 1 ? c.repeat : 1), 0);
|
|
64
|
+
if (expandedCount > 1) {
|
|
65
|
+
const isGrid = node.props.display === 'grid' || node.props.display === 'inline-grid';
|
|
66
|
+
const childLines = renderFlexChildren(children, innerW, depth + 1, m.width, isGrid);
|
|
67
|
+
for (const cl of childLines) {
|
|
68
|
+
boxLines.push(`${utils_1.BOX.v}${cl}${utils_1.BOX.v}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (const child of children) {
|
|
73
|
+
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
74
|
+
for (const cl of childBox) {
|
|
75
|
+
const line = ` ${cl}`;
|
|
76
|
+
if (line.length <= innerW + 1) {
|
|
77
|
+
boxLines.push(`${utils_1.BOX.v}${line}${' '.repeat(Math.max(0, innerW - line.length + 1))}${utils_1.BOX.v}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
boxLines.push(`${utils_1.BOX.v}${line.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
89
|
+
for (const cl of childBox) {
|
|
90
|
+
const line = ` ${cl}`;
|
|
91
|
+
if (line.length <= innerW + 1) {
|
|
92
|
+
boxLines.push(`${utils_1.BOX.v}${line}${' '.repeat(Math.max(0, innerW - line.length + 1))}${utils_1.BOX.v}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
boxLines.push(`${utils_1.BOX.v}${line.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
boxLines.push(`${utils_1.BOX.bl}${utils_1.BOX.h.repeat(innerW)}${utils_1.BOX.br}`);
|
|
101
|
+
return boxLines;
|
|
102
|
+
}
|
|
103
|
+
function renderFlexChildren(children, availW, depth, parentPxW, isGrid) {
|
|
104
|
+
// Expand repeat nodes into multiple copies
|
|
105
|
+
const expanded = [];
|
|
106
|
+
for (const child of children) {
|
|
107
|
+
const repeat = child.repeat && child.repeat > 1 ? child.repeat : 1;
|
|
108
|
+
for (let r = 0; r < repeat; r++) {
|
|
109
|
+
expanded.push({ ...child, repeat: 1, children: child.children });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// For grid: group into rows based on column count
|
|
113
|
+
let rows;
|
|
114
|
+
if (isGrid && expanded.length > 1) {
|
|
115
|
+
const childW = expanded[0].metrics.rect.width;
|
|
116
|
+
const cols = Math.max(1, Math.floor(parentPxW / childW));
|
|
117
|
+
rows = [];
|
|
118
|
+
for (let i = 0; i < expanded.length; i += cols) {
|
|
119
|
+
rows.push(expanded.slice(i, i + cols));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
rows = [expanded];
|
|
124
|
+
}
|
|
125
|
+
const allResult = [];
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
const gap = 1;
|
|
128
|
+
const totalGaps = (row.length - 1) * gap;
|
|
129
|
+
const minPerChild = 10;
|
|
130
|
+
const usableW = Math.max(availW - totalGaps, row.length * minPerChild);
|
|
131
|
+
const childWidths = [];
|
|
132
|
+
let totalUsed = 0;
|
|
133
|
+
for (let i = 0; i < row.length; i++) {
|
|
134
|
+
const ratio = row[i].metrics.rect.width / parentPxW;
|
|
135
|
+
const w = Math.max(Math.round(usableW * ratio), minPerChild);
|
|
136
|
+
childWidths.push(w);
|
|
137
|
+
totalUsed += w;
|
|
138
|
+
}
|
|
139
|
+
if (childWidths.length > 0 && usableW > totalUsed) {
|
|
140
|
+
childWidths[childWidths.length - 1] += usableW - totalUsed;
|
|
141
|
+
}
|
|
142
|
+
const childBoxes = [];
|
|
143
|
+
let maxLines = 0;
|
|
144
|
+
for (let i = 0; i < row.length; i++) {
|
|
145
|
+
const box = renderBox(row[i], childWidths[i], depth, true);
|
|
146
|
+
childBoxes.push(box);
|
|
147
|
+
if (box.length > maxLines)
|
|
148
|
+
maxLines = box.length;
|
|
149
|
+
}
|
|
150
|
+
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
151
|
+
let merged = '';
|
|
152
|
+
for (let i = 0; i < childBoxes.length; i++) {
|
|
153
|
+
const w = childWidths[i];
|
|
154
|
+
if (i > 0)
|
|
155
|
+
merged += ' ';
|
|
156
|
+
if (lineIdx < childBoxes[i].length) {
|
|
157
|
+
const line = childBoxes[i][lineIdx];
|
|
158
|
+
if (line.length < w) {
|
|
159
|
+
merged += line + ' '.repeat(w - line.length);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
merged += line.slice(0, w);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
merged += ' '.repeat(w);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
allResult.push(merged);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return allResult;
|
|
173
|
+
}
|
|
174
|
+
function renderEl(node, depth, parentW) {
|
|
175
|
+
if (depth > maxDepth) {
|
|
176
|
+
lines.push(' '.repeat(depth) + '... (depth limit)');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const m = node.metrics.rect;
|
|
180
|
+
if (m.width < 1 && m.height < 1 && node.children.length > 0) {
|
|
181
|
+
for (const child of node.children)
|
|
182
|
+
renderEl(child, depth, parentW);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (m.width < 1 && m.height < 1)
|
|
186
|
+
return;
|
|
187
|
+
const charW = boxCharW(m.width, depth);
|
|
188
|
+
const indent = ' '.repeat(depth);
|
|
189
|
+
const box = renderBox(node, charW, depth);
|
|
190
|
+
for (const line of box) {
|
|
191
|
+
lines.push(`${indent}${line}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
renderEl(snapshot.tree, 0, vpW);
|
|
195
|
+
const scaleStr = `${Math.round(vpW / termWidth)}px/char`;
|
|
196
|
+
const result = [];
|
|
197
|
+
result.push(`## Layout (${vpW}\u00D7${snapshot.viewport.height} viewport, scale: ${scaleStr})`);
|
|
198
|
+
result.push('```');
|
|
199
|
+
result.push(...lines);
|
|
200
|
+
result.push('```');
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSketch = renderSketch;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function buildSketchTree(node, issueLabels) {
|
|
6
|
+
if (!node)
|
|
7
|
+
return null;
|
|
8
|
+
const label = (0, utils_1.nodeLabel)(node);
|
|
9
|
+
const s = node.shape || {};
|
|
10
|
+
const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
|
|
11
|
+
const flags = [];
|
|
12
|
+
if (node.flags.overflowsParent)
|
|
13
|
+
flags.push('\u26A0parent-overflow');
|
|
14
|
+
if (node.flags.overflowsViewport)
|
|
15
|
+
flags.push('\u26A0viewport-overflow');
|
|
16
|
+
if (node.metrics.clientHeight === 0 && node.metrics.offsetHeight === 0)
|
|
17
|
+
flags.push('\u26A0collapsed');
|
|
18
|
+
if (node.flags.scrollable)
|
|
19
|
+
flags.push('\u2714scrollable');
|
|
20
|
+
if (node.containingBlockModifiers.length)
|
|
21
|
+
flags.push(`CB:${node.containingBlockModifiers.join(' ')}`);
|
|
22
|
+
const hasIssue = issueLabels.has(label) || flags.length > 0;
|
|
23
|
+
const children = [];
|
|
24
|
+
let omitted = 0;
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
const sketchChild = buildSketchTree(child, issueLabels);
|
|
27
|
+
if (!sketchChild)
|
|
28
|
+
continue;
|
|
29
|
+
if (sketchChild.hasIssue || hasDescendantIssue(sketchChild)) {
|
|
30
|
+
children.push(sketchChild);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
omitted++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { label, shape: shapeStr, flags, children, hasIssue, omitted };
|
|
37
|
+
}
|
|
38
|
+
function hasDescendantIssue(node) {
|
|
39
|
+
if (node.hasIssue)
|
|
40
|
+
return true;
|
|
41
|
+
return node.children.some(c => hasDescendantIssue(c));
|
|
42
|
+
}
|
|
43
|
+
function renderSketchNode(node, prefix, isLast) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const connector = isLast ? '\u2514\u2500 ' : '\u251C\u2500 ';
|
|
46
|
+
const flagStr = node.flags.length ? ` ${node.flags.join(' ')}` : '';
|
|
47
|
+
const issueMark = node.hasIssue && node.flags.length === 0 ? ' \u26A0' : '';
|
|
48
|
+
lines.push(`${prefix}${connector}${node.label} ${node.shape}${flagStr}${issueMark}`);
|
|
49
|
+
const childPrefix = prefix + (isLast ? ' ' : '\u2502 ');
|
|
50
|
+
const totalChildren = node.children.length + (node.omitted > 0 ? 1 : 0);
|
|
51
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
52
|
+
lines.push(...renderSketchNode(node.children[i], childPrefix, i === totalChildren - 1));
|
|
53
|
+
}
|
|
54
|
+
if (node.omitted > 0) {
|
|
55
|
+
const omConnector = '\u2514\u2500 ';
|
|
56
|
+
lines.push(`${childPrefix}${omConnector}... (${node.omitted} subtrees omitted)`);
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
function renderSketch(tree, findings) {
|
|
61
|
+
if (!tree)
|
|
62
|
+
return ['(empty tree)'];
|
|
63
|
+
const issueLabels = new Set();
|
|
64
|
+
for (const f of findings) {
|
|
65
|
+
if (f.level === 'warning' || f.level === 'error') {
|
|
66
|
+
if (f.location)
|
|
67
|
+
issueLabels.add(f.location);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const sketchRoot = buildSketchTree(tree, issueLabels);
|
|
71
|
+
if (!sketchRoot)
|
|
72
|
+
return ['(empty tree)'];
|
|
73
|
+
if (!hasDescendantIssue(sketchRoot) && !sketchRoot.hasIssue) {
|
|
74
|
+
return [`${sketchRoot.label} ${sketchRoot.shape} (no issues found in tree)`];
|
|
75
|
+
}
|
|
76
|
+
return renderSketchNode(sketchRoot, '', true);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sketch.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TreeNode, Confidence } from '../types';
|
|
2
|
+
export declare function nodeLabel(node: TreeNode): string;
|
|
3
|
+
export declare function smartLabel(node: TreeNode, maxLen: number): string;
|
|
4
|
+
export declare function isMultiColumn(node: TreeNode): boolean;
|
|
5
|
+
export declare const BOX: {
|
|
6
|
+
readonly tl: "┌";
|
|
7
|
+
readonly tr: "┐";
|
|
8
|
+
readonly bl: "└";
|
|
9
|
+
readonly br: "┘";
|
|
10
|
+
readonly h: "─";
|
|
11
|
+
readonly v: "│";
|
|
12
|
+
};
|
|
13
|
+
export declare const CONF_BADGE: Record<Confidence, string>;
|
|
14
|
+
export declare function confidenceSummary(findings: {
|
|
15
|
+
confidence: Confidence;
|
|
16
|
+
}[]): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONF_BADGE = exports.BOX = void 0;
|
|
4
|
+
exports.nodeLabel = nodeLabel;
|
|
5
|
+
exports.smartLabel = smartLabel;
|
|
6
|
+
exports.isMultiColumn = isMultiColumn;
|
|
7
|
+
exports.confidenceSummary = confidenceSummary;
|
|
8
|
+
function nodeLabel(node) {
|
|
9
|
+
const cls = node.classes.length ? `.${node.classes.join('.')}` : '';
|
|
10
|
+
const id = node.id ? `#${node.id}` : '';
|
|
11
|
+
return `${node.tag}${id}${cls}`;
|
|
12
|
+
}
|
|
13
|
+
function smartLabel(node, maxLen) {
|
|
14
|
+
const full = nodeLabel(node);
|
|
15
|
+
if (full.length <= maxLen)
|
|
16
|
+
return full;
|
|
17
|
+
const id = node.id ? `#${node.id}` : '';
|
|
18
|
+
const firstCls = node.classes.length > 0 ? `.${node.classes[0]}` : '';
|
|
19
|
+
const short = `${node.tag}${id}${firstCls}`;
|
|
20
|
+
if (short.length <= maxLen)
|
|
21
|
+
return short;
|
|
22
|
+
const tagId = `${node.tag}${id}`;
|
|
23
|
+
if (tagId.length <= maxLen)
|
|
24
|
+
return tagId;
|
|
25
|
+
if (node.tag.length <= maxLen)
|
|
26
|
+
return node.tag;
|
|
27
|
+
return node.tag.slice(0, Math.max(maxLen - 3, 1)) + '...';
|
|
28
|
+
}
|
|
29
|
+
function isMultiColumn(node) {
|
|
30
|
+
const display = node.props.display;
|
|
31
|
+
return (node.shape?.role === 'flex-row' ||
|
|
32
|
+
(display === 'flex' && node.props.flexDirection !== 'column') ||
|
|
33
|
+
display === 'grid' ||
|
|
34
|
+
display === 'inline-grid');
|
|
35
|
+
}
|
|
36
|
+
exports.BOX = { tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' };
|
|
37
|
+
exports.CONF_BADGE = {
|
|
38
|
+
DEFINITE: 'DEFINITE',
|
|
39
|
+
INDEFINITE: 'INDEFINITE',
|
|
40
|
+
UNVERIFIABLE: 'UNVERIFIABLE',
|
|
41
|
+
};
|
|
42
|
+
function confidenceSummary(findings) {
|
|
43
|
+
const counts = findings.reduce((acc, f) => {
|
|
44
|
+
acc[f.confidence] = (acc[f.confidence] || 0) + 1;
|
|
45
|
+
return acc;
|
|
46
|
+
}, {});
|
|
47
|
+
const parts = [
|
|
48
|
+
`DEFINITE ${counts.DEFINITE || 0}`,
|
|
49
|
+
`INDEFINITE ${counts.INDEFINITE || 0}`,
|
|
50
|
+
`UNVERIFIABLE ${counts.UNVERIFIABLE || 0}`,
|
|
51
|
+
];
|
|
52
|
+
return parts.join(' | ');
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=utils.js.map
|
package/dist/help.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nInspection:\n inspect <url> [selector] inspect the runtime css of a page (layout, scroll, colors, fonts, backgrounds)\n login <url> open browser for interactive login, then save state for later use\n state-import [file] import cookies from netscape format file into a playwright state file\n interactive <url> open browser and inspect interactively (repl mode)\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli (teaches agents how to use the cli)\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
|
|
3
3
|
"commands": {
|
|
4
4
|
"inspect": {
|
|
5
|
-
"help": "cssprobe-cli inspect <url> [selector]\n\nInspect the runtime CSS of a page (layout, scroll, colors, fonts, backgrounds)\n\nArguments:\n <url> url, file:// path, or local html file path\n [selector] css selector for the root element (auto-detected if omitted)\nOptions:\n --json output structured json instead of markdown\n --headed show the browser window\n --browser browser engine (default chromium)\n --zoom run 1x/0.5x viewport diagnosis\n --depth dom tree depth (default: auto, max 20)\n --max-nodes node count cap (default 60)\n --up-to ancestor stop tag (default html)\n --state path to saved state file (cookies + localstorage)\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --wait wait for user interaction before collecting (implies --headed)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
5
|
+
"help": "cssprobe-cli inspect <url> [selector]\n\nInspect the runtime CSS of a page (layout, scroll, colors, fonts, backgrounds)\n\nArguments:\n <url> url, file:// path, or local html file path\n [selector] css selector for the root element (auto-detected if omitted)\nOptions:\n --json output structured json instead of markdown\n --headed show the browser window\n --browser browser engine (default chromium)\n --zoom run 1x/0.5x viewport diagnosis\n --depth dom tree depth (default: auto, max 20)\n --max-nodes node count cap (default 60)\n --up-to ancestor stop tag (default html)\n --state path to saved state file (cookies + localstorage)\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --wait wait for user interaction before collecting (implies --headed)\n --viewport viewport size as wxh (e.g. 375x812, default 1280x720)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
6
6
|
"flags": {
|
|
7
7
|
"json": "boolean",
|
|
8
8
|
"headed": "boolean",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"state": "string",
|
|
15
15
|
"brief": "boolean",
|
|
16
16
|
"layout": "boolean",
|
|
17
|
-
"wait": "boolean"
|
|
17
|
+
"wait": "boolean",
|
|
18
|
+
"viewport": "string"
|
|
18
19
|
},
|
|
19
20
|
"args": [
|
|
20
21
|
"url",
|
|
@@ -42,11 +43,12 @@
|
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
"interactive": {
|
|
45
|
-
"help": "cssprobe-cli interactive <url>\n\nOpen browser and inspect interactively (REPL mode)\n\nArguments:\n <url> url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --state path to saved state file (cookies + localstorage)\n --depth dom tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
46
|
+
"help": "cssprobe-cli interactive <url>\n\nOpen browser and inspect interactively (REPL mode)\n\nArguments:\n <url> url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --state path to saved state file (cookies + localstorage)\n --depth dom tree depth (default: auto, max 20)\n --viewport viewport size as wxh (e.g. 375x812, default 1280x720)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
46
47
|
"flags": {
|
|
47
48
|
"browser": "string",
|
|
48
49
|
"state": "string",
|
|
49
|
-
"depth": "string"
|
|
50
|
+
"depth": "string",
|
|
51
|
+
"viewport": "string"
|
|
50
52
|
},
|
|
51
53
|
"args": [
|
|
52
54
|
"url"
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
"args": []
|
|
61
63
|
},
|
|
62
64
|
"config-set": {
|
|
63
|
-
"help": "cssprobe-cli config-set <key> <value>\n\nSet a configuration value\n\nArguments:\n <key> config key (browser, depth, headed)\n <value> config value\nOptions:\n --profile profile name",
|
|
65
|
+
"help": "cssprobe-cli config-set <key> <value>\n\nSet a configuration value\n\nArguments:\n <key> config key (browser, depth, headed, viewport)\n <value> config value\nOptions:\n --profile profile name",
|
|
64
66
|
"flags": {
|
|
65
67
|
"profile": "string"
|
|
66
68
|
},
|
|
@@ -238,12 +238,13 @@ confidence: DEFINITE 8 | INDEFINITE 0 | UNVERIFIABLE 1
|
|
|
238
238
|
export CSSPROBE_BROWSER=chromium
|
|
239
239
|
export CSSPROBE_DEPTH=6
|
|
240
240
|
export CSSPROBE_HEADED=true
|
|
241
|
+
export CSSPROBE_VIEWPORT=375x812
|
|
241
242
|
export CSSPROBE_PROFILE=default
|
|
242
243
|
```
|
|
243
244
|
|
|
244
245
|
Priority (highest to lowest):
|
|
245
|
-
1. CLI flags: `--browser`, `--depth`, `--headed`
|
|
246
|
-
2. Environment variables: `CSSPROBE_BROWSER`, `CSSPROBE_DEPTH`, `CSSPROBE_HEADED`
|
|
246
|
+
1. CLI flags: `--browser`, `--depth`, `--headed`, `--viewport`
|
|
247
|
+
2. Environment variables: `CSSPROBE_BROWSER`, `CSSPROBE_DEPTH`, `CSSPROBE_HEADED`, `CSSPROBE_VIEWPORT`
|
|
247
248
|
3. Config file: `~/.cssprobe-clirc`
|
|
248
249
|
|
|
249
250
|
---
|
package/package.json
CHANGED