Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { validate_arguments, parse_arguments, InvalidArgumentsError } from './arguments.js'
import { program, MissingDataError } from './program.js'
import { validate_arguments, parse_arguments } from './arguments.js'
import { program } from './program.js'
import { read } from './file-reader.js'
import { print as pretty } from './reporters/pretty.js'
import { print as tap } from './reporters/tap.js'
Expand Down
30 changes: 10 additions & 20 deletions src/cli/reporters/pretty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,30 +64,20 @@ export function print({ report, context }: Report, params: CliArguments) {
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] === 1) continue

// Rewind cursor N lines to render N previous lines
for (let j = i - NUM_LEADING_LINES; j < i; j++) {
// Make sure that we don't try to start before line 0
if (j >= 0) {
console.log(styleText('dim', line_number(j)), styleText('dim', indent(lines[j])))
}
for (let chunk of sheet.chunks.filter((chunk) => !chunk.is_covered)) {
// Render N leading lines
for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES, 0); x < chunk.start_line; x++) {
console.log(styleText('dim', line_number(x)), styleText('dim', indent(lines[x - 1])))
}

// Render uncovered lines while increasing cursor until reaching next covered block
while (line_coverage[i] === 0) {
console.log(styleText('red', line_number(i, false)), indent(lines[i]))
i++
// Render the uncovered chunk
for (let i = chunk.start_line; i <= chunk.end_line; i++) {
console.log(styleText('red', line_number(i, false)), indent(lines[i - 1]))
}

// 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', indent(lines[i])))
// Render N trailing lines
for (let y = chunk.end_line; y < Math.min(chunk.end_line + NUM_TRAILING_LINES, lines.length); y++) {
console.log(styleText('dim', line_number(y)), styleText('dim', indent(lines[y - 1])))
}

// Show empty line between blocks
console.log()
}
Expand Down
105 changes: 105 additions & 0 deletions src/lib/chunkify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { test, expect } from '@playwright/test'
import { chunkify, type ChunkedCoverage } from './chunkify'

test('creates chunks with outer chunks covered', () => {
let coverage = {
text: 'a { color: red; } b { color: green; } c { color: blue; }',
ranges: [
{ start: 0, end: 17 },
{ start: 38, end: 56 },
],
url: 'https://example.com',
}
let result = chunkify(coverage)
delete coverage.ranges
expect(result).toEqual({
...coverage,
chunks: [
{
start_offset: 0,
end_offset: 17,
is_covered: true,
},
{
start_offset: 17,
end_offset: 38,
is_covered: false,
},
{
start_offset: 38,
end_offset: 56,
is_covered: true,
},
],
} satisfies ChunkedCoverage)
})

test('creates chunks with only middle chunk covered', () => {
let coverage = {
text: 'a { color: red; } b { color: green; } c { color: blue; }',
ranges: [{ start: 17, end: 38 }],
url: 'https://example.com',
}
let result = chunkify(coverage)
delete coverage.ranges
expect(result).toEqual({
...coverage,
chunks: [
{
start_offset: 0,
end_offset: 17,
is_covered: false,
},
{
start_offset: 17,
end_offset: 38,
is_covered: true,
},
{
start_offset: 38,
end_offset: 56,
is_covered: false,
},
],
} satisfies ChunkedCoverage)
})

test('creates a single chunk when all is covered', () => {
let coverage = {
text: 'a { color: red; } b { color: green; } c { color: blue; }',
ranges: [{ start: 0, end: 56 }],
url: 'https://example.com',
}
let result = chunkify(coverage)
delete coverage.ranges
expect(result).toEqual({
...coverage,
chunks: [
{
start_offset: 0,
end_offset: 56,
is_covered: true,
},
],
} satisfies ChunkedCoverage)
})

test('creates a single chunk when none is covered', () => {
let coverage = {
text: 'a { color: red; } b { color: green; } c { color: blue; }',
ranges: [],
url: 'https://example.com',
}
let result = chunkify(coverage)
delete coverage.ranges
expect(result).toEqual({
...coverage,
chunks: [
{
start_offset: 0,
end_offset: 56,
is_covered: false,
},
],
} satisfies ChunkedCoverage)
})
91 changes: 91 additions & 0 deletions src/lib/chunkify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Coverage } from './parse-coverage'

type Chunk = {
start_offset: number
end_offset: number
is_covered: boolean
}

export type ChunkedCoverage = Omit<Coverage, 'ranges'> & {
chunks: Chunk[]
}

function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
let new_chunks: Chunk[] = []
let previous_chunk: Chunk | undefined

for (let i = 0; i < stylesheet.chunks.length; i++) {
let chunk = stylesheet.chunks.at(i)!

// If the current chunk is only whitespace or empty, ignore it
if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) {
continue
}

let latest_chunk = new_chunks.at(-1)

// merge current and previous if they are both covered or uncovered
if (i > 0 && previous_chunk && latest_chunk) {
if (previous_chunk.is_covered === chunk.is_covered) {
latest_chunk.end_offset = chunk.end_offset
previous_chunk = chunk
continue
}
// If the current chunk is only whitespace or empty, add it to the previous
else if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
latest_chunk.end_offset = chunk.end_offset
// do not update previous_chunk
continue
}
}

previous_chunk = chunk
new_chunks.push(chunk)
}

return {
...stylesheet,
chunks: new_chunks,
}
}

export function chunkify(stylesheet: Coverage): ChunkedCoverage {
let chunks = []
let offset = 0

for (let range of stylesheet.ranges) {
// Create non-covered chunk
if (offset !== range.start) {
chunks.push({
start_offset: offset,
end_offset: range.start,
is_covered: false,
})
offset = range.start
}

chunks.push({
start_offset: range.start,
end_offset: range.end,
is_covered: true,
})
offset = range.end
}

// fill up last chunk if necessary:
if (offset !== stylesheet.text.length - 1) {
chunks.push({
start_offset: offset,
end_offset: stylesheet.text.length,
is_covered: false,
})
}

let merged = merge({
url: stylesheet.url,
text: stylesheet.text,
chunks,
})

return merged
}
61 changes: 0 additions & 61 deletions src/lib/css-tree.d.ts

This file was deleted.

66 changes: 65 additions & 1 deletion src/lib/decuplicate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,61 @@
import type { Coverage, Range } from './parse-coverage.js'

// Combine multiple adjecent ranges into a single one
export function concatenate(ranges: Set<Range> | Range[]): Range[] {
let result: Range[] = []

for (let range of ranges) {
// Update the last range if this range starts at last-range-end + 1
if (result.length > 0 && (result.at(-1)!.end === range.start - 1 || result.at(-1)!.end === range.start)) {
result.at(-1)!.end = range.end
} else {
result.push(range)
}
}

return result
}

function dedupe_list(ranges: Range[]): Set<Range> {
let new_ranges: Set<Range> = new Set()

outer: for (let range of ranges) {
for (let processed_range of new_ranges) {
// Case: an existing range fits within this range -> replace it
// { start: 0, end: 100 },
// { start: 0, end: 200 }
if (range.start <= processed_range.start && range.end >= processed_range.end) {
new_ranges.delete(processed_range)
new_ranges.add(range)
continue outer
}

// Case: this range fits within an existing range -> skip it
// { start: 324, end: 485 }, --> exists
// { start: 364, end: 485 }, --> skip
// { start: 404, end: 485 }, --> skip
if (range.start >= processed_range.start && range.end <= processed_range.end) {
// console.log('skip', range)
continue outer
}
// Case: ranges partially overlap
// { start: 324, end: 444 },
// { start: 364, end: 485 },
if (range.start < processed_range.end && range.start > processed_range.start && range.end > processed_range.end) {
new_ranges.delete(processed_range)
new_ranges.add({
start: processed_range.start,
end: range.end,
})
continue outer
}
}
new_ranges.add(range)
}

return new_ranges
}

/**
* @description
* prerequisites
Expand All @@ -18,12 +75,15 @@ export function deduplicate_entries(entries: Coverage[]): Coverage[] {
// If not, add them
for (let range of entry.ranges) {
let found = false

for (let checked_range of ranges) {
// find exact range
if (checked_range.start === range.start && checked_range.end === range.end) {
found = true
break
}
}

if (!found) {
ranges.push(range)
}
Expand All @@ -36,5 +96,9 @@ export function deduplicate_entries(entries: Coverage[]): Coverage[] {
}
}

return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({ text, url, ranges }))
return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({
text,
url,
ranges: concatenate(dedupe_list(ranges.sort((a, b) => a.start - b.start))).sort((a, b) => a.start - b.start),
}))
}
Loading