1
# The CSS and html structure within this file was mostly generated by Claude and further refined.
4
modern_css_styles() -> String
6
Return modern CSS styling for the coverage report.
7
Fully static, no runtime JavaScript required.
9
function modern_css_styles()
10
return read(joinpath(@__DIR__, "style.css"), String)
14
highlight_with_show(line::Union{AnnotatedString, SubString{<:AnnotatedString}}) -> String
16
Take as input an annotated string containing synthax highlighting and generate an HTML representation of it using the `show` method with `MIME"text/html"()`, relying on `StyledStrings.jl` implementation to create HTML with synthax highlighting.
18
function highlight_with_show end
20
function calculate_file_stats(file::FileCoverageSummary)
21
total_lines = file.lines_tracked
22
covered_lines = file.lines_hit
23
missed_lines = total_lines - covered_lines
24
coverage_pct = total_lines == 0 ? 100.0 : (covered_lines / total_lines) * 100.0
28
covered = covered_lines,
29
missed = missed_lines,
30
coverage = coverage_pct,
31
missing_str = format_gaps(file),
36
coverage_badge_class(coverage::Float64) -> String
38
Return CSS class for coverage badge based on coverage percentage.
40
function coverage_badge_class(coverage::Float64)
41
return coverage >= 80 ? "coverage-excellent" :
42
coverage >= 60 ? "coverage-good" :
46
@component function file_summary_table_component(; data::PackageCoverage)
47
@div {class = "summary-table-section"} begin
48
@h2 "📋 Files Overview"
49
@table {class = "file-summary-table"} begin
53
@th {class = "stats-cell"} "Total Lines"
54
@th {class = "stats-cell"} "Covered"
55
@th {class = "stats-cell"} "Missed"
56
@th {class = "stats-cell"} "Coverage"
61
for (idx, file) in enumerate(data.files)
63
stats = calculate_file_stats(file)
64
badge_class = coverage_badge_class(stats.coverage)
65
file_anchor = "file-$idx"
69
@a {class = "file-link", href = "#$file_anchor"} $(fname)
71
@td {class = "stats-cell"} $(stats.total)
72
@td {class = "stats-cell"} $(stats.covered)
73
@td {class = "stats-cell"} $(stats.missed)
74
@td {class = "stats-cell"} begin
75
@span {class = "coverage-badge $badge_class"} "$(round(stats.coverage, digits=2))%"
77
@td {class = "missing-cell"} $(
78
isempty(stats.missing_str) ? "—" : stats.missing_str
86
@deftag macro file_summary_table_component end
88
# Component that renders the summary metrics section
89
@component function summary_section_component(; data::PackageCoverage)
90
lines_covered = data.lines_hit
91
lines_valid = data.lines_tracked
92
coverage_pct = (lines_covered / lines_valid) * 100
94
@div {class = "summary"} begin
95
@div {class = "metric"} begin
96
@div {class = "metric-label"} "Total Coverage"
97
@div {class = "metric-value"} "$(round(coverage_pct, digits=2))%"
98
@div {class = "metric-subtext"} "$(lines_covered) of $(lines_valid) lines"
100
@div {class = "metric"} begin
101
@div {class = "metric-label"} "Lines Covered"
102
@div {class = "metric-value"} "$(lines_covered)"
103
@div {class = "metric-subtext"} ""
105
@div {class = "metric"} begin
106
@div {class = "metric-label"} "Lines Uncovered"
107
@div {class = "metric-value"} "$(lines_valid - lines_covered)"
108
@div {class = "metric-subtext"} ""
110
@div {class = "metric"} begin
111
@div {class = "metric-label"} "Tracked Files"
112
@div {class = "metric-value"} "$(length(data.files))"
113
@div {class = "metric-subtext"} ""
117
@deftag macro summary_section_component end
119
# Component that renders a single line of source code with coverage highlighting
120
@component function code_line_component(;
122
content::AbstractString,
123
hits::Union{Int,Nothing},
126
line_class = if isnothing(hits)
127
"code-line line-neutral"
129
"code-line line-covered"
131
"code-line line-uncovered"
134
# Apply syntax highlighting
135
highlighted_content = html_function(content)
137
@div {class = line_class} begin
138
@div {class = "line-number"} $line_num
139
@div {class = "line-content"} @text highlighted_content
142
@deftag macro code_line_component end
144
# Component that renders a single file card with coverage information and source code
145
@component function file_card_component(;
146
file::FileCoverageSummary,
149
lines_hits::Vector{Union{Int,Nothing}},
153
stats = calculate_file_stats(file)
154
source_lines = extract_file_lines(lines_function, file.filename, pkg_dir)
156
badge_class = coverage_badge_class(stats.coverage)
157
file_anchor = "file-$file_index"
159
@div {class = "file-card", id = file_anchor} begin
160
@div {class = "file-header"} begin
161
@div {class = "file-name"} $(file.filename)
162
@div {class = "file-stats"} begin
163
@span {class = "stat"} begin
164
@span {class = "coverage-badge $badge_class"} "$(round(stats.coverage, digits=2))%"
166
@span {class = "stat"} begin
167
@span "📊 $(stats.covered)/$(stats.total) lines"
169
@span {class = "stat"} begin
170
@span "❌ $(stats.missed) missed"
175
# Source code section
176
@div {class = "source-code"} begin
177
for (i, line) in enumerate(source_lines)
178
@code_line_component {
181
hits = i > length(lines_hits) ? nothing : lines_hits[i],
187
# Missing lines section
188
if !isempty(stats.missing_str)
189
@div {class = "missing-lines"} begin
190
@strong "Uncovered Lines: "
191
@text stats.missing_str
196
@deftag macro file_card_component end
199
generate_html_report(lcov_file, html_file; title = nothing, pkg_dir = nothing)
201
Generate a static HTML report from a LCOV file using a native Julia solution.
203
The `lcov_file` and `html_file` arguments are the full paths to the LCOV file used as input and of the HTML file to be generated, respectively.
207
- `title = "Package Coverage Report"` is the title used at the top of the HTML report.
208
- `pkg_dir` is the directory of the package being covered. It is used to generate the source code links in the HTML report.
209
- `lines_function` is a function that takes an `IO` stream representing the contents of a file and returns a vector of `AbstractString` objects representing each line of the source file. When not provided, it defaults to [`ExtendedLocalCoverage.default_lines_function`](@ref).
210
- `html_function` is a function that takes a single line of source code as an `AbstractString` and returns an HTML-safe representation of that line, potentially with syntax highlighting. When not provided, it defaults to [`ExtendedLocalCoverage.default_html_function`](@ref).
212
function generate_html_report(
215
title::String = "Coverage Report",
217
lines_function = nothing,
218
html_function = nothing,
220
# Eventually set defaults for lines and html functions depending on loaded extensions
221
lines_function = @something lines_function default_lines_function()
222
html_function = @something html_function default_html_function(lines_function)
223
# Parse the LCOV file
224
raw_coverage = LCOV.readfile(lcov_file)
225
# Make the package directory absolute
226
pkg_dir = abspath(pkg_dir)
227
# We make the filepaths absolute
228
foreach(raw_coverage) do fc
229
fc.filename = abspath(pkg_dir, fc.filename)
231
data = eval_coverage_metrics(raw_coverage, pkg_dir)
234
open(output_file, "w") do io
235
# We disable debug mode which is automatically enabled in HypertextTemplates.jl when Revise is loaded. This is a hack as mentioned in https://github.com/MichaelHatherly/HypertextTemplates.jl/issues/36#issuecomment-3004032438
236
ctx = IOContext(io, HypertextTemplates._include_data_htloc() => false)
238
@html {lang = "en"} begin
240
@meta {charset = "UTF-8"}
243
content = "width=device-width, initial-scale=1.0",
246
@style @text modern_css_styles()
249
@div {class = "container"} begin
256
@summary_section_component {data = data}
259
@file_summary_table_component {data = data}
261
# File coverage details
262
@div {class = "files-section"} begin
263
@h2 "📁 File Coverage Details"
264
for (idx, file) in enumerate(data.files)
265
@file_card_component {
269
lines_hits = raw_coverage[idx].coverage,
278
"Generated by ExtendedLocalCoverage.jl using HypertextTemplates.jl"
288
highlighted_lines(io::IO)
290
Extract lines from the given `IO` stream and return them with syntax highlighting using `JuliaSyntaxHighlighting.jl`.
292
These lines usually processed with [`ExtendedLocalCoverage.highlight_with_show`](@ref) to get synthax highlighting of julia code in the static HTML report.
294
function highlighted_lines end
299
Extract lines from the given `IO` stream and return them as plain text without any syntax highlighting.
301
This function simply does `collect(eachline(io))` and is the default fallback used as `lines_function` when `JuliaSyntaxHighlighting.jl` is not loaded.
303
function plain_lines(io::IO)
304
collect(eachline(io))
306
function extract_file_lines(lines_function, filepath::String, pkg_dir::String)
307
fullpath = joinpath(pkg_dir, filepath)
308
isfile(fullpath) || throw(ArgumentError("Source file not found: $fullpath"))
309
return open(fullpath, "r") do io
315
default_html_function(lines_function)
317
Extract the default function to be passed as `html_function` argument to `generate_html_report`.
319
It takes the `lines_function` as an argument and returns `highlight_with_show` if the `lines_function` is `ExtendedLocalCoverage.highlighted_lines` and simply return the `String` constructor otherwise.
321
function default_html_function(lines_function)
322
return StyledStringsLoaded[] && lines_function === highlighted_lines ? highlight_with_show : String
326
default_lines_function()
327
Extract the default function to be passed as `lines_function` argument to `generate_html_report`.
329
If the `JuliaSyntaxHighlighting` package is loaded, this returns the `ExtendedLocalCoverage.highlighted_lines` function, otherwise it returns the `ExtendedLocalCoverage.plain_lines` function.
331
function default_lines_function()
332
return JuliaSyntaxHighlightingLoaded[] ? highlighted_lines : plain_lines