///|
pub fn collect_calendar_stats(calendar : Calendar) -> CalendarStats {
let mut total_components = 0
let mut total_properties = calendar.properties.length()
let mut events = 0
let mut todos = 0
let mut journals = 0
let mut timezones = 0
let mut observances = 0
let mut alarms = 0
let mut recurring_components = 0
let mut components_with_uid = 0
let mut unknown_properties = count_unknown_calendar_properties(calendar)
let mut unsupported_properties = count_unsupported_calendar_properties(
calendar,
)
for component in calendar.components {
total_components += component_total_components(component)
total_properties += component_total_properties(component)
events += component_count_by_name(component, "VEVENT")
todos += component_count_by_name(component, "VTODO")
journals += component_count_by_name(component, "VJOURNAL")
timezones += component_count_by_name(component, "VTIMEZONE")
observances += component_count_by_name(component, "STANDARD")
observances += component_count_by_name(component, "DAYLIGHT")
alarms += component_count_by_name(component, "VALARM")
recurring_components += component_count_recurring(component)
components_with_uid += component_count_with_uid(component)
unknown_properties += component_unknown_property_count_deep(component)
unsupported_properties += component_unsupported_property_count_deep(
component,
)
}
{
calendar_properties: calendar.properties.length(),
top_level_components: calendar.components.length(),
total_components,
total_properties,
events,
todos,
journals,
timezones,
observances,
alarms,
recurring_components,
components_with_uid,
unknown_properties,
unsupported_properties,
}
}
///|
pub fn event_summaries(calendar : Calendar) -> Array[EventSummary] {
let summaries : Array[EventSummary] = []
for event in calendar.events() {
summaries.push(summarize_event(event))
}
summaries
}
///|
pub fn summarize_event(event : Component) -> EventSummary {
{
uid: event.get("UID").unwrap_or(""),
summary: event.get("SUMMARY").unwrap_or(""),
starts_at: event.get("DTSTART").unwrap_or(""),
ends_at: event.get("DTEND").unwrap_or(""),
status: event.get("STATUS").unwrap_or(""),
location: event.get("LOCATION").unwrap_or(""),
recurring: event.has("RRULE") || event.has("RDATE"),
alarm_count: event_alarm_count(event),
}
}
///|
pub fn render_calendar_report(calendar : Calendar) -> String {
let stats = collect_calendar_stats(calendar)
let validation = validate_calendar(calendar, ValidationOptions::strict())
let b = StringBuilder::new()
b.write_string("# MoonCalGuard calendar report\n\n")
b.write_string("## Summary\n\n")
b.write_string("- accepted: ")
b.write_string(validation.accepted.to_string())
b.write_string("\n")
b.write_string("- components: ")
b.write_string(stats.total_components.to_string())
b.write_string("\n")
b.write_string("- properties: ")
b.write_string(stats.total_properties.to_string())
b.write_string("\n")
b.write_string("- events: ")
b.write_string(stats.events.to_string())
b.write_string("\n")
b.write_string("- todos: ")
b.write_string(stats.todos.to_string())
b.write_string("\n")
b.write_string("- journals: ")
b.write_string(stats.journals.to_string())
b.write_string("\n")
b.write_string("- timezones: ")
b.write_string(stats.timezones.to_string())
b.write_string("\n")
b.write_string("- alarms: ")
b.write_string(stats.alarms.to_string())
b.write_string("\n")
b.write_string("- recurring: ")
b.write_string(stats.recurring_components.to_string())
b.write_string("\n\n")
b.write_string("## Events\n\n")
let summaries = event_summaries(calendar)
if summaries.length() == 0 {
b.write_string("No VEVENT components.\n\n")
} else {
for item in summaries {
b.write_string("- ")
b.write_string(render_event_summary(item))
b.write_string("\n")
}
b.write_string("\n")
}
b.write_string("## Findings\n\n")
for finding in validation.findings {
b.write_string("- [")
b.write_string(finding.severity.name())
b.write_string("] ")
b.write_string(finding.code)
b.write_string(": ")
b.write_string(finding.message)
b.write_string("\n")
}
b.to_string()
}
///|
pub fn render_calendar_json(calendar : Calendar) -> String {
let stats = collect_calendar_stats(calendar)
let validation = validate_calendar(calendar, ValidationOptions::strict())
let b = StringBuilder::new()
b.write_string("{")
b.write_string("\"accepted\":")
b.write_string(validation.accepted.to_string())
b.write_string(",\"stats\":")
b.write_string(render_stats_json(stats))
b.write_string(",\"events\":[")
let summaries = event_summaries(calendar)
let mut i = 0
while i < summaries.length() {
if i > 0 {
b.write_string(",")
}
b.write_string(render_event_summary_json(summaries[i]))
i += 1
}
b.write_string("],\"findings\":[")
let mut j = 0
while j < validation.findings.length() {
if j > 0 {
b.write_string(",")
}
b.write_string(render_finding_json(validation.findings[j]))
j += 1
}
b.write_string("]}")
b.to_string()
}
///|
pub fn render_stats_json(stats : CalendarStats) -> String {
let b = StringBuilder::new()
b.write_string("{")
write_json_int_field(
b,
"calendar_properties",
stats.calendar_properties,
false,
)
write_json_int_field(
b,
"top_level_components",
stats.top_level_components,
true,
)
write_json_int_field(b, "total_components", stats.total_components, true)
write_json_int_field(b, "total_properties", stats.total_properties, true)
write_json_int_field(b, "events", stats.events, true)
write_json_int_field(b, "todos", stats.todos, true)
write_json_int_field(b, "journals", stats.journals, true)
write_json_int_field(b, "timezones", stats.timezones, true)
write_json_int_field(b, "observances", stats.observances, true)
write_json_int_field(b, "alarms", stats.alarms, true)
write_json_int_field(
b,
"recurring_components",
stats.recurring_components,
true,
)
write_json_int_field(
b,
"components_with_uid",
stats.components_with_uid,
true,
)
write_json_int_field(b, "unknown_properties", stats.unknown_properties, true)
write_json_int_field(
b,
"unsupported_properties",
stats.unsupported_properties,
true,
)
b.write_string("}")
b.to_string()
}
///|
pub fn render_event_summary(summary : EventSummary) -> String {
let b = StringBuilder::new()
if summary.uid.length() > 0 {
b.write_string(summary.uid)
} else {
b.write_string("(no uid)")
}
if summary.summary.length() > 0 {
b.write_string(" ")
b.write_string(unescape_text(summary.summary))
}
if summary.starts_at.length() > 0 {
b.write_string(" start=")
b.write_string(summary.starts_at)
}
if summary.ends_at.length() > 0 {
b.write_string(" end=")
b.write_string(summary.ends_at)
}
if summary.status.length() > 0 {
b.write_string(" status=")
b.write_string(summary.status)
}
if summary.location.length() > 0 {
b.write_string(" location=")
b.write_string(unescape_text(summary.location))
}
if summary.recurring {
b.write_string(" recurring")
}
if summary.alarm_count > 0 {
b.write_string(" alarms=")
b.write_string(summary.alarm_count.to_string())
}
b.to_string()
}
///|
pub fn render_event_summary_json(summary : EventSummary) -> String {
let b = StringBuilder::new()
b.write_string("{")
write_json_string_field(b, "uid", summary.uid, false)
write_json_string_field(b, "summary", summary.summary, true)
write_json_string_field(b, "starts_at", summary.starts_at, true)
write_json_string_field(b, "ends_at", summary.ends_at, true)
write_json_string_field(b, "status", summary.status, true)
write_json_string_field(b, "location", summary.location, true)
b.write_string(",\"recurring\":")
b.write_string(summary.recurring.to_string())
b.write_string(",\"alarm_count\":")
b.write_string(summary.alarm_count.to_string())
b.write_string("}")
b.to_string()
}
///|
pub fn render_finding_json(finding : Finding) -> String {
let b = StringBuilder::new()
b.write_string("{")
write_json_string_field(b, "severity", finding.severity.name(), false)
write_json_string_field(b, "code", finding.code, true)
write_json_string_field(b, "message", finding.message, true)
match finding.line {
Some(line) => {
b.write_string(",\"line\":")
b.write_string(line.to_string())
}
None => b.write_string(",\"line\":null")
}
b.write_string("}")
b.to_string()
}
///|
pub fn stats_health_score(stats : CalendarStats) -> Int {
let mut score = 100
if stats.events == 0 && stats.todos == 0 && stats.journals == 0 {
score -= 30
}
if stats.unknown_properties > 0 {
score -= stats.unknown_properties * 2
}
if stats.unsupported_properties > 0 {
score -= stats.unsupported_properties * 3
}
if stats.components_with_uid < stats.events + stats.todos + stats.journals {
score -= 10
}
if score < 0 {
0
} else {
score
}
}
///|
pub fn stats_complexity_label(stats : CalendarStats) -> String {
if stats.total_components >= 1000 {
"large"
} else if stats.total_components >= 100 {
"medium"
} else if stats.total_components >= 10 {
"small"
} else {
"tiny"
}
}
///|
pub fn stats_feature_label(stats : CalendarStats) -> String {
if stats.recurring_components > 0 && stats.alarms > 0 && stats.timezones > 0 {
"recurring-alarmed-timezoned"
} else if stats.recurring_components > 0 && stats.alarms > 0 {
"recurring-alarmed"
} else if stats.recurring_components > 0 {
"recurring"
} else if stats.alarms > 0 {
"alarmed"
} else {
"basic"
}
}
///|
pub fn render_stats_line(stats : CalendarStats) -> String {
let b = StringBuilder::new()
b.write_string(stats_complexity_label(stats))
b.write_string(" ")
b.write_string(stats_feature_label(stats))
b.write_string(" score=")
b.write_string(stats_health_score(stats).to_string())
b.write_string(" events=")
b.write_string(stats.events.to_string())
b.write_string(" todos=")
b.write_string(stats.todos.to_string())
b.write_string(" alarms=")
b.write_string(stats.alarms.to_string())
b.write_string(" recurring=")
b.write_string(stats.recurring_components.to_string())
b.to_string()
}
///|
fn component_total_components(component : Component) -> Int {
let mut total = 1
for child in component.components {
total += component_total_components(child)
}
total
}
///|
fn component_total_properties(component : Component) -> Int {
let mut total = component.properties.length()
for child in component.components {
total += component_total_properties(child)
}
total
}
///|
fn component_count_by_name(component : Component, name : String) -> Int {
let mut total = if component.name == name { 1 } else { 0 }
for child in component.components {
total += component_count_by_name(child, name)
}
total
}
///|
fn component_count_recurring(component : Component) -> Int {
let mut total = if component.has("RRULE") || component.has("RDATE") {
1
} else {
0
}
for child in component.components {
total += component_count_recurring(child)
}
total
}
///|
fn component_count_with_uid(component : Component) -> Int {
let mut total = if component.has("UID") { 1 } else { 0 }
for child in component.components {
total += component_count_with_uid(child)
}
total
}
///|
fn count_unknown_calendar_properties(calendar : Calendar) -> Int {
let mut total = 0
for prop in calendar.properties {
if !is_known_property(prop.name) {
total += 1
}
}
total
}
///|
fn count_unsupported_calendar_properties(calendar : Calendar) -> Int {
let mut total = 0
for prop in calendar.properties {
if !property_allowed_in_role(prop.name, CalendarRole) {
total += 1
}
}
total
}
///|
fn component_unknown_property_count_deep(component : Component) -> Int {
let mut total = component_unknown_property_count(component)
for child in component.components {
total += component_unknown_property_count_deep(child)
}
total
}
///|
fn component_unsupported_property_count_deep(component : Component) -> Int {
let mut total = component_unsupported_property_count(component)
for child in component.components {
total += component_unsupported_property_count_deep(child)
}
total
}
///|
fn write_json_string_field(
b : StringBuilder,
key : String,
value : String,
comma : Bool,
) -> Unit {
if comma {
b.write_string(",")
}
b.write_string("\"")
b.write_string(key)
b.write_string("\":\"")
b.write_string(json_escape(value))
b.write_string("\"")
}
///|
fn write_json_int_field(
b : StringBuilder,
key : String,
value : Int,
comma : Bool,
) -> Unit {
if comma {
b.write_string(",")
}
b.write_string("\"")
b.write_string(key)
b.write_string("\":")
b.write_string(value.to_string())
}
///|
fn json_escape(value : String) -> String {
let b = StringBuilder::new()
let mut i = 0
while i < value.length() {
let c = value[i]
if c == 34 {
b.write_string("\\\"")
} else if c == 92 {
b.write_string("\\\\")
} else if c == 10 {
b.write_string("\\n")
} else if c == 13 {
b.write_string("\\r")
} else if c == 9 {
b.write_string("\\t")
} else {
b.write_char(c.unsafe_to_char())
}
i += 1
}
b.to_string()
}