use std::collections::HashMap;
use std::rc::Rc;
use askama::Template;
use crate::config;
use crate::ticket_abstraction::AbstractTicket;
use crate::ticket_abstraction::TicketId;
#[derive(Template)]
#[template(path = "reference.adoc", escape = "none")]
struct Leaf<'a> {
id: &'a str,
title: &'a str,
intro_abstract: &'a str,
release_notes: &'a [String],
}
#[derive(Template)]
#[template(path = "assembly.adoc", escape = "none")]
struct Assembly<'a> {
id: &'a str,
title: &'a str,
intro_abstract: &'a str,
includes: &'a [String],
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum DocumentVariant {
External,
Internal,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Module {
WithContent {
file_name: String,
text: String,
included_modules: Option<Vec<Self>>,
},
Blank {
file_name: String,
content_type: &'static str,
title: String,
intro_abstract: String,
module_id: String,
},
}
impl Module {
pub fn include_statement(&self) -> String {
format!("include::{}[leveloffset=+1]", self.file_name())
}
pub fn file_name(&self) -> &str {
match self {
Self::Blank { file_name, .. } | Self::WithContent { file_name, .. } => file_name,
}
}
fn has_content(&self) -> bool {
match self {
Self::WithContent { .. } => true,
Self::Blank { .. } => false,
}
}
}
fn id_fragment(title: &str) -> String {
let mut title_with_replacements: String = title.to_lowercase();
let substitutions = [
(" ", "-"),
("(", ""),
(")", ""),
("?", ""),
("!", ""),
("'", ""),
("\"", ""),
("#", ""),
("%", ""),
("&", ""),
("*", ""),
(",", "-"),
(".", "-"),
("/", "-"),
(":", "-"),
(";", ""),
("@", "-at-"),
("\\", ""),
("`", ""),
("$", ""),
("^", ""),
("|", ""),
("=", "-"),
("[package]", ""),
("[option]", ""),
("[parameter]", ""),
("[variable]", ""),
("[command]", ""),
("[replaceable]", ""),
("[filename]", ""),
("[literal]", ""),
("[systemitem]", ""),
("[application]", ""),
("[function]", ""),
("[gui]", ""),
("[", ""),
("]", ""),
("{", ""),
("}", ""),
];
for (old, new) in substitutions {
title_with_replacements = title_with_replacements.replace(old, new);
}
title_with_replacements = title_with_replacements
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
while title_with_replacements.contains("--") {
title_with_replacements = title_with_replacements.replace("--", "-");
}
if title_with_replacements.ends_with('-') {
let len = title_with_replacements.len();
title_with_replacements = title_with_replacements[..len - 1].to_string();
}
title_with_replacements
}
impl config::Section {
fn render(
&self,
id: &str,
tickets: &[&AbstractTicket],
variant: DocumentVariant,
with_priv_footnote: bool,
ticket_stats: &mut HashMap<Rc<TicketId>, u32>,
) -> Option<String> {
let matching_tickets: Vec<_> = tickets.iter().filter(|t| self.matches_ticket(t)).collect();
for ticket in &matching_tickets {
ticket_stats
.entry(Rc::clone(&ticket.id))
.and_modify(|counter| *counter += 1)
.or_insert(1);
}
if matching_tickets.is_empty() {
None
} else {
let release_notes: Vec<_> = matching_tickets
.iter()
.map(|t| t.release_note(variant, with_priv_footnote))
.collect();
let intro_text = self.intro_abstract.as_ref().map_or(String::new(), |s| {
format!("[role=\"_abstract\"]\n{}", s)
});
let template = Leaf {
id,
title: &self.title,
intro_abstract: &intro_text,
release_notes: &release_notes,
};
Some(format!(
":_mod-docs-content-type: REFERENCE\n{}",
template.render().expect("Failed to render a reference module template.")
))
}
}
fn modules(
&self,
tickets: &[&AbstractTicket],
prefix: Option<&str>,
variant: DocumentVariant,
with_priv_footnote: bool,
ticket_stats: &mut HashMap<Rc<TicketId>, u32>,
) -> Module {
let matching_tickets: Vec<&AbstractTicket> = tickets
.iter()
.filter(|&&t| self.matches_ticket(t))
.copied()
.collect();
let module_id_fragment = id_fragment(&self.title);
let module_id = if let Some(prefix) = prefix {
format!("{prefix}-{module_id_fragment}")
} else {
module_id_fragment
};
if let Some(sections) = &self.subsections {
let file_name = format!("assembly_{module_id}.adoc");
let included_modules: Vec<Module> = sections
.iter()
.map(|s| {
s.modules(
&matching_tickets,
Some(&module_id),
variant,
with_priv_footnote,
ticket_stats,
)
})
.filter(Module::has_content)
.collect();
if included_modules.is_empty() {
Module::Blank {
file_name,
content_type: "ASSEMBLY",
title: self.title.clone(),
intro_abstract: self.intro_abstract.as_ref().map_or("".into(), |s| {
format!("[role=\"_abstract\"]\n{}", s)
}),
module_id,
}
} else {
let include_statements: Vec<String> = included_modules
.iter()
.map(Module::include_statement)
.collect();
let intro_text = self.intro_abstract.as_ref().map_or(String::new(), |s| {
format!("[role=\"_abstract\"]\n{}", s)
});
let template = Assembly {
id: &module_id,
title: &self.title,
intro_abstract: &intro_text,
includes: &include_statements,
};
let text = format!(
":_mod-docs-content-type: ASSEMBLY\n{}",
template.render().expect("Failed to render an assembly template.")
);
Module::WithContent {
file_name,
text,
included_modules: Some(included_modules),
}
}
} else {
let text = self.render(
&module_id,
tickets,
variant,
with_priv_footnote,
ticket_stats,
);
let file_name = format!("ref_{module_id}.adoc");
if let Some(text) = text {
Module::WithContent {
file_name,
text,
included_modules: None,
}
} else {
Module::Blank {
file_name,
content_type: "REFERENCE",
title: self.title.clone(),
intro_abstract: self.intro_abstract.as_ref().map_or("".into(), |s| {
format!("[role=\"_abstract\"]\n{}", s)
}),
module_id,
}
}
}
}
fn matches_ticket(&self, ticket: &AbstractTicket) -> bool {
let matches_doc_type = match &self.filter.doc_type {
Some(doc_types) => doc_types
.iter()
.any(|dt| dt.to_lowercase() == ticket.doc_type.to_lowercase()),
None => true,
};
let matches_subsystem = match &self.filter.subsystem {
Some(ssts) => {
let unwrapped_ssts = match &ticket.subsystems {
Ok(ssts) => ssts,
Err(e) => {
log::error!("Invalid subsystems field in ticket {}.", &ticket.id);
panic!("{}", e);
}
};
ssts.iter()
.any(|sst| {
unwrapped_ssts
.iter()
.any(|ticket_sst| sst.to_lowercase() == ticket_sst.to_lowercase())
})
}
None => true,
};
let matches_component = match &self.filter.component {
Some(components) => components
.iter()
.any(|cmp| {
ticket
.components
.iter()
.any(|ticket_cmp| cmp.to_lowercase() == ticket_cmp.to_lowercase())
}),
None => true,
};
matches_doc_type && matches_subsystem && matches_component
}
}
pub fn format_document(
tickets: &[&AbstractTicket],
template: &config::Template,
variant: DocumentVariant,
with_priv_footnote: bool,
) -> (Vec<Module>, HashMap<Rc<TicketId>, u32>) {
let mut ticket_stats = HashMap::new();
for ticket in tickets {
ticket_stats.insert(Rc::clone(&ticket.id), 0);
}
let chapters: Vec<_> = template
.chapters
.iter()
.map(|section| {
section.modules(
tickets,
None,
variant,
with_priv_footnote,
&mut ticket_stats,
)
})
.collect();
log::debug!("Chapters: {:#?}", chapters);
(chapters, ticket_stats)
}
pub fn report_usage_statistics(ticket_stats: &HashMap<Rc<TicketId>, u32>) {
let unused: Vec<String> = ticket_stats
.iter()
.filter(|&(_k, &v)| v == 0)
.map(|(k, _v)| Rc::clone(k).to_string())
.collect();
let overused: Vec<String> = ticket_stats
.iter()
.filter(|&(_k, &v)| v > 1)
.map(|(k, _v)| Rc::clone(k).to_string())
.collect();
if !unused.is_empty() {
log::warn!("Tickets unused in the templates:\n\t {}", unused.join(", "));
}
if !overused.is_empty() {
log::warn!(
"Tickets used more than once in the templates:\n\t {}",
overused.join(", ")
);
}
}