use super::*;
pub(in crate::output::pdf) fn render_security(
mut builder: renderreport::engine::ReportBuilder,
sec: &SecurityPresentation,
is_first: bool,
i18n: &I18n,
) -> renderreport::engine::ReportBuilder {
let security_title = i18n.t("section-security");
let sec_takeaway = super::first_sentence(&sec.interpretation);
builder = super::module_chapter_opener(builder, &security_title, &sec_takeaway, is_first);
builder = builder
.add_component(
ScoreCard::new(super::module_score_caption(i18n), sec.score)
.with_description(sec.band_label.as_str())
.with_thresholds(75, 40),
)
.add_component(module_customer_context(
i18n,
"security",
Some(sec.score),
&sec.interpretation,
));
if let Some(rest) = super::interpretation_beyond_takeaway(&sec.interpretation, &sec_takeaway) {
builder = builder.add_component(
Label::new(rest)
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL),
);
}
let header_count = sec
.headers
.iter()
.filter(|(_, status, _, _)| status.to_lowercase().contains("vorhanden") || status == "✓")
.count();
builder = builder.add_component(
MetricStrip::new(vec![
MetricStripItem::new("Header", format!("{}/{}", header_count, sec.headers.len()))
.with_accent(crate::output::pdf::design::score_color(
if sec.headers.is_empty() {
0
} else {
(header_count * 100 / sec.headers.len()) as u8
},
)),
MetricStripItem::new(
"HTTPS",
if sec
.ssl_info
.iter()
.any(|(k, v)| k.contains("HTTPS") && v == "Ja")
{
i18n.t("pdf-sec-https-yes")
} else {
i18n.t("pdf-sec-https-unclear")
},
)
.with_accent("#2563eb"),
MetricStripItem::new("Issues", sec.issues.len().to_string()).with_accent("#dc2626"),
])
.compact(),
);
if !sec.headers.is_empty() {
let mut table = AuditTable::new(vec![
TableColumn::new("Header"),
TableColumn::new("Status"),
TableColumn::new(i18n.t("pdf-sec-header-tier-column")),
TableColumn::new(i18n.t("pdf-seo-value")),
])
.with_title("Security Headers");
for (name, status, tier, val) in &sec.headers {
table = table.add_row(vec![
name.as_str(),
status.as_str(),
tier.as_str(),
val.as_str(),
]);
}
builder = builder.add_component(table);
builder = builder.add_component(
Label::new(i18n.t("pdf-sec-header-tier-note"))
.with_size("8.8pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL),
);
}
if !sec.ssl_info.is_empty() {
let mut kv = KeyValueList::new().with_title("SSL/TLS");
for (k, v) in &sec.ssl_info {
kv = kv.add(k, v);
}
builder = builder.add_component(kv);
}
if !sec.protection.is_empty() {
let title = i18n.t("pdf-sec-protection-title");
let mut kv = KeyValueList::new().with_title(&title);
for (name, kind) in &sec.protection {
kv = kv.add(name, kind);
}
builder = builder.add_component(kv);
}
if sec.score >= 80 && sec.issues.is_empty() {
builder = builder.add_component(clean_section_note(i18n));
}
for (title, sev, msg) in &sec.issues {
builder = builder.add_component(Finding::new(title, map_severity(sev), msg));
}
if !sec.recommendations.is_empty() {
let mut rec_list = List::new().with_title(i18n.t("label-improvement-suggestions"));
for rec in &sec.recommendations {
rec_list = rec_list.add_item(rec);
}
builder = builder.add_component(rec_list);
}
builder
}
pub(in crate::output::pdf) fn render_mobile(
mut builder: renderreport::engine::ReportBuilder,
mobile: &MobilePresentation,
is_first: bool,
i18n: &I18n,
) -> renderreport::engine::ReportBuilder {
let mobile_title = i18n.t("section-mobile-usability");
let mobile_takeaway = super::first_sentence(&mobile.interpretation);
builder = super::module_chapter_opener(builder, &mobile_title, &mobile_takeaway, is_first);
builder = builder
.add_component(
ScoreCard::new(super::module_score_caption(i18n), mobile.score)
.with_description(super::score_band_label(mobile.score, i18n))
.with_thresholds(75, 40),
)
.add_component(module_customer_context(
i18n,
"mobile",
Some(mobile.score),
&mobile.interpretation,
));
if let Some(rest) =
super::interpretation_beyond_takeaway(&mobile.interpretation, &mobile_takeaway)
{
builder = builder.add_component(
Label::new(rest)
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL),
);
}
let configured_label = i18n.t("mobile-configured");
let viewport_status = mobile
.viewport
.iter()
.find(|(k, _)| k.to_lowercase().contains("viewport"))
.map(|(_, v)| v.as_str())
.unwrap_or(&configured_label);
let touch_targets = mobile
.touch_targets
.iter()
.find(|(k, _)| k.to_lowercase().contains("zu klein") || k.to_lowercase().contains("small"))
.map(|(_, v)| v.as_str())
.unwrap_or("n/a");
builder = builder.add_component(
MetricStrip::new(vec![
MetricStripItem::new("Viewport", viewport_status).with_accent("#0f766e"),
MetricStripItem::new(i18n.t("mobile-touch-targets"), touch_targets)
.with_accent("#d97706"),
MetricStripItem::new("Issues", mobile.issues.len().to_string()).with_accent("#dc2626"),
])
.compact(),
);
if !mobile.viewport.is_empty() {
let mut kv = KeyValueList::new().with_title(i18n.t("mobile-viewport-config"));
for (k, v) in &mobile.viewport {
kv = kv.add(k, v);
}
builder = builder.add_component(kv);
}
if !mobile.touch_targets.is_empty() {
let mut kv = KeyValueList::new().with_title(i18n.t("mobile-touch-targets"));
for (k, v) in &mobile.touch_targets {
kv = kv.add(k, v);
}
builder = builder.add_component(kv);
}
if !mobile.font_analysis.is_empty() {
let mut kv = KeyValueList::new().with_title(i18n.t("mobile-font-analysis"));
for (k, v) in &mobile.font_analysis {
kv = kv.add(k, v);
}
builder = builder.add_component(kv);
}
if !mobile.content_sizing.is_empty() {
let mut kv = KeyValueList::new().with_title(i18n.t("mobile-content-sizing"));
for (k, v) in &mobile.content_sizing {
kv = kv.add(k, v);
}
builder = builder.add_component(kv);
}
for (cat, sev, msg) in &mobile.issues {
let label = mobile_category_label(cat, i18n);
builder = builder.add_component(Finding::new(&label, map_severity(sev), msg));
}
builder
}
pub(in crate::output::pdf) fn mobile_category_label(category: &str, i18n: &I18n) -> String {
let key = format!("mobile-cat-{category}");
let translated = i18n.t(&key);
if translated != key {
return translated;
}
category
.split(['_', '-'])
.filter(|w| !w.is_empty())
.map(|w| {
let mut chars = w.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}