// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
let svg_namespace_uri = "http://www.w3.org/2000/svg"
///|
fn svg_nonvisual_content_element(name : @xml.XmlName) -> Bool {
let admitted_namespace = name.namespace_uri is None ||
name.namespace_uri == Some(svg_namespace_uri)
admitted_namespace &&
(
name.local_name == "title" ||
name.local_name == "desc" ||
name.local_name == "metadata"
)
}
///|
fn report_span(span : @xml.SourceSpan) -> ReportSourceSpan {
{ start_offset: span.start.offset, end_offset: span.end.offset }
}
///|
pub fn report_span_slice(input : String, span : ReportSourceSpan) -> String {
"\{input.view(start_offset=span.start_offset, end_offset=span.end_offset)}"
}
///|
fn nonvisual_content_spans_or_raise(
input : String,
) -> Array[ReportSourceSpan] raise {
let spans : Array[ReportSourceSpan] = []
let reader = @xml.NamespaceReader::from_string(input)
let mut nonvisual_depth = 0
let mut content_start = 0
for event in reader.read_events_until_eof() {
match event.kind {
Start(element) if nonvisual_depth == 0 &&
svg_nonvisual_content_element(element.name) => {
nonvisual_depth = 1
content_start = event.span.end.offset
}
Start(_) if nonvisual_depth > 0 => nonvisual_depth += 1
End(_) if nonvisual_depth > 0 => {
nonvisual_depth -= 1
if nonvisual_depth == 0 {
spans.push({
start_offset: content_start,
end_offset: event.span.start.offset,
})
}
}
_ => ()
}
}
spans
}
///|
pub fn visual_semantic_input(input : String) -> String {
let spans = nonvisual_content_spans_or_raise(input) catch {
_ => return input
}
if spans.is_empty() {
return input
}
let output = StringBuilder::new()
let mut cursor = 0
for span in spans {
output.write_string(
report_span_slice(input, {
start_offset: cursor,
end_offset: span.start_offset,
}),
)
output.write_string(String::make(span.end_offset - span.start_offset, ' '))
cursor = span.end_offset
}
output.write_string(
report_span_slice(input, {
start_offset: cursor,
end_offset: input.length(),
}),
)
output.to_string()
}
///|
priv struct SourceAuditPathFrame {
path : String
child_names : Array[String]
child_counts : Array[Int]
}
///|
fn source_audit_expanded_name(name : @xml.XmlName) -> String {
match name.namespace_uri {
Some(namespace_uri) => "{\{namespace_uri}}\{name.local_name}"
None => name.local_name
}
}
///|
fn source_audit_child_path(
frame : SourceAuditPathFrame,
name : @xml.XmlName,
) -> String {
let expanded = source_audit_expanded_name(name)
let mut index = 0
while index < frame.child_names.length() {
if frame.child_names[index] == expanded {
frame.child_counts[index] += 1
return "\{frame.path}/\{expanded}[\{frame.child_counts[index]}]"
}
index += 1
}
frame.child_names.push(expanded)
frame.child_counts.push(1)
"\{frame.path}/\{expanded}[1]"
}
///|
fn source_audit_metadata_attribute(attribute : @xml.NamespaceAttribute) -> Bool {
attribute.name.prefix is None &&
(
attribute.name.local_name.has_prefix("aria-") ||
attribute.name.local_name.has_prefix("data-")
)
}
///|
/// One exact authored fact in the optional nonvisual source audit.
pub(all) struct SourceAuditFact {
kind : String
path : String
name : String
authored_value : String
source_span : ReportSourceSpan
} derive(ToJson)
///|
/// One changed, inserted, or deleted nonvisual source fact.
pub(all) struct SourceAuditDifference {
id : String
kind : String
path : String
name : String
before : SourceAuditFact?
after : SourceAuditFact?
} derive(ToJson)
///|
/// A bounded parse failure from one side of a source-only audit.
pub(all) struct SourceAuditDiagnostic {
code : String
source_role : String
source_span : ReportSourceSpan?
} derive(ToJson)
///|
/// An independently versioned source-only nonvisual metadata audit.
pub(all) struct SourceAuditReport {
audit_schema_version : String
analysis_status : String
differences : Array[SourceAuditDifference]
diagnostics : Array[SourceAuditDiagnostic]
} derive(ToJson)
///|
/// Serialize this source audit as indented JSON.
pub fn SourceAuditReport::to_json_string(self : SourceAuditReport) -> String {
ToJson::to_json(self).stringify(indent=2)
}
///|
/// Serialize this source audit without presentation whitespace.
pub fn SourceAuditReport::to_compact_json_string(
self : SourceAuditReport,
) -> String {
ToJson::to_json(self).stringify()
}
///|
priv struct OpenSourceAuditContent {
kind : String
path : String
name : String
start_offset : Int
}
///|
fn collect_nonvisual_source_facts_or_raise(
input : String,
) -> Array[SourceAuditFact] raise {
let facts : Array[SourceAuditFact] = []
let frames : Array[SourceAuditPathFrame] = [
{ path: "", child_names: [], child_counts: [] },
]
let reader = @xml.NamespaceReader::from_string(input)
let mut nonvisual_depth = 0
let mut open_content : OpenSourceAuditContent? = None
for event in reader.read_events_until_eof() {
match event.kind {
Start(element) => {
let path = source_audit_child_path(
frames[frames.length() - 1],
element.name,
)
if nonvisual_depth == 0 {
if svg_nonvisual_content_element(element.name) {
nonvisual_depth = 1
open_content = Some({
kind: "element_content",
path,
name: source_audit_expanded_name(element.name),
start_offset: event.span.end.offset,
})
} else {
for attribute in element.attributes {
if source_audit_metadata_attribute(attribute) {
let span = report_span(attribute.value_span)
facts.push({
kind: "attribute",
path,
name: source_audit_expanded_name(attribute.name),
authored_value: report_span_slice(input, span),
source_span: span,
})
}
}
}
} else {
nonvisual_depth += 1
}
frames.push({ path, child_names: [], child_counts: [] })
}
Empty(element) => {
let path = source_audit_child_path(
frames[frames.length() - 1],
element.name,
)
if nonvisual_depth == 0 {
if svg_nonvisual_content_element(element.name) {
let content_offset = event.span.end.offset - 2
facts.push({
kind: "element_content",
path,
name: source_audit_expanded_name(element.name),
authored_value: "",
source_span: {
start_offset: content_offset,
end_offset: content_offset,
},
})
} else {
for attribute in element.attributes {
if source_audit_metadata_attribute(attribute) {
let span = report_span(attribute.value_span)
facts.push({
kind: "attribute",
path,
name: source_audit_expanded_name(attribute.name),
authored_value: report_span_slice(input, span),
source_span: span,
})
}
}
}
}
}
End(_) => {
if nonvisual_depth > 0 {
nonvisual_depth -= 1
if nonvisual_depth == 0 {
match open_content {
Some(content) => {
let span : ReportSourceSpan = {
start_offset: content.start_offset,
end_offset: event.span.start.offset,
}
facts.push({
kind: content.kind,
path: content.path,
name: content.name,
authored_value: report_span_slice(input, span),
source_span: span,
})
}
None => ()
}
open_content = None
}
}
ignore(frames.pop())
}
_ => ()
}
}
facts.sort_by(fn(left, right) {
source_audit_fact_key(left).compare(source_audit_fact_key(right))
})
facts
}
///|
fn source_audit_fact_key(fact : SourceAuditFact) -> String {
"\{fact.kind}\u{0}\{fact.path}\u{0}\{fact.name}"
}
///|
fn source_audit_fact_by_key(
facts : Array[SourceAuditFact],
key : String,
) -> SourceAuditFact? {
for fact in facts {
if source_audit_fact_key(fact) == key {
return Some(fact)
}
}
None
}
///|
priv enum SourceAuditCollection {
SourceAuditCollected(Array[SourceAuditFact])
SourceAuditParseFailed(SourceAuditDiagnostic)
}
///|
fn collect_nonvisual_source_facts(
input : String,
source_role : String,
) -> SourceAuditCollection {
try collect_nonvisual_source_facts_or_raise(input) catch {
@xml.At(error=_, span~) =>
SourceAuditParseFailed({
code: "svg_parse_failed",
source_role,
source_span: Some(report_span(span)),
})
_ =>
SourceAuditParseFailed({
code: "svg_parse_failed",
source_role,
source_span: None,
})
} noraise {
facts => SourceAuditCollected(facts)
}
}
///|
/// Audit nonvisual SVG metadata independently from visual comparison.
pub fn audit_nonvisual_metadata(
before_svg : String,
after_svg : String,
) -> SourceAuditReport {
let before = collect_nonvisual_source_facts(before_svg, "before")
let after = collect_nonvisual_source_facts(after_svg, "after")
let diagnostics : Array[SourceAuditDiagnostic] = []
let before_facts = match before {
SourceAuditCollected(facts) => facts
SourceAuditParseFailed(diagnostic) => {
diagnostics.push(diagnostic)
[]
}
}
let after_facts = match after {
SourceAuditCollected(facts) => facts
SourceAuditParseFailed(diagnostic) => {
diagnostics.push(diagnostic)
[]
}
}
if !diagnostics.is_empty() {
return {
audit_schema_version: "1.0",
analysis_status: "failed",
differences: [],
diagnostics,
}
}
let keys : Array[String] = []
for fact in before_facts + after_facts {
let key = source_audit_fact_key(fact)
if !keys.contains(key) {
keys.push(key)
}
}
keys.sort()
let differences : Array[SourceAuditDifference] = []
for key in keys {
let before_fact = source_audit_fact_by_key(before_facts, key)
let after_fact = source_audit_fact_by_key(after_facts, key)
if before_fact.map(fact => fact.authored_value) ==
after_fact.map(fact => fact.authored_value) {
continue
}
let template = match (before_fact, after_fact) {
(Some(fact), _) | (_, Some(fact)) => fact
_ => abort("source audit key has no fact")
}
differences.push({
id: "source-audit:\{differences.length()}",
kind: template.kind,
path: template.path,
name: template.name,
before: before_fact,
after: after_fact,
})
}
{
audit_schema_version: "1.0",
analysis_status: "complete",
differences,
diagnostics: [],
}
}