use renderreport::components::advanced::{
ChecklistPanel, ChecklistRow, Grid, KeyValueList, List, MetricStrip, MetricStripItem, PageBreak,
};
use renderreport::components::charts::{Gauge, GaugeThreshold};
use renderreport::components::text::{Label, TextBlock};
use renderreport::components::{AuditTable, Finding, ScoreCard, TableColumn};
use renderreport::prelude::*;
use crate::i18n::I18n;
use crate::output::localized::{is_english, pick};
use crate::output::report_model::*;
use super::helpers::{
manual_recheck_instruction, map_severity, score_quality_color, score_quality_label,
truncate_with_ellipsis,
};
mod accessibility;
mod ai_transparency;
mod commerce;
mod dark_mode;
mod design_quality;
mod experience;
mod html_conform;
mod indicators;
mod network_dns;
mod overview;
mod performance;
mod platform;
mod seo;
pub(super) use accessibility::{render_a11y_journey_findings, render_screen_reader_section};
pub(super) use ai_transparency::render_ai_transparency;
pub(super) use commerce::render_commerce;
pub(super) use dark_mode::render_dark_mode;
pub(super) use design_quality::render_design_quality;
pub(super) use experience::{render_journey, render_ux};
pub(super) use html_conform::render_html_conform;
pub(super) use indicators::{
render_ai_visibility, render_best_practices, render_content_visibility, render_source_quality,
render_tech_stack,
};
pub(super) use network_dns::render_network_dns;
pub(super) use overview::{render_budget_violations, render_search_experience};
pub(super) use performance::render_performance;
pub(super) use platform::{mobile_category_label, render_mobile, render_security};
pub(super) use seo::render_seo;
fn clean_section_note(i18n: &I18n) -> Label {
Label::new(i18n.t("pdf-section-clean"))
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL)
}
fn module_customer_context(
i18n: &I18n,
module_key: &str,
score: Option<u32>,
_interpretation: &str,
) -> Label {
let en = is_english(i18n);
let scored = match module_key {
"content_visibility" | "html_conform" => None,
_ => score,
};
let weakness = match scored {
Some(s) if s < 50 => Some(pick(
i18n,
"Der Score zeigt in diesem Bereich eine deutliche Schwäche.",
"The score indicates a clear weakness in this area.",
)),
Some(s) if s < 75 => Some(pick(
i18n,
"Der Score zeigt in diesem Bereich erkennbares Verbesserungspotenzial.",
"The score indicates visible improvement potential in this area.",
)),
_ => None,
};
let module_text = match (module_key, en) {
("performance", true) => "Visitors may experience delays, unstable rendering or unnecessary data transfer before the page feels usable.",
("performance", false) => "Besucher können Verzögerungen, instabiles Rendering oder unnötige Datenmenge erleben, bevor die Seite nutzbar wirkt.",
("seo", true) => "Search engines and AI systems need clear titles, headings, structured data and enough readable content to understand the page.",
("seo", false) => "Suchmaschinen und KI-Systeme benötigen klare Titel, Überschriften, strukturierte Daten und ausreichend lesbaren Inhalt, um die Seite zu verstehen.",
("search_experience", true) => "This score combines technical findability with whether users, search engines and AI systems can actually understand and trust the content.",
("search_experience", false) => "Dieser Wert verbindet technische Auffindbarkeit mit der Frage, ob Nutzer, Suchmaschinen und KI-Systeme die Inhalte tatsächlich verstehen und ihnen vertrauen können.",
("security", true) => "Security headers and HTTPS signals influence visible trust and reduce avoidable browser-side risk in the checked scope.",
("security", false) => "Security Header und HTTPS-Signale beeinflussen sichtbares Vertrauen und reduzieren vermeidbare Browser-Risiken im geprüften Umfang.",
("html_conform", true) => "Clean, spec-conformant HTML helps browsers, assistive technologies and search engines parse and render the page reliably.",
("html_conform", false) => "Sauberes, spezifikationskonformes HTML hilft Browsern, assistiven Technologien und Suchmaschinen, die Seite zuverlässig zu verarbeiten und darzustellen.",
("mobile", true) => "Mobile visitors depend on readable text, fitting content and controls that are easy to tap on small screens.",
("mobile", false) => "Mobile Besucher sind auf lesbaren Text, passende Inhaltsbreiten und gut antippbare Bedienelemente angewiesen.",
("ux", true) => "This indicator estimates whether the page feels understandable, consistent and low-friction for common visitor tasks.",
("ux", false) => "Dieser Indikator schätzt, ob die Seite für typische Besucheraufgaben verständlich, konsistent und reibungsarm wirkt.",
("journey", true) => "This indicator estimates whether visitors can move through the page intent without avoidable friction.",
("journey", false) => "Dieser Indikator schätzt, ob Besucher ohne vermeidbare Reibung durch den Zweck der Seite kommen.",
("source_quality", true) => "Source quality signals show whether content appears substantial, consistent and trustworthy enough to support decisions.",
("source_quality", false) => "Quellenqualität zeigt, ob Inhalte substanziell, konsistent und vertrauenswürdig genug wirken, um Entscheidungen zu stützen.",
("ai_visibility", true) => "AI visibility indicates whether content can be parsed, chunked, attributed and cited by AI-assisted systems.",
("ai_visibility", false) => "KI-Sichtbarkeit zeigt, ob Inhalte von KI-gestützten Systemen gelesen, gegliedert, zugeordnet und zitiert werden können.",
("content_visibility", true) => "Content visibility combines discoverability, trust and topical depth signals; it is an indicator, not a guarantee of reach.",
("content_visibility", false) => "Content Visibility verbindet Auffindbarkeit, Vertrauen und inhaltliche Tiefe; es ist ein Indikator, keine Reichweiten-Garantie.",
_ if en => "This module describes customer-facing quality beyond pure accessibility findings.",
_ => "Dieses Modul beschreibt kundennahe Qualität über reine Accessibility-Befunde hinaus.",
};
let mut parts = Vec::new();
if let Some(w) = weakness {
parts.push(w);
}
parts.push(module_text);
let text = parts.join(" ");
Label::new(text)
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL)
}
pub(super) fn derived_from_note(i18n: &I18n, module_name: &str) -> Option<Label> {
let sources = crate::taxonomy::module_derived_from(module_name);
if sources.is_empty() {
return None;
}
let en = is_english(i18n);
let names: Vec<String> = sources
.iter()
.map(|key| {
let translated = i18n.t(&format!("module-{key}"));
if translated == format!("module-{key}") {
key.to_string()
} else {
translated
}
})
.collect();
let list = names.join(", ");
let text = if en {
format!(
"This indicator re-reads results already reported under {list}. It measures nothing of its own, so its agreement with those modules is not independent confirmation."
)
} else {
format!(
"Dieser Indikator liest Ergebnisse neu, die bereits unter {list} berichtet werden. Er misst nichts Eigenes — seine Übereinstimmung mit diesen Modulen ist keine unabhängige Bestätigung."
)
};
Some(
Label::new(text)
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL),
)
}
pub(super) fn a11y_penalty_note(
builder: renderreport::engine::ReportBuilder,
penalty: u32,
score_before: u32,
i18n: &I18n,
) -> renderreport::engine::ReportBuilder {
if penalty == 0 {
return builder;
}
let text = if is_english(i18n) {
format!(
"Includes an accessibility penalty of {penalty} points: {score_before} before it was applied."
)
} else {
format!(
"Enthält einen Abzug von {penalty} Punkten für die Barrierefreiheits-Befunde: {score_before} vor dem Abzug."
)
};
builder.add_component(
Label::new(text)
.with_size("10.5pt")
.with_color(crate::output::pdf::design::tokens::NEUTRAL),
)
}
pub(super) fn interpretation_beyond_takeaway<'a>(
interpretation: &'a str,
takeaway: &str,
) -> Option<&'a str> {
let rest = interpretation
.trim()
.strip_prefix(takeaway.trim())
.unwrap_or(interpretation)
.trim();
(!rest.is_empty()).then_some(rest)
}
fn score_gauge_thresholds() -> Vec<GaugeThreshold> {
vec![
GaugeThreshold {
value: 0.0,
color: crate::output::pdf::design::tokens::DANGER.to_string(),
},
GaugeThreshold {
value: 40.0,
color: crate::output::pdf::design::tokens::WARN_DEEP.to_string(),
},
GaugeThreshold {
value: 75.0,
color: crate::output::pdf::design::tokens::SUCCESS.to_string(),
},
]
}
pub(super) fn module_chapter_opener(
mut builder: renderreport::engine::ReportBuilder,
title: &str,
takeaway: &str,
is_first: bool,
) -> renderreport::engine::ReportBuilder {
use renderreport::components::advanced::{PageBreak, SectionHeaderSplit};
if !is_first {
builder = builder.add_component(PageBreak::new());
}
builder.add_component(SectionHeaderSplit::new(title, takeaway).with_level(2))
}
pub(super) fn first_sentence(text: &str) -> String {
let t = text.trim();
match t.find(". ") {
Some(idx) => t[..=idx].trim().to_string(),
None => t.to_string(),
}
}
pub(super) fn module_score_caption(i18n: &I18n) -> &'static str {
if is_english(i18n) {
"Overall score · 0–100"
} else {
"Gesamtwertung · 0–100"
}
}
pub(super) fn score_band_label(score: u32, i18n: &I18n) -> &'static str {
crate::registry::FIVE_BAND.label(score as f32, is_english(i18n))
}
fn vital_status(rating: &str) -> &'static str {
match rating {
"good" => "good",
"needs-improvement" => "warn",
"poor" => "bad",
_ => "info",
}
}
fn vital_rating_label(rating: &str, en: bool) -> &'static str {
match (rating, en) {
("good", true) => "Good",
("needs-improvement", true) => "Needs improvement",
("poor", true) => "Poor",
("good", false) => "Gut",
("needs-improvement", false) => "Verbesserungswürdig",
("poor", false) => "Schlecht",
_ => "—",
}
}
fn vital_color(rating: &str) -> &'static str {
crate::output::pdf::design::status_color(vital_status(rating))
}
fn journey_category_label(category: &str, i18n: &I18n) -> String {
let en = is_english(i18n);
let label = match category {
"TabOrder" => Some(if en { "Tab order" } else { "Tab-Reihenfolge" }),
"FocusTrap" => Some(if en { "Focus trap" } else { "Fokus-Falle" }),
"StateTransition" => Some(if en {
"State transition"
} else {
"Zustandswechsel"
}),
"FocusRestoration" => Some(if en {
"Focus restoration"
} else {
"Fokus-Wiederherstellung"
}),
"FormError" => Some(if en {
"Form error announcement"
} else {
"Formularfehler-Ansage"
}),
"SpaNavigation" => Some(if en {
"SPA navigation"
} else {
"SPA-Navigation"
}),
"HiddenFocusable" => Some(if en {
"Hidden focusable element"
} else {
"Verstecktes fokussierbares Element"
}),
"SkipLink" => Some(if en { "Skip link" } else { "Skip-Link" }),
"FocusIndicator" => Some(if en {
"Focus indicator"
} else {
"Fokus-Indikator"
}),
"MenuJourney" => Some(if en {
"Menu navigation"
} else {
"Menü-Navigation"
}),
"TabsJourney" => Some(if en {
"Tab navigation"
} else {
"Tab-Navigation"
}),
_ => None,
};
if let Some(label) = label {
return label.to_string();
}
let mut out = String::new();
for (i, ch) in category.char_indices() {
if ch == '_' || ch == '-' {
out.push(' ');
} else if ch.is_uppercase() && i > 0 && !out.ends_with(' ') {
out.push(' ');
out.push(ch);
} else {
out.push(ch);
}
}
out
}
#[cfg(test)]
mod tests;