///|
/// A registry is MoonFolio's site-level view: many parsed documents become a
/// searchable and reviewable inventory of content folios.
///|
pub(all) struct DocumentSource {
path : String
text : String
} derive(Eq, Debug)
///|
pub(all) struct FolioRecord {
path : String
document : Document
folio : ContentFolio
diagnostics : Array[Diagnostic]
labels : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct FolioRegistry {
base_path : String
records : Array[FolioRecord]
} derive(Eq, Debug)
///|
pub(all) struct FormatCount {
format : MatterFormat
count : Int
} derive(Eq, Debug)
///|
pub(all) struct TagCount {
tag : String
count : Int
} derive(Eq, Debug)
///|
pub(all) struct DuplicateValue {
value : String
paths : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct FormatBucket {
format : MatterFormat
records : Array[FolioRecord]
} derive(Eq, Debug)
///|
pub(all) struct TagBucket {
tag : String
records : Array[FolioRecord]
} derive(Eq, Debug)
///|
pub(all) struct RegistryFilter {
tag : String
path_prefix : String
use_format : Bool
format : MatterFormat
use_draft : Bool
draft : Bool
valid_only : Bool
} derive(Eq, Debug)
///|
pub(all) struct RegistrySummary {
total : Int
valid : Int
invalid : Int
errors : Int
warnings : Int
drafts : Int
published : Int
unique_slugs : Int
unique_fingerprints : Int
duplicate_slugs : Array[DuplicateValue]
duplicate_fingerprints : Array[DuplicateValue]
formats : Array[FormatCount]
tags : Array[TagCount]
health_score : Int
} derive(Eq, Debug)
///|
pub fn registry_empty(base_path : String) -> FolioRegistry {
{ base_path, records: [] }
}
///|
pub fn registry_filter_all() -> RegistryFilter {
{
tag: "",
path_prefix: "",
use_format: false,
format: None,
use_draft: false,
draft: false,
valid_only: false,
}
}
///|
pub fn registry_from_sources(
sources : Array[DocumentSource],
base_path : String,
required : Array[String],
) -> FolioRegistry {
let records : Array[FolioRecord] = []
for source in sources {
records.push(record_from_source(source, base_path, required))
}
{ base_path, records }
}
///|
pub fn registry_from_documents(
docs : Array[Document],
base_path : String,
) -> FolioRegistry {
let records : Array[FolioRecord] = []
for i in 0.. FolioRecord {
let output = parse_with_required(source.text, required)
record_from_parse_output(source.path, output, base_path)
}
///|
pub fn record_from_parse_output(
path : String,
output : ParseOutput,
base_path : String,
) -> FolioRecord {
record_from_document(path, output.document, output.diagnostics, base_path)
}
///|
pub fn record_from_document(
path : String,
doc : Document,
diagnostics : Array[Diagnostic],
base_path : String,
) -> FolioRecord {
let folio = build_folio(doc, base_path)
{
path,
document: doc,
folio,
diagnostics: copy_diagnostics(diagnostics),
labels: record_labels(doc),
}
}
///|
pub fn registry_add_record(
registry : FolioRegistry,
record : FolioRecord,
) -> FolioRegistry {
let records = copy_records(registry.records)
records.push(record)
{ base_path: registry.base_path, records }
}
///|
pub fn registry_add_source(
registry : FolioRegistry,
source : DocumentSource,
required : Array[String],
) -> FolioRegistry {
registry_add_record(
registry,
record_from_source(source, registry.base_path, required),
)
}
///|
pub fn registry_replace_record(
registry : FolioRegistry,
record : FolioRecord,
) -> FolioRegistry {
let records : Array[FolioRecord] = []
let mut replaced = false
for existing in registry.records {
if existing.path == record.path {
records.push(record)
replaced = true
} else {
records.push(existing)
}
}
if !replaced {
records.push(record)
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_remove_path(
registry : FolioRegistry,
path : String,
) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
if record.path != path {
records.push(record)
}
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_rebase(
registry : FolioRegistry,
base_path : String,
) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
records.push(
record_from_document(
record.path,
record.document,
record.diagnostics,
base_path,
),
)
}
{ base_path, records }
}
///|
pub fn registry_merge(
left : FolioRegistry,
right : FolioRegistry,
) -> FolioRegistry {
let mut merged = {
base_path: left.base_path,
records: copy_records(left.records),
}
for record in right.records {
merged = registry_replace_record(merged, record)
}
merged
}
///|
pub fn registry_contains_path(registry : FolioRegistry, path : String) -> Bool {
registry.records.any(record => record.path == path)
}
///|
pub fn registry_find_by_path(
registry : FolioRegistry,
path : String,
) -> FolioRecord? {
for record in registry.records {
if record.path == path {
return Some(record)
}
}
None
}
///|
pub fn registry_find_by_slug(
registry : FolioRegistry,
slug : String,
) -> FolioRecord? {
for record in registry.records {
if record.folio.slug == slug {
return Some(record)
}
}
None
}
///|
pub fn registry_find_by_permalink(
registry : FolioRegistry,
permalink : String,
) -> FolioRecord? {
for record in registry.records {
if record.folio.permalink == permalink {
return Some(record)
}
}
None
}
///|
pub fn registry_find_by_fingerprint(
registry : FolioRegistry,
fingerprint : String,
) -> FolioRecord? {
for record in registry.records {
if record.folio.fingerprint == fingerprint {
return Some(record)
}
}
None
}
///|
pub fn registry_filter(
registry : FolioRegistry,
filter : RegistryFilter,
) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
if record_matches_filter(record, filter) {
records.push(record)
}
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_by_tag(registry : FolioRegistry, tag : String) -> FolioRegistry {
let filter = registry_filter_all()
registry_filter(registry, {
tag: normalize_tag(tag),
path_prefix: filter.path_prefix,
use_format: filter.use_format,
format: filter.format,
use_draft: filter.use_draft,
draft: filter.draft,
valid_only: filter.valid_only,
})
}
///|
pub fn registry_by_format(
registry : FolioRegistry,
format : MatterFormat,
) -> FolioRegistry {
registry_filter(registry, {
tag: "",
path_prefix: "",
use_format: true,
format,
use_draft: false,
draft: false,
valid_only: false,
})
}
///|
pub fn registry_by_path_prefix(
registry : FolioRegistry,
prefix : String,
) -> FolioRegistry {
registry_filter(registry, {
tag: "",
path_prefix: prefix,
use_format: false,
format: None,
use_draft: false,
draft: false,
valid_only: false,
})
}
///|
pub fn registry_drafts(registry : FolioRegistry) -> FolioRegistry {
registry_filter(registry, {
tag: "",
path_prefix: "",
use_format: false,
format: None,
use_draft: true,
draft: true,
valid_only: false,
})
}
///|
pub fn registry_published(registry : FolioRegistry) -> FolioRegistry {
registry_filter(registry, {
tag: "",
path_prefix: "",
use_format: false,
format: None,
use_draft: true,
draft: false,
valid_only: false,
})
}
///|
pub fn registry_valid(registry : FolioRegistry) -> FolioRegistry {
registry_filter(registry, {
tag: "",
path_prefix: "",
use_format: false,
format: None,
use_draft: false,
draft: false,
valid_only: true,
})
}
///|
pub fn registry_invalid(registry : FolioRegistry) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
if record_has_errors(record) {
records.push(record)
}
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_with_warnings(registry : FolioRegistry) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
if record_has_warnings(record) {
records.push(record)
}
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_summary(registry : FolioRegistry) -> RegistrySummary {
let total = registry.records.length()
let mut valid = 0
let mut invalid = 0
let mut errors = 0
let mut warnings = 0
let mut drafts = 0
let mut published = 0
for record in registry.records {
if record_has_errors(record) {
invalid = invalid + 1
} else {
valid = valid + 1
}
errors = errors + count_error_diagnostics(record.diagnostics)
warnings = warnings + count_warning_diagnostics(record.diagnostics)
if record_is_draft(record) {
drafts = drafts + 1
} else {
published = published + 1
}
}
let duplicate_slugs = registry_duplicate_slugs(registry)
let duplicate_fingerprints = registry_duplicate_fingerprints(registry)
let formats = registry_format_counts(registry)
let tags = registry_tag_counts(registry)
let unique_slugs = registry_unique_slugs(registry).length()
let unique_fingerprints = registry_unique_fingerprints(registry).length()
let health_score = registry_health_score_parts(
total,
errors,
warnings,
duplicate_slugs.length(),
duplicate_fingerprints.length(),
)
{
total,
valid,
invalid,
errors,
warnings,
drafts,
published,
unique_slugs,
unique_fingerprints,
duplicate_slugs,
duplicate_fingerprints,
formats,
tags,
health_score,
}
}
///|
pub fn registry_health_score(registry : FolioRegistry) -> Int {
registry_summary(registry).health_score
}
///|
pub fn registry_manifest(registry : FolioRegistry) -> String {
let lines : Array[String] = []
for record in registry.records {
lines.push(record.folio.manifest_line)
}
lines.join("\n")
}
///|
pub fn registry_manifest_sorted(registry : FolioRegistry) -> String {
let records = registry_sort_by_slug(registry).records
let lines : Array[String] = []
for record in records {
lines.push(record.folio.manifest_line)
}
lines.join("\n")
}
///|
pub fn registry_paths(registry : FolioRegistry) -> Array[String] {
let paths : Array[String] = []
for record in registry.records {
paths.push(record.path)
}
paths
}
///|
pub fn registry_slugs(registry : FolioRegistry) -> Array[String] {
let slugs : Array[String] = []
for record in registry.records {
slugs.push(record.folio.slug)
}
slugs
}
///|
pub fn registry_titles(registry : FolioRegistry) -> Array[String] {
let titles : Array[String] = []
for record in registry.records {
titles.push(record.folio.title)
}
titles
}
///|
pub fn registry_permalinks(registry : FolioRegistry) -> Array[String] {
let values : Array[String] = []
for record in registry.records {
values.push(record.folio.permalink)
}
values
}
///|
pub fn registry_unique_tags(registry : FolioRegistry) -> Array[String] {
let tags : Array[String] = []
for record in registry.records {
for tag in record.labels {
if !contains_string(tags, tag) {
tags.push(tag)
}
}
}
sorted_strings(tags)
}
///|
pub fn registry_unique_slugs(registry : FolioRegistry) -> Array[String] {
let slugs : Array[String] = []
for record in registry.records {
if !contains_string(slugs, record.folio.slug) {
slugs.push(record.folio.slug)
}
}
sorted_strings(slugs)
}
///|
pub fn registry_unique_fingerprints(registry : FolioRegistry) -> Array[String] {
let values : Array[String] = []
for record in registry.records {
if !contains_string(values, record.folio.fingerprint) {
values.push(record.folio.fingerprint)
}
}
sorted_strings(values)
}
///|
pub fn registry_duplicate_slugs(
registry : FolioRegistry,
) -> Array[DuplicateValue] {
duplicates_from_records(registry.records, duplicate_key_slug)
}
///|
pub fn registry_duplicate_fingerprints(
registry : FolioRegistry,
) -> Array[DuplicateValue] {
duplicates_from_records(registry.records, duplicate_key_fingerprint)
}
///|
pub fn registry_format_counts(registry : FolioRegistry) -> Array[FormatCount] {
let counts : Array[FormatCount] = []
for record in registry.records {
increment_format_count(counts, record.document.format)
}
counts
}
///|
pub fn registry_tag_counts(registry : FolioRegistry) -> Array[TagCount] {
let counts : Array[TagCount] = []
for record in registry.records {
for tag in record.labels {
increment_tag_count(counts, tag)
}
}
sort_tag_counts(counts)
}
///|
pub fn registry_group_by_format(
registry : FolioRegistry,
) -> Array[FormatBucket] {
let buckets : Array[FormatBucket] = []
for record in registry.records {
push_format_bucket(buckets, record)
}
buckets
}
///|
pub fn registry_group_by_tag(registry : FolioRegistry) -> Array[TagBucket] {
let buckets : Array[TagBucket] = []
for tag in registry_unique_tags(registry) {
let records : Array[FolioRecord] = []
for record in registry.records {
if contains_string(record.labels, tag) {
records.push(record)
}
}
buckets.push({ tag, records })
}
buckets
}
///|
pub fn registry_sort_by_slug(registry : FolioRegistry) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
insert_record_by_slug(records, record)
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_sort_by_title(registry : FolioRegistry) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
insert_record_by_title(records, record)
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_sort_by_path(registry : FolioRegistry) -> FolioRegistry {
let records : Array[FolioRecord] = []
for record in registry.records {
insert_record_by_path(records, record)
}
{ base_path: registry.base_path, records }
}
///|
pub fn registry_stable_ids(registry : FolioRegistry) -> Array[String] {
let ids : Array[String] = []
for record in registry.records {
ids.push(record_stable_id(record))
}
ids
}
///|
pub fn record_stable_id(record : FolioRecord) -> String {
record.path + "|" + record.folio.slug + "|" + record.folio.fingerprint
}
///|
pub fn record_is_draft(record : FolioRecord) -> Bool {
get_bool(record.document, "draft").unwrap_or(false)
}
///|
pub fn record_has_errors(record : FolioRecord) -> Bool {
count_error_diagnostics(record.diagnostics) > 0
}
///|
pub fn record_has_warnings(record : FolioRecord) -> Bool {
count_warning_diagnostics(record.diagnostics) > 0
}
///|
pub fn record_label_line(record : FolioRecord) -> String {
if record.labels.length() == 0 {
"(none)"
} else {
record.labels.join(",")
}
}
///|
pub fn registry_table(registry : FolioRegistry) -> String {
let lines : Array[String] = ["path | slug | status | labels"]
for record in registry_sort_by_path(registry).records {
let status = if record_has_errors(record) {
"error"
} else if record_has_warnings(record) {
"warning"
} else {
"ok"
}
lines.push(
record.path +
" | " +
record.folio.slug +
" | " +
status +
" | " +
record_label_line(record),
)
}
lines.join("\n")
}
///|
fn record_matches_filter(record : FolioRecord, filter : RegistryFilter) -> Bool {
if !filter.tag.is_empty() && !contains_string(record.labels, filter.tag) {
return false
}
if !filter.path_prefix.is_empty() &&
!record.path.has_prefix(filter.path_prefix) {
return false
}
if filter.use_format && record.document.format != filter.format {
return false
}
if filter.use_draft && record_is_draft(record) != filter.draft {
return false
}
if filter.valid_only && record_has_errors(record) {
return false
}
true
}
///|
fn record_labels(doc : Document) -> Array[String] {
match get_list(doc, "tags") {
Some(tags) => normalize_tags(tags)
None => []
}
}
///|
fn copy_records(records : Array[FolioRecord]) -> Array[FolioRecord] {
let result : Array[FolioRecord] = []
for record in records {
result.push(record)
}
result
}
///|
fn copy_diagnostics(diagnostics : Array[Diagnostic]) -> Array[Diagnostic] {
let result : Array[Diagnostic] = []
for diagnostic in diagnostics {
result.push(diagnostic)
}
result
}
///|
fn duplicate_key_slug(record : FolioRecord) -> String {
record.folio.slug
}
///|
fn duplicate_key_fingerprint(record : FolioRecord) -> String {
record.folio.fingerprint
}
///|
fn duplicates_from_records(
records : Array[FolioRecord],
get_key : (FolioRecord) -> String,
) -> Array[DuplicateValue] {
let values : Array[String] = []
for record in records {
let key = get_key(record)
if !contains_string(values, key) {
values.push(key)
}
}
let duplicates : Array[DuplicateValue] = []
for value in values {
let paths : Array[String] = []
for record in records {
if get_key(record) == value {
paths.push(record.path)
}
}
if paths.length() > 1 {
duplicates.push({ value, paths })
}
}
duplicates
}
///|
fn increment_format_count(
counts : Array[FormatCount],
format : MatterFormat,
) -> Unit {
for i in 0.. Unit {
for i in 0.. Array[TagCount] {
let sorted : Array[TagCount] = []
for item in counts {
insert_tag_count(sorted, item)
}
sorted
}
///|
fn push_format_bucket(
buckets : Array[FormatBucket],
record : FolioRecord,
) -> Unit {
for i in 0.. Unit {
for i in 0.. Unit {
for i in 0.. Unit {
for i in 0.. Unit {
for i in 0.. Int {
if total == 0 {
return 0
}
let score = 100 -
errors * 18 -
warnings * 6 -
duplicate_slugs * 12 -
duplicate_fingerprints * 8
clamp_int(score, 0, 100)
}