diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a1641a7..8a40dca 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -37,4 +37,30 @@ jobs: with: name: css-coverage path: css-coverage/*.json - retention-days: 30 \ No newline at end of file + retention-days: 30 + + css-coverage: + name: Verify CSS Code Coverage + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + cache: 'npm' + node-version: '>=22.18.0' + + - name: Install dependencies + run: npm install --no-fund --no-audit --ignore-scripts + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: css-coverage + path: ./css-coverage + + - name: Analyze CSS Code Coverage + run: node scripts/analyze-css-coverage.ts --coverageDir=./css-coverage --minLineCoverage=.8 --minFileLineCoverage=.62 --showUncovered=violations diff --git a/scripts/analyze-css-coverage.ts b/scripts/analyze-css-coverage.ts new file mode 100644 index 0000000..a713940 --- /dev/null +++ b/scripts/analyze-css-coverage.ts @@ -0,0 +1,180 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import { calculate_coverage } from '../src/lib/components/coverage/calculate-coverage.ts' +import { DOMParser } from 'linkedom' +import type { Coverage } from '../src/lib/components/coverage/types.ts' +import { parseArgs, styleText } from 'node:util' +import * as v from 'valibot' +import { parse_json } from '../src/lib/components/coverage/parse-coverage.ts' + +// TODO: architecture +// Create CLI that +// - creates cli config +// - validates cli input against schema +// Create Program() that +// - takes input (parsed and validated args) +// - runs and returns calculations +// - compares it with arguments passed +// - returns a Result (passed: boolean, violations, etc.) +// Create a printer that accepts a formatter +// - formats can be tap, pretty (default), minimal etc. + +let args = process.argv.slice(2) + +let { values } = parseArgs({ + args, + allowPositionals: true, + options: { + // TODO: allow glob? + // TODO: convert to coveragedir, min-line-coverage, etc. + coverageDir: { + type: 'string' + }, + minLineCoverage: { + type: 'string' + }, + minFileLineCoverage: { + type: 'string', + default: '0' + }, + showUncovered: { + type: 'string', + default: 'none' + } + } +}) + +const showUncoveredOptions = { + none: 'none', + all: 'all', + violations: 'violations' +} as const + +let valuesSchema = v.object({ + coverageDir: v.pipe(v.string(), v.nonEmpty()), + // Coerce args string to number and validate that it's between 0 and 1 + minLineCoverage: v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1)), + // Coerce args string to number and validate that it's between 0 and 1 + minFileLineCoverage: v.optional(v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1))), + showUncovered: v.optional(v.pipe(v.string(), v.enum(showUncoveredOptions)), 'none') +}) + +let parse_result = v.safeParse(valuesSchema, values) +if (!parse_result.success) { + console.error(styleText(['red', 'bold'], 'Failure'), ': invalid arguments') + for (let issue of parse_result.issues) { + console.error(`- ${issue.path?.map((p) => p.key).join('.')}: ${issue.message}`) + } + process.exit(1) +} +let { coverageDir, minLineCoverage, minFileLineCoverage, showUncovered } = parse_result.output + +function parse_html(html: string) { + return new DOMParser().parseFromString(html, 'text/html') +} + +let files = fs.readdirSync(coverageDir) + +if (files.length === 0) { + console.error(styleText(['red', 'bold'], 'Failure'), `: no JSON files found in ${coverageDir}`) + process.exit(1) +} + +console.log(`Checking ${files.length} files...`) + +let data = files.reduce((all_files, file_path) => { + if (!file_path.endsWith('.json')) return all_files + try { + let content = fs.readFileSync(path.resolve(coverageDir, file_path), 'utf-8') + let parsed = parse_json(content) + all_files.push(...parsed) + return all_files + } catch { + return all_files + } +}, [] as Coverage[]) + +let result = calculate_coverage(data, parse_html) + +console.log(`Analyzed ${result.files_found} coverage entries`) + +// Verify minLineCoverage +if (result.line_coverage_ratio >= minLineCoverage) { + console.log( + `${styleText(['bold', 'green'], 'Success')}: total line coverage is ${(result.line_coverage_ratio * 100).toFixed(2)}%` + ) +} else { + console.error( + `${styleText(['bold', 'red'], 'Failed')}: line coverage is ${(result.line_coverage_ratio * 100).toFixed(2)}% which is lower than the threshold of ${minLineCoverage}` + ) + process.exitCode = 1 +} + +// Verify minFileLineCoverage +if (minFileLineCoverage !== undefined && minFileLineCoverage !== 0) { + if (result.coverage_per_stylesheet.some((sheet) => sheet.line_coverage_ratio < minFileLineCoverage)) { + console.error( + `${styleText(['bold', 'red'], 'Failed')}: Not all files meet the minimum line coverage of ${minFileLineCoverage * 100}%:` + ) + process.exitCode = 1 + } else { + console.log( + `${styleText(['bold', 'green'], 'Success')}: all files pass minFileLineCoverage of ${minFileLineCoverage * 100}%` + ) + } +} + +if (showUncovered !== 'none') { + const NUM_LEADING_LINES = 3 + const NUM_TRAILING_LINES = NUM_LEADING_LINES + let terminal_width = process.stdout.columns || 80 + let line_number = (num: number, covered: boolean = true) => + `${num.toString().padStart(5, ' ')} ${covered ? '│' : '☓'} ` + + for (let sheet of result.coverage_per_stylesheet) { + if ( + (sheet.line_coverage_ratio !== 1 && showUncovered === 'all') || + (minFileLineCoverage !== undefined && + minFileLineCoverage !== 0 && + sheet.line_coverage_ratio < minFileLineCoverage && + showUncovered === 'violations') + ) { + console.log() + console.log(styleText('dim', '─'.repeat(terminal_width))) + console.log(sheet.url) + console.log( + `Coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%, ${sheet.covered_lines}/${sheet.total_lines} lines covered` + ) + if (minFileLineCoverage) { + let lines_to_cover = minFileLineCoverage * sheet.total_lines - sheet.covered_lines + console.log( + `💡 Cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${minFileLineCoverage * 100}%` + ) + } + console.log(styleText('dim', '─'.repeat(terminal_width))) + + let lines = sheet.text.split('\n') + let line_coverage = sheet.line_coverage + + for (let i = 0; i < lines.length; i++) { + if (line_coverage[i] === 0) { + // Rewind cursor N lines to render N previous lines + for (let j = i - NUM_LEADING_LINES; j < i; j++) { + console.log(styleText('dim', line_number(j)), styleText('dim', lines[j])) + } + // Render uncovered lines while increasing cursor until reaching next covered block + while (line_coverage[i] === 0) { + console.log(styleText('red', line_number(i, false)), lines[i]) + i++ + } + // Forward cursor N lines to render N trailing lines + for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) { + console.log(styleText('dim', line_number(i)), styleText('dim', lines[i])) + } + // Show empty line between blocks + console.log() + } + } + } + } +} diff --git a/src/lib/components/coverage/Coverage.spec.ts b/src/lib/components/coverage/Coverage.spec.ts index a34339c..04ad5bc 100644 --- a/src/lib/components/coverage/Coverage.spec.ts +++ b/src/lib/components/coverage/Coverage.spec.ts @@ -57,7 +57,7 @@ test.describe('loading example file', () => { .toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css') await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB') await expect.soft(first_row.getByRole('cell').nth(2)).toHaveAccessibleName('2,619') - await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%') + await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%') // Elements in correct state: await expect.soft(table.getByRole('row').nth(1)).toHaveAttribute('aria-selected', 'true') @@ -73,8 +73,8 @@ test.describe('loading example file', () => { await expect.soft(ranges.nth(3)).toHaveClass(/uncovered/) await expect.soft(ranges.nth(0)).toHaveText('1 2 3 4 5 6 7 8 9 10 11 12 13 14') - await expect.soft(ranges.nth(1)).toHaveText('15 16 17 18') - await expect.soft(ranges.nth(3)).toHaveText('51 52 53 54 55 56') + await expect.soft(ranges.nth(1)).toHaveText('15 16 17') + await expect.soft(ranges.nth(3)).toHaveText('51 52 53 54 55') }) test('selecting a row', async ({ page }) => { @@ -109,7 +109,7 @@ test.describe('loading example file', () => { .soft(first_row.getByRole('cell').nth(0)) .toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css') await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB') - await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%') + await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%') }) test('sorting by total size', async ({ page }) => { @@ -123,7 +123,7 @@ test.describe('loading example file', () => { .soft(first_row.getByRole('cell').nth(0)) .toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css') await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB') - await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%') + await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%') let css_content = await page.getByTestId('pre-css').textContent() expect.soft(css_content).toContain('.logo.svelte-1jiwtxp {') diff --git a/src/lib/components/coverage/Coverage.svelte b/src/lib/components/coverage/Coverage.svelte index e2faedd..53756f6 100644 --- a/src/lib/components/coverage/Coverage.svelte +++ b/src/lib/components/coverage/Coverage.svelte @@ -60,7 +60,9 @@ return sort_direction === 'asc' ? a.total_bytes - b.total_bytes : b.total_bytes - a.total_bytes } if (sort_by === 'coverage') { - return sort_direction === 'asc' ? a.coverage_ratio - b.coverage_ratio : b.coverage_ratio - a.coverage_ratio + return sort_direction === 'asc' + ? a.line_coverage_ratio - b.line_coverage_ratio + : b.line_coverage_ratio - a.line_coverage_ratio } if (sort_by === 'name') { return sort_direction === 'asc' ? string_sort(a.url, b.url) : string_sort(b.url, a.url) @@ -110,12 +112,12 @@
Coverage
-
{format_percentage(calculated.coverage_ratio)}
-
{format_percentage(calculated.line_coverage)} of lines
+
{format_percentage(calculated.byte_coverage_ratio)}
+
{format_percentage(calculated.line_coverage_ratio)} of lines
Total
-
{format_filesize(calculated.used_bytes + calculated.unused_bytes)}
+
{format_filesize(calculated.total_bytes)}
{format_number(calculated.total_lines)} lines
@@ -152,17 +154,17 @@ {#each sorted_items as item_index, index} {@const stylesheet = calculated.coverage_per_stylesheet[item_index]} - {@const { url, total_bytes, total_lines, coverage_ratio, covered_lines } = stylesheet} + {@const { url, total_bytes, total_lines, line_coverage_ratio, covered_lines } = stylesheet} {url} {format_filesize(total_bytes)} {format_number(total_lines)} - {format_percentage(coverage_ratio)} + {format_percentage(line_coverage_ratio)}
- +
diff --git a/src/lib/components/coverage/calculate-coverage.spec.ts b/src/lib/components/coverage/calculate-coverage.spec.ts index 7059796..82466cb 100644 --- a/src/lib/components/coverage/calculate-coverage.spec.ts +++ b/src/lib/components/coverage/calculate-coverage.spec.ts @@ -170,12 +170,12 @@ test.describe('calculates coverage', () => { let result = calculate_coverage(coverage, html_parser) expect.soft(result.files_found).toBe(1) expect.soft(result.total_bytes).toBe(80) - expect.soft(result.used_bytes).toBe(37) - expect.soft(result.unused_bytes).toBe(41) + expect.soft(result.used_bytes).toBe(42) + expect.soft(result.unused_bytes).toBe(38) expect.soft(result.total_lines).toBe(11) expect.soft(result.covered_lines).toBe(7) expect.soft(result.uncovered_lines).toBe(11 - 7) - expect.soft(result.line_coverage).toBe(7 / 11) + expect.soft(result.line_coverage_ratio).toBe(7 / 11) }) test('calculates stats per stylesheet', () => { @@ -189,7 +189,7 @@ test.describe('calculates coverage', () => { expect.soft(sheet.total_lines).toBe(11) expect.soft(sheet.covered_lines).toBe(7) expect.soft(sheet.uncovered_lines).toBe(4) - expect.soft(sheet.coverage_ratio).toBe(7 / 11) + expect.soft(sheet.line_coverage_ratio).toBe(7 / 11) expect.soft(sheet.line_coverage).toEqual(new Uint8Array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1])) }) }) @@ -226,12 +226,12 @@ test.describe('calculates coverage', () => { let result = calculate_coverage(coverage, html_parser) expect.soft(result.files_found).toBe(1) expect.soft(result.total_bytes).toBe(174) - expect.soft(result.used_bytes).toBe(75) - expect.soft(result.unused_bytes).toBe(93) + expect.soft(result.used_bytes).toBe(91) + expect.soft(result.unused_bytes).toBe(83) expect.soft(result.total_lines).toBe(21) expect.soft(result.covered_lines).toBe(12) expect.soft(result.uncovered_lines).toBe(21 - 12) - expect.soft(result.line_coverage).toBe(12 / 21) + expect.soft(result.line_coverage_ratio).toBe(12 / 21) }) test('calculates stats per stylesheet', () => { @@ -247,7 +247,7 @@ test.describe('calculates coverage', () => { expect.soft(sheet.total_lines).toBe(21) expect.soft(sheet.covered_lines).toBe(12) expect.soft(sheet.uncovered_lines).toBe(21 - 12) - expect.soft(sheet.coverage_ratio).toBe(12 / 21) + expect.soft(sheet.line_coverage_ratio).toBe(12 / 21) expect .soft(sheet.line_coverage) .toEqual(new Uint8Array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1])) @@ -284,10 +284,10 @@ test.describe('calculates coverage', () => { test('counts totals', () => { let result = calculate_coverage(coverage, html_parser) - expect.soft(result.covered_lines).toBe(8) - expect.soft(result.uncovered_lines).toBe(6) + expect.soft(result.covered_lines).toBe(9) + expect.soft(result.uncovered_lines).toBe(5) expect.soft(result.total_lines).toBe(14) - expect.soft(result.line_coverage).toBe(8 / 14) + expect.soft(result.line_coverage_ratio).toBe(9 / 14) }) test('extracts and formats css', () => { @@ -318,11 +318,11 @@ test.describe('calculates coverage', () => { // h1 {} 1, 1, 1, 1, // comment + p {} - 0, 0, 0, 0, 0, + 0, 0, 0, 0, // @media 1, // h1 { - 0, + 1, 0, // color: green; } 1, 1, 1 ]) diff --git a/src/lib/components/coverage/calculate-coverage.ts b/src/lib/components/coverage/calculate-coverage.ts index 385e79c..f029d26 100644 --- a/src/lib/components/coverage/calculate-coverage.ts +++ b/src/lib/components/coverage/calculate-coverage.ts @@ -1,6 +1,6 @@ -import type { Coverage, Range } from './types' -import { prettify } from './prettify' -import { ext } from './ext' +import type { Coverage, Range } from './types.ts' +import { prettify } from './prettify.ts' +import { ext } from './ext.ts' import type { HTMLDocument } from 'linkedom/types/html/document' interface HtmlParser { @@ -127,6 +127,29 @@ export function deduplicate_entries( return checked_stylesheets } +type CoverageData = { + unused_bytes: number + used_bytes: number + total_bytes: number + line_coverage_ratio: number + byte_coverage_ratio: number + total_lines: number + covered_lines: number + uncovered_lines: number +} + +export type StylesheetCoverage = CoverageData & { + url: string + text: string + ranges: Range[] + line_coverage: Uint8Array +} + +export type CoverageResult = CoverageData & { + files_found: number + coverage_per_stylesheet: StylesheetCoverage[] +} + /** * @description * CSS Code Coverage calculation @@ -138,99 +161,83 @@ export function deduplicate_entries( * 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed) * 5. Calculate line-coverage, byte-coverage per stylesheet */ - -// TODO: add flag for prettification on/off -// When disabled we can skip the prettify step as well as recalculating the ranges in HTML (get_css_and_ranges_from_html) -// This also means that when pretty=true, parse_html MUST also be included. parse_html is optional when pretty=false -export function calculate_coverage(browser_coverage: Coverage[], parse_html: HtmlParser) { - let total_bytes = 0 - let used_bytes = 0 - let unused_bytes = 0 - let total_lines = 0 - let covered_lines = 0 - let uncovered_lines = 0 - let files_found = browser_coverage.length - let filtered_coverage = filter_coverage(browser_coverage, parse_html) +export function calculate_coverage(coverage: Coverage[], parse_html: HtmlParser): CoverageResult { + let files_found = coverage.length + let filtered_coverage = filter_coverage(coverage, parse_html) let prettified_coverage = prettify(filtered_coverage) let deduplicated = deduplicate_entries(prettified_coverage) - // SECTION: calculate used vs. unused bytes - // We sort the ranges by their start position - // Then we iterate over the ranges and calculate the used bytes - for (let [text, { ranges }] of deduplicated) { - total_bytes += text.length - let last_position = 0 - ranges.sort((a, b) => a.start - b.start) - for (let range of ranges) { - if (range.start > last_position) { - let unused_text = text.slice(last_position, range.start) - unused_bytes += unused_text.length - } - used_bytes += range.end - range.start - 1 - last_position = range.end - } - } - // SECTION: calculate coverage for each individual stylesheet we found let coverage_per_stylesheet = Array.from(deduplicated).map(([text, { url, ranges }]) => { - let file_used_bytes = ranges.reduce((acc, range) => acc + (range.end - range.start), 0) - let trimmed_text = text.trim() + function is_line_covered(line: string, start_offset: number) { + let end = start_offset + line.length + let next_offset = end + 1 // account for newline character + let is_empty = /^\s*$/.test(line) + let is_closing_brace = line.endsWith('}') - let lines = trimmed_text.split('\n') + if (!is_empty && !is_closing_brace) { + for (let range of ranges) { + if (range.start > end || range.end < start_offset) { + continue + } + if (range.start <= start_offset && range.end >= end) { + return true + } else if (line.startsWith('@') && range.start > start_offset && range.start < next_offset) { + return true + } + } + } + return false + } + + let lines = text.split('\n') let total_file_lines = lines.length let line_coverage = new Uint8Array(total_file_lines) let file_lines_covered = 0 + let file_total_bytes = text.length + let file_bytes_covered = 0 let offset = 0 - let index = 0 - for (let line of lines) { + + for (let index = 0; index < lines.length; index++) { + let line = lines[index] let start = offset let end = offset + line.length let next_offset = end + 1 // +1 for the newline character - let is_in_range = false - let trimmed_line = line.trim() - let is_empty = trimmed_line.length === 0 - let is_closing_brace = !is_empty && trimmed_line === '}' + let is_empty = /^\s*$/.test(line) + let is_closing_brace = line.endsWith('}') + let is_in_range = is_line_covered(line, start) + let is_covered = false - if (!is_empty && !is_closing_brace) { - for (let range of ranges) { - if (range.start <= start && range.end >= end) { - is_in_range = true - break - } else if (trimmed_line.startsWith('@') && range.start > start && range.start < next_offset) { - is_in_range = true - break - } - } - } - - let prev_is_covered = index > 0 ? line_coverage[index - 1] === 1 : false + let prev_is_covered = index > 0 && line_coverage[index - 1] === 1 if (is_in_range && !is_closing_brace && !is_empty) { - file_lines_covered++ - line_coverage[index] = 1 + is_covered = true } else if ((is_empty || is_closing_brace) && prev_is_covered) { + is_covered = true + } else if (is_empty && !prev_is_covered && is_line_covered(lines[index + 1], next_offset)) { + // If the next line is covered, mark this empty line as covered + is_covered = true + } + + line_coverage[index] = is_covered ? 1 : 0 + + if (is_covered) { file_lines_covered++ - line_coverage[index] = 1 - } else if (is_empty && line_coverage[index - 1] === 0) { - line_coverage[index] = 0 - } else { - line_coverage[index] = 0 + file_bytes_covered += line.length + 1 } + offset = next_offset - index++ } - total_lines += total_file_lines - covered_lines += file_lines_covered - uncovered_lines += total_file_lines - file_lines_covered - return { url, - text: trimmed_text, + text, ranges, - used_bytes: file_used_bytes, - total_bytes: trimmed_text.length, - coverage_ratio: file_lines_covered / total_file_lines, + unused_bytes: file_total_bytes - file_bytes_covered, + used_bytes: file_bytes_covered, + total_bytes: file_total_bytes, + line_coverage_ratio: file_lines_covered / total_file_lines, + byte_coverage_ratio: file_bytes_covered / file_total_bytes, line_coverage, total_lines: total_file_lines, covered_lines: file_lines_covered, @@ -238,19 +245,37 @@ export function calculate_coverage(browser_coverage: Coverage[], parse_html: Htm } }) - let coverage_ratio = - coverage_per_stylesheet.reduce((acc, sheet) => acc + sheet.coverage_ratio, 0) / coverage_per_stylesheet.length + let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = + coverage_per_stylesheet.reduce( + (totals, sheet) => { + totals.total_lines += sheet.total_lines + totals.total_covered_lines += sheet.covered_lines + totals.total_uncovered_lines += sheet.uncovered_lines + totals.total_bytes += sheet.total_bytes + totals.total_used_bytes += sheet.used_bytes + totals.total_unused_bytes += sheet.unused_bytes + return totals + }, + { + total_lines: 0, + total_covered_lines: 0, + total_uncovered_lines: 0, + total_bytes: 0, + total_used_bytes: 0, + total_unused_bytes: 0 + } + ) return { files_found, total_bytes, total_lines, - used_bytes, - covered_lines, - unused_bytes, - uncovered_lines, - coverage_ratio, - line_coverage: covered_lines / total_lines, + used_bytes: total_used_bytes, + covered_lines: total_covered_lines, + unused_bytes: total_unused_bytes, + uncovered_lines: total_uncovered_lines, + byte_coverage_ratio: total_used_bytes / total_bytes, + line_coverage_ratio: total_covered_lines / total_lines, coverage_per_stylesheet } } diff --git a/src/lib/components/coverage/parse-coverage.ts b/src/lib/components/coverage/parse-coverage.ts new file mode 100644 index 0000000..acd5e79 --- /dev/null +++ b/src/lib/components/coverage/parse-coverage.ts @@ -0,0 +1,25 @@ +import * as v from 'valibot' +import type { Coverage } from './types' + +let CoverageSchema = v.array( + v.object({ + text: v.undefinedable(v.string()), + url: v.string(), + ranges: v.array( + v.object({ + start: v.number(), + end: v.number() + }) + ) + }) +) + +export function parse_json(input: string) { + try { + let parse_result = JSON.parse(input) + v.parse(CoverageSchema, parse_result) + return parse_result as Coverage[] + } catch { + return [] as Coverage[] + } +} diff --git a/src/lib/components/coverage/prettify.ts b/src/lib/components/coverage/prettify.ts index 71ee73d..8d8a23d 100644 --- a/src/lib/components/coverage/prettify.ts +++ b/src/lib/components/coverage/prettify.ts @@ -37,46 +37,44 @@ export function prettify(coverage: Coverage[]): Coverage[] { let index = 0 tokenize(text, (type, start, end) => { - if (!irrelevant_tokens.has(type)) { - index++ + if (irrelevant_tokens.has(type)) return + index++ - // format-css changes the Url token to a Function,String,RightParenthesis token sequence - if (type === tokenTypes.Url) { - index += 2 - } + // format-css changes the Url token to a Function,String,RightParenthesis token sequence + if (type === tokenTypes.Url) { + index += 2 + } - let range_index = is_in_range(start, end) - if (range_index !== -1) { - ext_ranges[range_index]!.tokens.push(index) - } + let range_index = is_in_range(start, end) + if (range_index !== -1) { + ext_ranges[range_index]!.tokens.push(index) } }) - let new_tokens: { index: number; start: number; end: number }[] = [] + let new_tokens: Map = new Map() index = 0 tokenize(formatted, (type, start, end) => { - if (!irrelevant_tokens.has(type)) { - index++ + if (irrelevant_tokens.has(type)) return + index++ - // format-css changes the Url token to a Function,String,RightParenthesis token sequence - if (type === tokenTypes.Url) { - index += 2 - } - - new_tokens.push({ index, start, end }) + // format-css changes the Url token to a Function,String,RightParenthesis token sequence + if (type === tokenTypes.Url) { + index += 2 } + + new_tokens.set(index, { start, end }) }) let new_ranges: Range[] = [] for (let range of ext_ranges) { - let start_token = new_tokens.find((token) => token.index === range.tokens.at(0)) - let end_token = new_tokens.find((token) => token.index === range.tokens.at(-1)) + let start_token = new_tokens.get(range.tokens.at(0)!) + let end_token = new_tokens.get(range.tokens.at(-1)!) if (start_token !== undefined && end_token !== undefined) { new_ranges.push({ - start: start_token?.start, - end: end_token?.end + start: start_token.start, + end: end_token.end }) } } diff --git a/src/lib/css/style.css b/src/lib/css/style.css index 198c175..8368be8 100644 --- a/src/lib/css/style.css +++ b/src/lib/css/style.css @@ -342,7 +342,7 @@ /* Special cases */ --uneven-tr-bg: color-mix(in srgb, var(--gray-450) 10%, transparent); /* Mimics the 'Highlight' CSS system color keyword */ - --highlight: rgb(from light-dark(var(--accent-600), var(--accent)) r g b / 0.12); + --highlight: rgb(from light-dark(var(--accent-600), var(--accent)) r g b / 0.14); /* Setup theme */ color-scheme: dark light; diff --git a/src/routes/(public)/css-coverage/+page.svelte b/src/routes/(public)/css-coverage/+page.svelte index bcedb80..b1cc8db 100644 --- a/src/routes/(public)/css-coverage/+page.svelte +++ b/src/routes/(public)/css-coverage/+page.svelte @@ -4,37 +4,14 @@ import Label from '$components/Label.svelte' import Icon from '$components/Icon.svelte' import Seo from '$components/Seo.svelte' - import * as v from 'valibot' import Content from './content.md' import Markdown from '$components/Markdown.svelte' import Container from '$components/Container.svelte' import Heading from '$components/Heading.svelte' - - let CoverageSchema = v.array( - v.object({ - text: v.undefinedable(v.string()), - url: v.string(), - ranges: v.array( - v.object({ - start: v.number(), - end: v.number() - }) - ) - }) - ) + import { parse_json } from '$components/coverage/parse-coverage' let data: Coverage[] = $state([]) - function parse_json(input: string) { - try { - let parse_result = JSON.parse(input) - v.parse(CoverageSchema, parse_result) - return parse_result as Coverage[] - } catch (error) { - return [] as Coverage[] - } - } - async function onchange(event: Event) { let files = (event.target as HTMLInputElement)?.files let new_data: Coverage[] = [] diff --git a/tsconfig.json b/tsconfig.json index d1e7390..39f95c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "sourceMap": false, "strict": true, "moduleResolution": "node", + "allowImportingTsExtensions": true, // https://svelte.dev/docs/svelte/typescript#tsconfig.json-settings "target": "ES2022",