Add a summary report set from Markdown
from-markdown takes a hand-written summary such as SUMMARY.md and sets it in the full report's dress: the same cover, header, footers, table of contents, headings and grids. The shared macros move to macros.html.j2 so both templates draw from them, and layout.render_pdf takes the template and document title to use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ace2e1803
commit
864e2ff824
10 files changed
+573
-92
No files matched your search
@@ -0,0 +1,92 @@
|
||||
{#
|
||||
The pieces the report templates share: `report.html.j2`, the full report, and `summary.html.j2`,
|
||||
the summary set from Markdown. Kept apart so that the two documents draw a label, a value or a
|
||||
table the same way, and a change to one is a change to both.
|
||||
#}
|
||||
{# A value is drawn by what it is, not by where it sits: an identifier, a path or a CVSS vector is
|
||||
set in the monospace face wherever it appears, and a severity carries its colour. #}
|
||||
{% macro value(row) %}
|
||||
{% if row.token %}<span class="token">{{ row.token }}</span> — {% endif %}
|
||||
{% if row.kind == "severity" %}
|
||||
<span class="sev sev-{{ row.value|lower }}">{{ row.value }}</span>
|
||||
{% elif row.kind == "mono" %}
|
||||
<span class="mono">{{ row.value }}</span>
|
||||
{% else %}
|
||||
{{ row.value }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro facts(rows, wide=false) %}
|
||||
<table class="facts{% if wide %} wide{% endif %}">
|
||||
{% for row in rows %}
|
||||
<tr{% if row.gap %} class="gap"{% endif %}>
|
||||
<td>{{ row.label }}:</td>
|
||||
<td>{{ value(row) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro card(title, rows, note="") %}
|
||||
{# Wrapped, because break-inside on the table does not hold its caption: the title would stay on
|
||||
one page while the rows it titles moved to the next. #}
|
||||
<div class="block">
|
||||
<table class="card">
|
||||
<caption>{{ title }}</caption>
|
||||
{% for row in rows %}
|
||||
<tr{% if row.aside %} class="aside"{% endif %}>
|
||||
<th>{{ row.label }}</th>
|
||||
<td>{{ value(row) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if note %}<p class="note card-note">{{ note }}</p>{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Scaled against the busiest row, not against the total: what is read off a row of these is which
|
||||
one is the outlier, and a scale set by the sum flattens all of them. #}
|
||||
{% macro distribution(rows, legend) %}
|
||||
{# The bars and the legend that decodes them are one thing; a break between them leaves a reader
|
||||
with coloured segments and no key. #}
|
||||
<div class="block">
|
||||
<table class="bars">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td{% if row.mono %} class="mono"{% endif %}>{{ row.label }}</td>
|
||||
<td class="count">{{ row.count }}</td>
|
||||
<td class="track">
|
||||
<div class="bar">
|
||||
{% for part in row.parts %}
|
||||
<span class="seg-{{ part.severity|lower }}" style="width: {{ part.share }}%"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="legend">
|
||||
{% for item in legend.entries %}
|
||||
<span class="item"><span class="key seg-{{ item.severity|lower }}"></span>{{ item.label }}</span>
|
||||
{% endfor %}
|
||||
{% if legend.caveat %}<span class="caveat">— {{ legend.caveat }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro grid(table) %}
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr>{% for column in table.columns %}<th>{{ column }}</th>{% endfor %}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in table.rows %}
|
||||
<tr{% if row.total %} class="total"{% endif %}>
|
||||
{% for cell in row.cells %}
|
||||
<td>{% if cell is mapping %}{{ value(cell) }}{% else %}{{ cell }}{% endif %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
@@ -470,3 +470,70 @@ p.note {
|
||||
.toc a::after {
|
||||
content: leader(".") target-counter(attr(href), page);
|
||||
}
|
||||
|
||||
/* --- Prose (the summary report) ----------------------------------------------------------- */
|
||||
|
||||
/* The summary is written in Markdown, so it brings what the full report never has: running text,
|
||||
lists and links. Set to the full report's measure and rhythm, so the two read as one series. */
|
||||
.prose p,
|
||||
.prose ul,
|
||||
.prose ol,
|
||||
.prose blockquote {
|
||||
max-width: 165mm;
|
||||
}
|
||||
|
||||
/* Every section of the summary opens a page, so the space a heading keeps above it would only push
|
||||
the page down. */
|
||||
.prose h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose ul,
|
||||
.prose ol {
|
||||
margin: 0 0 3mm 0;
|
||||
padding-left: 5mm;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 1.4mm;
|
||||
}
|
||||
|
||||
.prose li > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--border);
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
margin: 0 0 3mm 0;
|
||||
padding-left: 3mm;
|
||||
border-left: 0.8pt solid var(--border);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
background: var(--label-fill);
|
||||
border: 0.4pt solid var(--border);
|
||||
padding: 2mm;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0 3mm 0;
|
||||
}
|
||||
|
||||
.prose hr {
|
||||
border: none;
|
||||
border-top: 0.4pt solid var(--border);
|
||||
margin: 5mm 0;
|
||||
}
|
||||
|
||||
/* The severity colour, where it marks a score in a table, is on the cell itself. */
|
||||
.grid td.sev {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cover-intro {
|
||||
margin-top: 9mm;
|
||||
}
|
||||
@@ -8,93 +8,7 @@
|
||||
The context this expects is described by `layout.CONTEXT`, and `sample.sample_context()` builds a
|
||||
complete one.
|
||||
#}
|
||||
{# A value is drawn by what it is, not by where it sits: an identifier, a path or a CVSS vector is
|
||||
set in the monospace face wherever it appears, and a severity carries its colour. #}
|
||||
{% macro value(row) %}
|
||||
{% if row.token %}<span class="token">{{ row.token }}</span> — {% endif %}
|
||||
{% if row.kind == "severity" %}
|
||||
<span class="sev sev-{{ row.value|lower }}">{{ row.value }}</span>
|
||||
{% elif row.kind == "mono" %}
|
||||
<span class="mono">{{ row.value }}</span>
|
||||
{% else %}
|
||||
{{ row.value }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro facts(rows, wide=false) %}
|
||||
<table class="facts{% if wide %} wide{% endif %}">
|
||||
{% for row in rows %}
|
||||
<tr{% if row.gap %} class="gap"{% endif %}>
|
||||
<td>{{ row.label }}:</td>
|
||||
<td>{{ value(row) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro card(title, rows, note="") %}
|
||||
{# Wrapped, because break-inside on the table does not hold its caption: the title would stay on
|
||||
one page while the rows it titles moved to the next. #}
|
||||
<div class="block">
|
||||
<table class="card">
|
||||
<caption>{{ title }}</caption>
|
||||
{% for row in rows %}
|
||||
<tr{% if row.aside %} class="aside"{% endif %}>
|
||||
<th>{{ row.label }}</th>
|
||||
<td>{{ value(row) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if note %}<p class="note card-note">{{ note }}</p>{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Scaled against the busiest row, not against the total: what is read off a row of these is which
|
||||
one is the outlier, and a scale set by the sum flattens all of them. #}
|
||||
{% macro distribution(rows, legend) %}
|
||||
{# The bars and the legend that decodes them are one thing; a break between them leaves a reader
|
||||
with coloured segments and no key. #}
|
||||
<div class="block">
|
||||
<table class="bars">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td{% if row.mono %} class="mono"{% endif %}>{{ row.label }}</td>
|
||||
<td class="count">{{ row.count }}</td>
|
||||
<td class="track">
|
||||
<div class="bar">
|
||||
{% for part in row.parts %}
|
||||
<span class="seg-{{ part.severity|lower }}" style="width: {{ part.share }}%"></span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="legend">
|
||||
{% for item in legend.entries %}
|
||||
<span class="item"><span class="key seg-{{ item.severity|lower }}"></span>{{ item.label }}</span>
|
||||
{% endfor %}
|
||||
{% if legend.caveat %}<span class="caveat">— {{ legend.caveat }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro grid(table) %}
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr>{% for column in table.columns %}<th>{{ column }}</th>{% endfor %}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in table.rows %}
|
||||
<tr{% if row.total %} class="total"{% endif %}>
|
||||
{% for cell in row.cells %}
|
||||
<td>{% if cell is mapping %}{{ value(cell) }}{% else %}{{ cell }}{% endif %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
{% from "macros.html.j2" import value, facts, card, distribution, grid %}
|
||||
|
||||
{# ------------------------------------------------------------------------------------------ #}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{#
|
||||
The summary report, set from a Markdown file in the dress of the full report: the same cover,
|
||||
running header and footers, table of contents and headings, with the body in between.
|
||||
|
||||
The body arrives already rendered -- :mod:`summary` turns the Markdown into HTML whose elements
|
||||
are the ones `report.css` already styles -- so this file only places it. The context this expects
|
||||
is described by `summary.CONTEXT`.
|
||||
#}
|
||||
{% from "macros.html.j2" import facts %}
|
||||
|
||||
<div class="page-header"><img src="{{ cover.logo }}" alt="CipherCell" /></div>
|
||||
<span class="footer-scan">{{ cover.footer }}</span>
|
||||
<span class="footer-date">{{ cover.footer_date }}</span>
|
||||
|
||||
<div class="cover-mark">
|
||||
<img src="{{ cover.logo }}" alt="CipherCell" />
|
||||
</div>
|
||||
|
||||
<div class="cover-title">{{ cover.title }}</div>
|
||||
|
||||
{{ facts(cover.identity) }}
|
||||
|
||||
{% if cover.intro %}
|
||||
<div class="prose cover-intro">{{ cover.intro }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if contents %}
|
||||
<h1 style="break-before: page">Table of Contents</h1>
|
||||
<ol class="toc">
|
||||
{% for entry in contents %}
|
||||
<li><a href="#{{ entry.id }}">{{ entry.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
<div class="prose">{{ body }}</div>
|
||||
Reference in new issue
Block a user