cssprobe-cli 0.2.12 → 0.2.13

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/AGENTS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `cssprobe-cli` — Runtime CSS probe for layout/scroll/overflow/color/font inspection with session-based browser management. Entrypoint: `bin/cssprobe-cli.js` → `require('../dist/index')`.
6
6
  Build: `npm run build` (= `tsc` + `esbuild` collector IIFE bundle + `esbuild` daemon entry + `tsx scripts/generate-help.ts`).
7
- Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 45 tests).
7
+ Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 58 tests).
8
8
  Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
9
9
 
10
10
  ## Architecture
@@ -41,7 +41,7 @@ src/
41
41
  - **Session-based architecture**: Inspired by playwright-cli. `open` starts a daemon process, subsequent commands connect via Unix socket. Single session (default) mode.
42
42
  - **Daemon process**: Long-running background process that manages the browser and executes commands. Communicates via Unix domain socket.
43
43
  - **Three-layer engine**: `collector.ts` (browser, esbuild IIFE) → `analyzer.ts` (Node, pure functions) → `renderer.ts` (Markdown/JSON). Collector only gathers facts; analyzer does inference with confidence; renderer formats output.
44
- - **Collector bundling**: `collector.ts` is bundled by esbuild as an IIFE (`format: 'iife', globalName: '__cssprobe-cli'`). The launcher injects it via `page.addScriptTag({ content })`, then calls `window.__cssprobe-cli.collect(cfg)`.
44
+ - **Collector bundling**: `collector.ts` is bundled by esbuild as an IIFE (`format: 'iife', globalName: '__cssprobe_cli'`). The launcher injects it via `page.addScriptTag({ content })`, then calls `window.__cssprobe_cli.collect(cfg)`.
45
45
  - **Confidence model**: Every `Finding` carries `confidence: 'DEFINITE' | 'INDEFINITE' | 'UNVERIFIABLE'`. Computed values = DEFINITE. Percent declarations = INDEFINITE. Missing/blocked declarations = UNVERIFIABLE.
46
46
  - **Command definition**: `declareCommand({ name, category, description, args?, options? })` — purely declarative, no execution logic.
47
47
  - **Command dispatch**: `minimist` parse → `parseCommand()` Zod validate → session command execution via daemon socket.
@@ -101,11 +101,10 @@ src/
101
101
  npm run build
102
102
  ├── 1. tsc # Compile TS → dist/ (includes .d.ts)
103
103
  ├── 2. esbuild collector.ts # Bundle browser-side code → dist/collector-bundle.js (IIFE)
104
- ├── 3. esbuild daemonEntry.ts # Bundle daemon entry → dist/daemonEntry.js (CJS)
105
- └── 4. tsx generate-help.ts # Zod schemas → dist/help.json
104
+ └── 3. tsx generate-help.ts # Zod schemas → dist/help.json
106
105
  ```
107
106
 
108
- The collector bundle is a self-contained IIFE that exposes `window.__cssprobe-cli.collect(cfg)`. It runs entirely in the browser context — no Node imports.
107
+ The collector bundle is a self-contained IIFE that exposes `window.__cssprobe_cli.collect(cfg)`. It runs entirely in the browser context — no Node imports.
109
108
 
110
109
  ## Session Mode
111
110
 
package/README.md CHANGED
@@ -318,7 +318,7 @@ cssprobe-cli skill-uninstall
318
318
  ```bash
319
319
  npm install
320
320
  npm run build # tsc + esbuild collector + daemon entry + generate help.json
321
- npm test # Run unit tests (45 tests)
321
+ npm test # Run unit tests (58 tests)
322
322
  npx tsc --noEmit # Type check only
323
323
  ```
324
324
 
@@ -19,20 +19,8 @@ export declare class BrowserLauncher {
19
19
  constructor(options?: LauncherOptions);
20
20
  open(url: string): Promise<void>;
21
21
  collect(cfg: CollectConfig): Promise<Snapshot>;
22
- /** Measure the DOM tree depth from a root selector to the deepest leaf. */
23
- measureDepth(selector: string): Promise<number>;
24
- /** Wait for user to interact with the page. Press Enter to continue. */
25
- waitForUser(): Promise<void>;
26
22
  /** Navigate to a new URL in the existing page. */
27
23
  navigate(url: string): Promise<void>;
28
- /** Scan the page for likely root elements (modals, dialogs, scroll containers). */
29
- autoDetectRoot(): Promise<string[]>;
30
- /**
31
- * Interactive login flow: opens browser in headed mode, navigates to URL,
32
- * waits for user to complete login, then saves storage state.
33
- * Returns the path to the saved state file.
34
- */
35
- loginAndSave(url: string, statePath: string): Promise<string>;
36
24
  /** Get the current page object. */
37
25
  getPage(): Page | null;
38
26
  close(): Promise<void>;
@@ -63,6 +63,7 @@ class BrowserLauncher {
63
63
  this.options = options;
64
64
  }
65
65
  async open(url) {
66
+ await this.close();
66
67
  const engine = this.options.browser || 'chromium';
67
68
  const factory = engine === 'firefox' ? playwright_core_1.firefox :
68
69
  engine === 'webkit' ? playwright_core_1.webkit :
@@ -107,35 +108,6 @@ class BrowserLauncher {
107
108
  }, cfg);
108
109
  return snapshot;
109
110
  }
110
- /** Measure the DOM tree depth from a root selector to the deepest leaf. */
111
- async measureDepth(selector) {
112
- if (!this.page)
113
- throw new Error('Browser not opened. Call open() first.');
114
- return this.page.evaluate((sel) => {
115
- const root = document.querySelector(sel);
116
- if (!root)
117
- return 0;
118
- let maxDepth = 0;
119
- const walk = (el, depth) => {
120
- if (depth > maxDepth)
121
- maxDepth = depth;
122
- for (const child of Array.from(el.children))
123
- walk(child, depth + 1);
124
- };
125
- walk(root, 0);
126
- return maxDepth;
127
- }, selector);
128
- }
129
- /** Wait for user to interact with the page. Press Enter to continue. */
130
- async waitForUser() {
131
- console.error('');
132
- console.error(' Browser is open. Perform your actions (login, click buttons, open dialogs, etc.).');
133
- console.error(' Press Enter here when ready to inspect...');
134
- console.error('');
135
- await new Promise(resolve => {
136
- process.stdin.once('data', () => resolve());
137
- });
138
- }
139
111
  /** Navigate to a new URL in the existing page. */
140
112
  async navigate(url) {
141
113
  if (!this.page)
@@ -153,100 +125,6 @@ class BrowserLauncher {
153
125
  await this.page.goto(`file://${resolved}`, { waitUntil: 'load', timeout: 30000 });
154
126
  }
155
127
  }
156
- /** Scan the page for likely root elements (modals, dialogs, scroll containers). */
157
- async autoDetectRoot() {
158
- if (!this.page)
159
- throw new Error('Browser not opened. Call open() first.');
160
- return this.page.evaluate(() => {
161
- const candidates = [];
162
- const seen = new Set();
163
- function addSelector(sel, score) {
164
- if (seen.has(sel))
165
- return;
166
- seen.add(sel);
167
- candidates.push({ selector: sel, score });
168
- }
169
- // Modals / dialogs
170
- for (const el of Array.from(document.querySelectorAll('.s-kit-modal, [class*="dialog"], [class*="modal"], [class*="popup"]'))) {
171
- const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).join('.');
172
- if (cls)
173
- addSelector(`.${cls.split(/\s+/)[0]}`, 10);
174
- }
175
- // Scroll containers (overflow-y: auto/scroll with content overflow)
176
- for (const el of Array.from(document.querySelectorAll('*'))) {
177
- const cs = getComputedStyle(el);
178
- if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') &&
179
- el.scrollHeight > el.clientHeight + 1 &&
180
- el.clientHeight > 100) {
181
- const tag = el.tagName.toLowerCase();
182
- const id = el.id ? `#${el.id}` : '';
183
- const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
184
- const sel = id || (cls ? `${tag}.${cls}` : tag);
185
- addSelector(sel, 5);
186
- }
187
- }
188
- // Sections with substantial content
189
- for (const el of Array.from(document.querySelectorAll('section, [class*="section"], main, [role="main"]'))) {
190
- const tag = el.tagName.toLowerCase();
191
- const cls = (el.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 2).join('.');
192
- const sel = cls ? `${tag}.${cls}` : tag;
193
- addSelector(sel, 3);
194
- }
195
- return candidates
196
- .sort((a, b) => b.score - a.score)
197
- .slice(0, 5)
198
- .map(c => c.selector);
199
- });
200
- }
201
- /**
202
- * Interactive login flow: opens browser in headed mode, navigates to URL,
203
- * waits for user to complete login, then saves storage state.
204
- * Returns the path to the saved state file.
205
- */
206
- async loginAndSave(url, statePath) {
207
- const engine = this.options.browser || 'chromium';
208
- const factory = engine === 'firefox' ? playwright_core_1.firefox :
209
- engine === 'webkit' ? playwright_core_1.webkit :
210
- playwright_core_1.chromium;
211
- const execPath = findBrowserExecutable(engine);
212
- const launchOptions = { headless: false };
213
- if (execPath)
214
- launchOptions.executablePath = execPath;
215
- this.browser = await factory.launch(launchOptions);
216
- const vp = this.options.viewport || { width: 1280, height: 720 };
217
- this.context = await this.browser.newContext({ viewport: vp });
218
- this.page = await this.context.newPage();
219
- // Navigate to the target URL
220
- if (url.startsWith('http://') || url.startsWith('https://')) {
221
- await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
222
- }
223
- else {
224
- throw new Error('--login only works with http/https URLs');
225
- }
226
- // Wait for user to complete login (detect URL change or navigation)
227
- console.error('');
228
- console.error(' Browser opened. Please complete the login manually.');
229
- console.error(' The browser will detect when you navigate away from the login page,');
230
- console.error(' or you can press Enter here when done.');
231
- console.error('');
232
- // Wait for either URL change or Enter keypress
233
- await Promise.race([
234
- this.page.waitForURL('**/*', { timeout: 300000 }).catch(() => { }),
235
- new Promise(resolve => {
236
- process.stdin.once('data', () => resolve());
237
- }),
238
- ]);
239
- // Small delay to let cookies/localStorage settle
240
- await this.page.waitForTimeout(1000);
241
- // Save state
242
- const resolvedPath = path_1.default.resolve(statePath);
243
- const dir = path_1.default.dirname(resolvedPath);
244
- if (!fs_1.default.existsSync(dir))
245
- fs_1.default.mkdirSync(dir, { recursive: true });
246
- await this.context.storageState({ path: resolvedPath });
247
- console.error(` State saved to: ${resolvedPath}`);
248
- return resolvedPath;
249
- }
250
128
  /** Get the current page object. */
251
129
  getPage() {
252
130
  return this.page;
@@ -137,13 +137,11 @@ async function handleOpen(command, args, output) {
137
137
  state,
138
138
  _: ['open', url],
139
139
  }, 'open');
140
- // Wait a bit for daemon to start
141
- await new Promise(resolve => setTimeout(resolve, 1000));
142
- // Load session and navigate
143
- const session = await (0, session_1.loadSession)();
144
- if (session) {
145
- await session.run({ _: ['goto', url] });
146
- }
140
+ // Wait for the daemon to be ready (poll session file instead of fixed delay)
141
+ const session = await waitForSession(5000);
142
+ if (!session)
143
+ throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
144
+ await session.run({ _: ['goto', url] });
147
145
  const result = {
148
146
  sessionId: 'default',
149
147
  pid,
@@ -164,6 +162,16 @@ async function handleOpen(command, args, output) {
164
162
  output.error(e instanceof Error ? e.message : String(e));
165
163
  }
166
164
  }
165
+ async function waitForSession(timeoutMs) {
166
+ const deadline = Date.now() + timeoutMs;
167
+ while (Date.now() < deadline) {
168
+ const session = await (0, session_1.loadSession)();
169
+ if (session && await session.canConnect())
170
+ return session;
171
+ await new Promise(resolve => setTimeout(resolve, 100));
172
+ }
173
+ return undefined;
174
+ }
167
175
  async function handleClose(output) {
168
176
  try {
169
177
  const session = await (0, session_1.loadSession)();
@@ -255,7 +263,10 @@ async function handleSessionCommand(commandName, command, args, output) {
255
263
  }
256
264
  }
257
265
  // Run command in session
258
- const result = await session.run({ ...cmdArgs, _: daemonArgs });
266
+ const runArgs = { ...cmdArgs, _: daemonArgs };
267
+ if (args.json)
268
+ runArgs.json = true;
269
+ const result = await session.run(runArgs);
259
270
  if (output.json) {
260
271
  try {
261
272
  console.log(JSON.parse(result.text));
@@ -375,7 +386,7 @@ function parseNetscapeCookies(content) {
375
386
  const parts = trimmed.split('\t');
376
387
  if (parts.length < 7)
377
388
  continue;
378
- const [domain, flag, path, secure, expires, name, ...valueParts] = parts;
389
+ const [domain, httpOnly, path, secure, expires, name, ...valueParts] = parts;
379
390
  const value = valueParts.join('\t');
380
391
  const expiresNum = parseInt(expires, 10);
381
392
  cookies.push({
@@ -384,7 +395,7 @@ function parseNetscapeCookies(content) {
384
395
  domain,
385
396
  path: path || '/',
386
397
  expires: isNaN(expiresNum) || expiresNum === 0 ? -1 : expiresNum,
387
- httpOnly: false,
398
+ httpOnly: httpOnly === 'TRUE',
388
399
  secure: secure === 'TRUE',
389
400
  sameSite: secure === 'TRUE' ? 'None' : 'Lax',
390
401
  });
@@ -24,6 +24,34 @@ var __cssprobe_cli = (() => {
24
24
  classifyShape: () => classifyShape,
25
25
  collect: () => collect
26
26
  });
27
+
28
+ // src/engine/specificity.ts
29
+ function specificity(selector) {
30
+ if (selector === "inline") return Number.MAX_SAFE_INTEGER;
31
+ let score = 0;
32
+ for (const token of selector.split(/[ >+~]+/)) {
33
+ const ids = (token.match(/#[\w-]+/g) || []).length;
34
+ const attrsClasses = (token.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
35
+ const types = (token.match(/^[\w-]+|::[\w-]+/g) || []).length;
36
+ score += ids * 100 + attrsClasses * 10 + types;
37
+ }
38
+ return score;
39
+ }
40
+ function pickWinning(arr) {
41
+ if (arr.length === 0) return arr[0];
42
+ let winner = arr[0];
43
+ let best = -1;
44
+ for (const d of arr) {
45
+ const s = d.selector === "inline" ? Number.MAX_SAFE_INTEGER : specificity(d.selector);
46
+ if (s >= best) {
47
+ best = s;
48
+ winner = d;
49
+ }
50
+ }
51
+ return winner;
52
+ }
53
+
54
+ // src/engine/collector.ts
27
55
  var WANTED_PROPS = [
28
56
  // layout
29
57
  "height",
@@ -149,8 +177,7 @@ var __cssprobe_cli = (() => {
149
177
  const getDeclared = (prop) => {
150
178
  const arr = declared && declared[prop];
151
179
  if (!arr || arr.length === 0) return null;
152
- const inline = arr.find((d) => d.selector === "inline");
153
- return inline ? inline.value : arr[0].value;
180
+ return pickWinning(arr).value;
154
181
  };
155
182
  const sizeClass = (prop, maxProp) => {
156
183
  const val = getDeclared(prop);
@@ -309,7 +336,7 @@ var __cssprobe_cli = (() => {
309
336
  tree: null,
310
337
  crossOriginBlocked: crossOriginBlocked.count,
311
338
  blockedSheetUrls: blockedSheetUrls.slice(0, 5),
312
- error: `\u672A\u627E\u5230 ${cfg.rootSelector}`,
339
+ error: `Not found: ${cfg.rootSelector}`,
313
340
  candidates
314
341
  };
315
342
  }
@@ -16,6 +16,7 @@ const socketConnection_1 = require("../utils/socketConnection");
16
16
  const launcher_1 = require("../browser/launcher");
17
17
  const analyzer_1 = require("../engine/analyzer");
18
18
  const renderer_1 = require("../engine/renderer");
19
+ const dom_tree_1 = require("../engine/renderers/dom-tree");
19
20
  const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
20
21
  async function startDaemon(sessionName, clientInfo, options) {
21
22
  const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
@@ -110,7 +111,7 @@ async function executeCommand(launcher, args) {
110
111
  const lines = [];
111
112
  lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
112
113
  lines.push('```');
113
- renderNode(snapshot.tree, lines, 0);
114
+ lines.push(...(0, dom_tree_1.renderNode)(snapshot.tree, 0));
114
115
  lines.push('```');
115
116
  return { text: lines.join('\n') };
116
117
  }
@@ -143,8 +144,7 @@ async function executeCommand(launcher, args) {
143
144
  return { text: 'No warnings or errors found.' };
144
145
  }
145
146
  const lines = important.map(f => {
146
- const icon = f.level === 'error' ? '⚠' : '⚠';
147
- return `- ${icon} ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ''}`;
147
+ return `- ⚠ ${f.message} [${f.confidence}]${f.location ? ` @ ${f.location}` : ''}`;
148
148
  });
149
149
  return { text: lines.join('\n') };
150
150
  }
@@ -162,7 +162,7 @@ async function executeCommand(launcher, args) {
162
162
  const page = launcher.getPage();
163
163
  if (!page)
164
164
  throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
165
- const buffer = await page.screenshot({ fullPage: args.fullPage });
165
+ const buffer = await page.screenshot({ fullPage: args['full-page'] });
166
166
  const base64 = buffer.toString('base64');
167
167
  return { text: `data:image/png;base64,${base64}` };
168
168
  }
@@ -204,25 +204,6 @@ async function executeCommand(launcher, args) {
204
204
  throw new Error(`Unknown command: ${command}`);
205
205
  }
206
206
  }
207
- function renderNode(node, lines, indent) {
208
- if (!node)
209
- return;
210
- const cls = node.classes?.length ? `.${node.classes.join('.')}` : '';
211
- const id = node.id ? `#${node.id}` : '';
212
- const m = node.metrics;
213
- const s = node.shape || {};
214
- const shapeStr = s.role ? `[${s.role} h:${s.heightStrategy} w:${s.widthStrategy}]` : '';
215
- const flag = [];
216
- if (node.flags?.overflowsParent)
217
- flag.push('⚠parent-overflow');
218
- if (node.flags?.overflowsViewport)
219
- flag.push('⚠viewport-overflow');
220
- if (m?.clientHeight === 0 && m?.offsetHeight === 0)
221
- flag.push('⚠collapsed');
222
- lines.push(`${' '.repeat(indent)}<${node.tag}${id}${cls}> ${shapeStr} [${node.props?.position},${node.props?.display}] rect(${m?.rect?.width}×${m?.rect?.height})${flag.length ? ' ' + flag.join(' ') : ''}`);
223
- for (const child of node.children || [])
224
- renderNode(child, lines, indent + 1);
225
- }
226
207
  async function socketExists(socketPath) {
227
208
  try {
228
209
  const stat = await fs_1.default.promises.stat(socketPath);
@@ -13,7 +13,20 @@ const options = {
13
13
  headed: args.includes('--headed'),
14
14
  browser: args.find(a => a.startsWith('--browser='))?.split('=')[1],
15
15
  state: args.find(a => a.startsWith('--state='))?.split('=')[1],
16
+ viewport: parseViewportArg(args.find(a => a.startsWith('--viewport='))?.split('=')[1]),
16
17
  };
18
+ function parseViewportArg(raw) {
19
+ if (!raw)
20
+ return undefined;
21
+ const m = raw.match(/^(\d+)[xX*](\d+)$/);
22
+ if (!m)
23
+ return undefined;
24
+ const width = Number(m[1]);
25
+ const height = Number(m[2]);
26
+ if (width > 0 && height > 0)
27
+ return { width, height };
28
+ return undefined;
29
+ }
17
30
  async function main() {
18
31
  try {
19
32
  const clientInfo = (0, session_1.createClientInfo)();
@@ -50,10 +50,18 @@ class Session {
50
50
  }
51
51
  async _connect() {
52
52
  return await new Promise(resolve => {
53
+ let settled = false;
53
54
  const socket = net_1.default.createConnection(this.config.socketPath, () => {
55
+ settled = true;
54
56
  resolve({ socket });
55
57
  });
56
58
  socket.on('error', error => {
59
+ if (settled) {
60
+ socket.destroy();
61
+ return;
62
+ }
63
+ // Only clean up the socket file when the daemon is confirmed not listening
64
+ // (ECONNREFUSED / ENOENT). Do not unlink on transient failures of a live socket.
57
65
  if (os_1.default.platform() !== 'win32')
58
66
  void fs_1.default.promises.unlink(this.config.socketPath).catch(() => { }).then(() => resolve({ error }));
59
67
  else
@@ -86,6 +94,8 @@ class Session {
86
94
  daemonArgs.push(`--browser=${args.browser}`);
87
95
  if (args.state)
88
96
  daemonArgs.push(`--state=${args.state}`);
97
+ if (args.viewport)
98
+ daemonArgs.push(`--viewport=${args.viewport.width}x${args.viewport.height}`);
89
99
  const child = require('child_process').spawn(process.execPath, daemonArgs, {
90
100
  detached: true,
91
101
  stdio: ['ignore', 'pipe', err],
@@ -3,13 +3,14 @@
3
3
  // No browser or filesystem access — directly unit-testable.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.analyze = analyze;
6
+ const specificity_1 = require("./specificity");
6
7
  // ─── Confidence helpers ───
7
- /** Pick the winning declared value for a property (inline wins, else first rule). */
8
+ /** Pick the winning declared value for a property (inline wins, else highest specificity). */
8
9
  function declaredFor(node, prop) {
9
10
  const arr = node.declared && node.declared[prop];
10
11
  if (!arr || arr.length === 0)
11
12
  return null;
12
- return arr.find(d => d.selector === 'inline') || arr[0];
13
+ return (0, specificity_1.pickWinning)(arr);
13
14
  }
14
15
  /**
15
16
  * Confidence of a declaration-backed claim:
@@ -66,9 +67,6 @@ function makeFinding(partial) {
66
67
  };
67
68
  }
68
69
  // ─── Ancestors ───
69
- function findAncestor(ancestors, predicate) {
70
- return ancestors.find(predicate);
71
- }
72
70
  function allAncestorsContentSized(ancestors) {
73
71
  return ancestors.length > 0 && ancestors.every(a => a.shape.heightStrategy === 'content');
74
72
  }
@@ -238,7 +236,7 @@ function analyzeFlex(snapshot) {
238
236
  const pLabel = nodeLabel(parent);
239
237
  const cLabel = nodeLabel(child);
240
238
  const pDir = parent.props.flexDirection || 'row';
241
- const cMinH = parent.props.minHeight;
239
+ const cMinH = child.props.minHeight;
242
240
  const cMinW = child.props.minWidth;
243
241
  const cFlexGrow = child.props.flexGrow;
244
242
  const cFlexShrink = child.props.flexShrink;
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.classifyShape = classifyShape;
7
7
  exports.collect = collect;
8
+ const specificity_1 = require("./specificity");
8
9
  // ─── Declared-value scanning ───
9
10
  const WANTED_PROPS = [
10
11
  // layout
@@ -101,8 +102,7 @@ function classifyShape(cs, declared) {
101
102
  const arr = declared && declared[prop];
102
103
  if (!arr || arr.length === 0)
103
104
  return null;
104
- const inline = arr.find(d => d.selector === 'inline');
105
- return inline ? inline.value : arr[0].value;
105
+ return (0, specificity_1.pickWinning)(arr).value;
106
106
  };
107
107
  const sizeClass = (prop, maxProp) => {
108
108
  const val = getDeclared(prop);
@@ -256,7 +256,7 @@ function collect(cfg) {
256
256
  tree: null,
257
257
  crossOriginBlocked: crossOriginBlocked.count,
258
258
  blockedSheetUrls: blockedSheetUrls.slice(0, 5),
259
- error: `未找到 ${cfg.rootSelector}`,
259
+ error: `Not found: ${cfg.rootSelector}`,
260
260
  candidates,
261
261
  };
262
262
  }
@@ -7,7 +7,7 @@ function renderFindings(findings) {
7
7
  const lines = [];
8
8
  lines.push('## Findings');
9
9
  if (findings.length === 0) {
10
- lines.push('- 无异常发现');
10
+ lines.push('- No issues found');
11
11
  return lines;
12
12
  }
13
13
  for (const f of findings) {
@@ -0,0 +1,6 @@
1
+ export interface DeclaredLike {
2
+ selector: string;
3
+ value: string;
4
+ }
5
+ export declare function specificity(selector: string): number;
6
+ export declare function pickWinning<T extends DeclaredLike>(arr: T[]): T;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.specificity = specificity;
4
+ exports.pickWinning = pickWinning;
5
+ function specificity(selector) {
6
+ if (selector === 'inline')
7
+ return Number.MAX_SAFE_INTEGER;
8
+ let score = 0;
9
+ for (const token of selector.split(/[ >+~]+/)) {
10
+ const ids = (token.match(/#[\w-]+/g) || []).length;
11
+ const attrsClasses = (token.match(/\.[\w-]+|\[[^\]]*\]|:(?!:)[\w-]+/g) || []).length;
12
+ const types = (token.match(/^[\w-]+|::[\w-]+/g) || []).length;
13
+ score += ids * 100 + attrsClasses * 10 + types;
14
+ }
15
+ return score;
16
+ }
17
+ function pickWinning(arr) {
18
+ if (arr.length === 0)
19
+ return arr[0];
20
+ let winner = arr[0];
21
+ let best = -1;
22
+ for (const d of arr) {
23
+ const s = d.selector === 'inline' ? Number.MAX_SAFE_INTEGER : specificity(d.selector);
24
+ if (s >= best) {
25
+ best = s;
26
+ winner = d;
27
+ }
28
+ }
29
+ return winner;
30
+ }
31
+ //# sourceMappingURL=specificity.js.map