// 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.
///|
priv suberror IoError {
IoError(String)
}
///|
async fn read_svg(path : String) -> String {
if path == "-" {
@stdio.stdin.read_all().text() catch {
error => raise IoError("Failed to read stdin: \{error}")
}
} else {
@fs.read_file_to_string(path, encoding="utf8") catch {
error => raise IoError("Failed to read \{path}: \{Repr(error)}")
}
}
}
///|
fn read_resource_bundle(
resources : Array[@cli.Resource],
) -> @lib.ResourceBundle raise IoError {
let entries : Array[@lib.ResourceBundleEntry] = []
for resource in resources {
let bytes = @fs.read_file_to_bytes(resource.path) catch {
error =>
raise IoError(
"Failed to read resource \{resource.path}: \{Repr(error)}",
)
}
entries.push({
locator: resource.locator,
media_type: resource.media_type,
bytes,
})
}
{ entries, }
}
///|
fn write_file(path : String, contents : String) -> Unit raise IoError {
@fs.write_string_to_file(path, contents, encoding="utf8") catch {
error => raise IoError("Failed to write \{path}: \{Repr(error)}")
}
}
///|
async fn write_stdout(contents : String) -> Unit {
@stdio.stdout.write(contents) catch {
error => raise IoError("Failed to write report: \{error}")
}
}
///|
async fn write_error(message : String) -> Unit noraise {
@stdio.stderr.write(message + "\n") catch {
_ => ()
}
}
///|
async fn execute(config : @cli.Config) -> Int32 noraise {
try {
let before_svg = read_svg(config.before_path)
let after_svg = read_svg(config.after_path)
let before_resources = read_resource_bundle(config.before_resources)
let after_resources = read_resource_bundle(config.after_resources)
let artifact = @cli.compare_sources_with_artifact(
before_svg, after_svg, config, before_resources, after_resources,
) catch {
@lib.CheckpointBudgetExceeded(max_checkpoints~) => {
write_error(
"Deterministic checkpoint budget \{max_checkpoints} exhausted",
)
return 1
}
@lib.Cancelled => {
write_error("Comparison cancelled")
return 1
}
@cli.InvalidArguments(message) => {
write_error(message)
return 2
}
error => {
write_error("Command failed: \{error}")
return 2
}
}
let report = artifact.report
let json = @cli.serialize_report(report, config)
match config.output_path {
Some(path) => write_file(path, json)
None => write_stdout(json + "\n")
}
match config.html_path {
Some(path) =>
write_file(
path,
@lib.render_html_report(
before_svg,
after_svg,
report,
artifact.difference_rgba,
),
)
None => ()
}
if report.analysis_status == "failed" {
1
} else {
0
}
} catch {
IoError(message) => {
write_error(message)
2
}
error => {
write_error("Command failed: \{error}")
2
}
}
}
///|
async fn run(args : ArrayView[String]) -> Int32 noraise {
let config = @cli.parse(args) catch {
InvalidArguments(message) => {
write_error(message)
return 2
}
}
execute(config)
}
///|
async fn main {
let code = run(@env.args())
if code != 0 {
@sys.exit(code)
}
}