///|
/// Structural accessibility and safety checks for generated SVG.
pub(all) struct SvgAudit {
has_title : Bool
has_description : Bool
has_aria_labelledby : Bool
has_role_img : Bool
has_unsafe_script : Bool
issue_count : Int
} derive(Eq, Debug)
///|
/// A report bundle with visual and non-visual representations.
pub(all) struct ReportArtifact {
svg : String
markdown : String
alt_text : String
summary : ChartSummary
audit : SvgAudit
} derive(Eq, Debug)
///|
pub fn SvgAudit::passed(self : SvgAudit) -> Bool {
self.issue_count == 0
}
///|
/// Audits the accessibility contract used by MoonChartKit report artifacts.
pub fn audit_svg(svg : String) -> SvgAudit {
let has_title = svg.contains(" ReportArtifact {
let summary = ChartSummary::from_points(points)
let svg = render_accessible_svg(
title,
description,
points,
trend,
summary,
size,
theme,
)
let markdown = render_markdown_report(title, description, points, summary)
let alt_text = "\{title}. \{description}. \{summary.count} categories, total \{summary.total}, average \{summary.average}, maximum \{summary.max}."
{ svg, markdown, alt_text, summary, audit: audit_svg(svg) }
}
///|
fn render_accessible_svg(
title : String,
description : String,
points : Array[DataPoint],
trend : Array[Int],
summary : ChartSummary,
size : ChartSize,
theme : ChartTheme,
) -> String {
let area = size.plot_area()
let chart_top = area.y + 146
let chart_height = normalize_positive(area.height - 166, 1)
let left_width = area.width / 2
let trend_left = area.x + left_width + 48
let trend_width = normalize_positive(area.width - left_width - 48, 1)
let max = normalize_positive(summary.max, 1)
let buf = StringBuilder()
buf.write_string(
"")
buf.to_string()
}
///|
fn render_markdown_report(
title : String,
description : String,
points : Array[DataPoint],
summary : ChartSummary,
) -> String {
let buf = StringBuilder()
buf.write_string("## \{escape_markdown(title)}\n\n")
buf.write_string("\{escape_markdown(description)}\n\n")
buf.write_string(
"Total: **\{summary.total}** | Average: **\{summary.average}** | Max: **\{summary.max}**\n\n",
)
buf.write_string("| Label | Value | Share |\n| --- | ---: | ---: |\n")
for point in points {
buf.write_string(
"| \{escape_markdown(point.label)} | \{point.value} | \{percent_of(point.value, summary.total)}% |\n",
)
}
buf.to_string()
}
///|
fn escape_markdown(value : String) -> String {
value
.replace(old="\\", new="\\\\")
.replace(old="|", new="\\|")
.replace(old="<", new="<")
.replace(old=">", new=">")
}