///|
fn append_rule_to_sections(
sections : Array[RuleSection],
rule : String,
unsectioned : Array[String],
icann : Array[String],
private_rules : Array[String],
) -> Unit {
for section in sections {
match section {
UnsectionedRule => unsectioned.push(rule)
IcannSection => icann.push(rule)
PrivateSection => private_rules.push(rule)
}
}
}
///|
fn SuffixList::collect_serialized_rules(
self : SuffixList,
node_index : Int,
suffix : String,
unsectioned : Array[String],
icann : Array[String],
private_rules : Array[String],
) -> Unit {
if suffix.length() > 0 {
append_rule_to_sections(
self.exact_sections[node_index],
suffix,
unsectioned,
icann,
private_rules,
)
append_rule_to_sections(
self.wildcard_sections[node_index],
"*." + suffix,
unsectioned,
icann,
private_rules,
)
append_rule_to_sections(
self.exception_sections[node_index],
"!" + suffix,
unsectioned,
icann,
private_rules,
)
}
for label, child_index in self.children[node_index] {
let child_suffix = if suffix.length() == 0 {
label
} else {
label + "." + suffix
}
self.collect_serialized_rules(
child_index, child_suffix, unsectioned, icann, private_rules,
)
}
}
///|
fn section_block(name : String, rules : Array[String]) -> String {
"// ===BEGIN \{name} DOMAINS===\n" +
rules.join("\n") +
"\n// ===END \{name} DOMAINS==="
}
///|
fn compare_code_units(left : String, right : String) -> Int {
let common_length = if left.length() < right.length() {
left.length()
} else {
right.length()
}
for i in 0.. (Array[String], Array[String], Array[String]) {
let unsectioned : Array[String] = []
let icann : Array[String] = []
let private_rules : Array[String] = []
self.collect_serialized_rules(0, "", unsectioned, icann, private_rules)
unsectioned.sort_by(compare_code_units)
icann.sort_by(compare_code_units)
private_rules.sort_by(compare_code_units)
(unsectioned, icann, private_rules)
}
///|
/// Serialize the compiled rule set to deterministic, parseable PSL text.
///
/// Rules are lowercased and sorted lexicographically within their source
/// section. Comments, blank lines, trailing fields, and original rule order are
/// intentionally not retained. Non-empty output always ends in one newline.
pub fn SuffixList::to_psl_text(self : SuffixList) -> String {
let (unsectioned, icann, private_rules) = self.sorted_rules_by_section()
let blocks : Array[String] = []
if unsectioned.length() > 0 {
blocks.push(unsectioned.join("\n"))
}
if icann.length() > 0 {
blocks.push(section_block("ICANN", icann))
}
if private_rules.length() > 0 {
blocks.push(section_block("PRIVATE", private_rules))
}
if blocks.length() == 0 {
""
} else {
blocks.join("\n\n") + "\n"
}
}