///|
/// Report exporters are intentionally text-first. They work in WASM, command
/// line tools, and CI logs without needing filesystem or terminal extensions.
///|
pub(all) struct ReportOptions {
include_manifest : Bool
include_diagnostics : Bool
include_suggestions : Bool
include_unchanged_diff : Bool
max_items : Int
} derive(Eq, Debug)
///|
pub fn default_report_options() -> ReportOptions {
{
include_manifest: true,
include_diagnostics: true,
include_suggestions: true,
include_unchanged_diff: false,
max_items: 50,
}
}
///|
pub fn compact_report_options() -> ReportOptions {
{
include_manifest: false,
include_diagnostics: true,
include_suggestions: false,
include_unchanged_diff: false,
max_items: 20,
}
}
///|
pub fn diagnostic_severity_name(severity : Severity) -> String {
match severity {
Warning => "warning"
Error => "error"
}
}
///|
pub fn diagnostic_to_markdown(diagnostic : Diagnostic) -> String {
"- " +
diagnostic_severity_name(diagnostic.severity) +
" `" +
diagnostic.code +
"` line " +
diagnostic.line.to_string() +
": " +
diagnostic.message
}
///|
pub fn diagnostics_to_markdown(diagnostics : Array[Diagnostic]) -> String {
if diagnostics.length() == 0 {
return "- no diagnostics"
}
let lines : Array[String] = []
for diagnostic in diagnostics {
lines.push(diagnostic_to_markdown(diagnostic))
}
lines.join("\n")
}
///|
pub fn document_report_markdown(output : ParseOutput) -> String {
let doc = output.document
let buf = StringBuilder(size_hint=256)
buf.write_string("## MoonFolio Document\n\n")
buf.write_string("- format: `")
buf.write_string(format_name(doc.format))
buf.write_string("`\n")
buf.write_string("- fields: ")
buf.write_string(doc.fields.length().to_string())
buf.write_string("\n")
buf.write_string("- metadata lines: ")
buf.write_string(document_metadata_line_count(doc).to_string())
buf.write_string("\n")
buf.write_string("- diagnostics: ")
buf.write_string(output.diagnostics.length().to_string())
buf.write_string("\n")
if doc.order.length() > 0 {
buf.write_string("\n### Fields\n\n")
for key in doc.order {
match doc.fields.get(key) {
Some(value) => {
buf.write_string("- `")
buf.write_string(key)
buf.write_string("`: ")
buf.write_string(meta_value_display(value))
buf.write_string("\n")
}
None => ()
}
}
}
buf.write_string("\n### Diagnostics\n\n")
buf.write_string(diagnostics_to_markdown(output.diagnostics))
buf.to_string()
}
///|
pub fn record_report_markdown(record : FolioRecord) -> String {
let buf = StringBuilder(size_hint=256)
buf.write_string("### ")
buf.write_string(record.path)
buf.write_string("\n\n")
buf.write_string("- title: ")
buf.write_string(record.folio.title)
buf.write_string("\n")
buf.write_string("- slug: `")
buf.write_string(record.folio.slug)
buf.write_string("`\n")
buf.write_string("- permalink: `")
buf.write_string(record.folio.permalink)
buf.write_string("`\n")
buf.write_string("- fingerprint: `")
buf.write_string(record.folio.fingerprint)
buf.write_string("`\n")
buf.write_string("- status: ")
buf.write_string(record_status(record))
buf.write_string("\n")
buf.write_string("- labels: ")
buf.write_string(record_label_line(record))
buf.write_string("\n")
if record.diagnostics.length() > 0 {
buf.write_string("\n")
buf.write_string(diagnostics_to_markdown(record.diagnostics))
buf.write_string("\n")
}
buf.to_string()
}
///|
pub fn registry_report_markdown(
registry : FolioRegistry,
options : ReportOptions,
) -> String {
let summary = registry_summary(registry)
let buf = StringBuilder(size_hint=512)
buf.write_string("## MoonFolio Registry\n\n")
buf.write_string("- total: ")
buf.write_string(summary.total.to_string())
buf.write_string("\n")
buf.write_string("- valid: ")
buf.write_string(summary.valid.to_string())
buf.write_string("\n")
buf.write_string("- invalid: ")
buf.write_string(summary.invalid.to_string())
buf.write_string("\n")
buf.write_string("- warnings: ")
buf.write_string(summary.warnings.to_string())
buf.write_string("\n")
buf.write_string("- drafts: ")
buf.write_string(summary.drafts.to_string())
buf.write_string("\n")
buf.write_string("- health score: ")
buf.write_string(summary.health_score.to_string())
buf.write_string("\n")
write_format_counts(buf, summary.formats)
write_tag_counts(buf, summary.tags, options.max_items)
if summary.duplicate_slugs.length() > 0 {
write_duplicate_section(buf, "Duplicate Slugs", summary.duplicate_slugs)
}
if summary.duplicate_fingerprints.length() > 0 {
write_duplicate_section(
buf,
"Duplicate Fingerprints",
summary.duplicate_fingerprints,
)
}
buf.write_string("\n### Records\n\n")
let records = registry_sort_by_path(registry).records
let limit = report_limit(records.length(), options.max_items)
for i in 0.. limit {
buf.write_string("- ")
buf.write_string((records.length() - limit).to_string())
buf.write_string(" records omitted by report options\n")
}
if options.include_manifest {
buf.write_string("\n### Manifest\n\n```text\n")
buf.write_string(registry_manifest_sorted(registry))
buf.write_string("\n```\n")
}
buf.to_string()
}
///|
pub fn profile_report_markdown(report : ProfileReport) -> String {
let buf = StringBuilder(size_hint=256)
buf.write_string("## MoonFolio Profile\n\n")
buf.write_string("- ok: ")
buf.write_string(report.ok.to_string())
buf.write_string("\n")
buf.write_string("- score: ")
buf.write_string(report.score.to_string())
buf.write_string("\n")
buf.write_string("- checked: ")
buf.write_string(report.checked.length().to_string())
buf.write_string("\n")
buf.write_string("- errors: ")
buf.write_string(count_error_findings(report.findings).to_string())
buf.write_string("\n")
buf.write_string("- warnings: ")
buf.write_string(count_warning_findings(report.findings).to_string())
buf.write_string("\n")
if report.findings.length() > 0 {
buf.write_string("\n### Findings\n\n")
for finding in report.findings {
buf.write_string("- ")
buf.write_string(diagnostic_severity_name(finding.severity))
buf.write_string(" `")
buf.write_string(profile_kind_code(finding.kind))
buf.write_string("`: ")
buf.write_string(finding.message)
buf.write_string("\n")
}
}
if report.suggestions.length() > 0 {
buf.write_string("\n### Suggestions\n\n")
for suggestion in report.suggestions {
buf.write_string("- ")
buf.write_string(suggestion)
buf.write_string("\n")
}
}
buf.to_string()
}
///|
pub fn diff_report_markdown(
report : DiffReport,
options : ReportOptions,
) -> String {
let selected = if options.include_unchanged_diff {
report
} else {
snapshot_without_unchanged(report)
}
let buf = StringBuilder(size_hint=256)
buf.write_string("## MoonFolio Diff\n\n")
buf.write_string("- ")
buf.write_string(diff_summary_line(report))
buf.write_string("\n")
buf.write_string("- release safe: ")
buf.write_string(diff_is_release_safe(report).to_string())
buf.write_string("\n")
if selected.changes.length() > 0 {
buf.write_string("\n### Changes\n\n")
let limit = report_limit(selected.changes.length(), options.max_items)
for i in 0.. `")
buf.write_string(change.after_fingerprint)
buf.write_string("`")
}
buf.write_string("\n")
}
}
buf.to_string()
}
///|
pub fn document_report_json(output : ParseOutput) -> String {
let doc = output.document
let buf = StringBuilder(size_hint=256)
buf.write_string("{")
json_pair_string(buf, "format", format_name(doc.format), first=true)
json_pair_int(buf, "fields", doc.fields.length(), first=false)
json_pair_int(buf, "diagnostics", output.diagnostics.length(), first=false)
buf.write_string(",\"keys\":")
json_string_array(buf, doc.order)
buf.write_string(",\"diagnostic_items\":")
diagnostics_json_array(buf, output.diagnostics)
buf.write_string("}")
buf.to_string()
}
///|
pub fn registry_report_json(registry : FolioRegistry) -> String {
let summary = registry_summary(registry)
let buf = StringBuilder(size_hint=512)
buf.write_string("{")
json_pair_int(buf, "total", summary.total, first=true)
json_pair_int(buf, "valid", summary.valid, first=false)
json_pair_int(buf, "invalid", summary.invalid, first=false)
json_pair_int(buf, "errors", summary.errors, first=false)
json_pair_int(buf, "warnings", summary.warnings, first=false)
json_pair_int(buf, "drafts", summary.drafts, first=false)
json_pair_int(buf, "health_score", summary.health_score, first=false)
buf.write_string(",\"paths\":")
json_string_array(buf, registry_paths(registry))
buf.write_string(",\"slugs\":")
json_string_array(buf, registry_slugs(registry))
buf.write_string(",\"tags\":")
json_string_array(buf, registry_unique_tags(registry))
buf.write_string("}")
buf.to_string()
}
///|
pub fn profile_report_json(report : ProfileReport) -> String {
let buf = StringBuilder(size_hint=256)
buf.write_string("{")
json_pair_bool(buf, "ok", report.ok, first=true)
json_pair_int(buf, "score", report.score, first=false)
json_pair_int(buf, "findings", report.findings.length(), first=false)
json_pair_int(
buf,
"errors",
count_error_findings(report.findings),
first=false,
)
json_pair_int(
buf,
"warnings",
count_warning_findings(report.findings),
first=false,
)
buf.write_string(",\"suggestions\":")
json_string_array(buf, report.suggestions)
buf.write_string("}")
buf.to_string()
}
///|
pub fn diff_report_json(report : DiffReport) -> String {
let buf = StringBuilder(size_hint=256)
buf.write_string("{")
json_pair_bool(buf, "has_changes", report.has_changes, first=true)
json_pair_int(buf, "added", report.added, first=false)
json_pair_int(buf, "removed", report.removed, first=false)
json_pair_int(buf, "changed", report.changed, first=false)
json_pair_int(buf, "renamed", report.renamed, first=false)
json_pair_int(buf, "unchanged", report.unchanged, first=false)
buf.write_string(",\"paths\":")
json_string_array(buf, diff_paths(report))
buf.write_string("}")
buf.to_string()
}
///|
pub fn meta_value_display(value : MetaValue) -> String {
match value {
Text(text) => text
Bool(flag) => flag.to_string()
Int(number) => number.to_string()
List(items) => "[" + items.join(", ") + "]"
}
}
///|
pub fn record_status(record : FolioRecord) -> String {
if record_has_errors(record) {
"error"
} else if record_has_warnings(record) {
"warning"
} else if record_is_draft(record) {
"draft"
} else {
"ok"
}
}
///|
fn write_format_counts(
buf : StringBuilder,
counts : Array[FormatCount],
) -> Unit {
if counts.length() == 0 {
return
}
buf.write_string("\n### Formats\n\n")
for item in counts {
buf.write_string("- `")
buf.write_string(format_name(item.format))
buf.write_string("`: ")
buf.write_string(item.count.to_string())
buf.write_string("\n")
}
}
///|
fn write_tag_counts(
buf : StringBuilder,
counts : Array[TagCount],
max_items : Int,
) -> Unit {
if counts.length() == 0 {
return
}
buf.write_string("\n### Tags\n\n")
let limit = report_limit(counts.length(), max_items)
for i in 0.. Unit {
buf.write_string("\n### ")
buf.write_string(title)
buf.write_string("\n\n")
for duplicate in duplicates {
buf.write_string("- `")
buf.write_string(duplicate.value)
buf.write_string("`: ")
buf.write_string(duplicate.paths.join(", "))
buf.write_string("\n")
}
}
///|
fn report_limit(length : Int, max_items : Int) -> Int {
if max_items <= 0 || max_items > length {
length
} else {
max_items
}
}
///|
fn json_pair_string(
buf : StringBuilder,
key : String,
value : String,
first~ : Bool,
) -> Unit {
if !first {
buf.write_string(",")
}
buf.write_string("\"")
buf.write_string(escape_string(key))
buf.write_string("\":\"")
buf.write_string(escape_string(value))
buf.write_string("\"")
}
///|
fn json_pair_int(
buf : StringBuilder,
key : String,
value : Int,
first~ : Bool,
) -> Unit {
if !first {
buf.write_string(",")
}
buf.write_string("\"")
buf.write_string(escape_string(key))
buf.write_string("\":")
buf.write_string(value.to_string())
}
///|
fn json_pair_bool(
buf : StringBuilder,
key : String,
value : Bool,
first~ : Bool,
) -> Unit {
if !first {
buf.write_string(",")
}
buf.write_string("\"")
buf.write_string(escape_string(key))
buf.write_string("\":")
buf.write_string(value.to_string())
}
///|
fn json_string_array(buf : StringBuilder, values : Array[String]) -> Unit {
buf.write_string("[")
for i in 0.. 0 {
buf.write_string(",")
}
buf.write_string("\"")
buf.write_string(escape_string(values[i]))
buf.write_string("\"")
}
buf.write_string("]")
}
///|
fn diagnostics_json_array(
buf : StringBuilder,
diagnostics : Array[Diagnostic],
) -> Unit {
buf.write_string("[")
for i in 0.. 0 {
buf.write_string(",")
}
let diagnostic = diagnostics[i]
buf.write_string("{")
json_pair_string(
buf,
"severity",
diagnostic_severity_name(diagnostic.severity),
first=true,
)
json_pair_string(buf, "code", diagnostic.code, first=false)
json_pair_int(buf, "line", diagnostic.line, first=false)
json_pair_string(buf, "message", diagnostic.message, first=false)
buf.write_string("}")
}
buf.write_string("]")
}