use citum_schema::options::{Config, bibliography::BibliographyConfig};
use citum_schema::template::{Rendering, TemplateComponent, TemplateTitle, TitleType};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProcTemplateComponent {
pub template_component: TemplateComponent,
pub template_index: Option<usize>,
pub value: String,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub url: Option<String>,
pub ref_type: Option<String>,
pub config: Option<Config>,
pub bibliography_config: Option<BibliographyConfig>,
pub item_language: Option<String>,
pub sentence_initial: bool,
pub pre_formatted: bool,
}
pub type ProcTemplate = Vec<ProcTemplateComponent>;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProcEntry {
pub id: String,
pub template: ProcTemplate,
pub metadata: super::format::ProcEntryMetadata,
}
use super::format::{OutputFormat, SemanticAttribute};
use super::plain::PlainText;
fn resolve_semantic_class(component: &ProcTemplateComponent) -> Option<String> {
use citum_schema::template::{DateVariable, SimpleVariable};
match &component.template_component {
TemplateComponent::Title(t) => match t.title {
TitleType::Primary => Some("citum-title".to_string()),
TitleType::ParentMonograph | TitleType::ParentSerial => {
Some("citum-container-title".to_string())
}
_ => Some("citum-title".to_string()),
},
TemplateComponent::Contributor(c) => Some(format!("citum-{}", c.contributor.as_str())),
TemplateComponent::Date(d) => Some(format!(
"citum-{}",
match d.date {
DateVariable::Issued => "issued",
DateVariable::Accessed => "accessed",
DateVariable::OriginalPublished => "original-published",
DateVariable::Submitted => "submitted",
DateVariable::EventDate => "event-date",
}
)),
TemplateComponent::Number(n) => Some(format!("citum-{}", n.number.as_key())),
TemplateComponent::Variable(v) => Some(format!(
"citum-{}",
match v.variable {
SimpleVariable::Doi => "doi",
SimpleVariable::Url => "url",
SimpleVariable::Isbn => "isbn",
SimpleVariable::Issn => "issn",
SimpleVariable::Pmid => "pmid",
SimpleVariable::Note => "note",
SimpleVariable::Publisher => "publisher",
SimpleVariable::PublisherPlace => "publisher-place",
SimpleVariable::ContainerTitleShort => "container-title-short",
SimpleVariable::Archive => "archive",
_ => "variable",
}
)),
_ => None,
}
}
#[must_use]
pub fn render_component(component: &ProcTemplateComponent) -> String {
PlainText.finish(render_component_with_format::<PlainText>(component))
}
#[must_use]
pub fn render_component_with_format<F: OutputFormat<Output = String>>(
component: &ProcTemplateComponent,
) -> F::Output {
render_component_with_format_and_renderer::<F>(component, &F::default(), true)
}
pub fn render_component_with_format_and_renderer<F: OutputFormat<Output = String>>(
component: &ProcTemplateComponent,
fmt: &F,
show_semantics: bool,
) -> F::Output {
let rendering = get_effective_rendering(component);
if rendering.suppress == Some(true) {
return fmt.text("");
}
let prefix = rendering.prefix.as_deref().unwrap_or_default();
let suffix = rendering.suffix.as_deref().unwrap_or_default();
let inner_prefix = rendering
.wrap
.as_ref()
.and_then(|w| w.inner_prefix.as_deref())
.unwrap_or_default();
let inner_suffix = rendering
.wrap
.as_ref()
.and_then(|w| w.inner_suffix.as_deref())
.unwrap_or_default();
let mut output = if component.pre_formatted {
fmt.join(vec![component.value.clone()], "")
} else {
fmt.text(&component.value)
};
if rendering.emph == Some(true) {
output = fmt.emph(output);
}
if rendering.strong == Some(true) {
output = fmt.strong(output);
}
if rendering.small_caps == Some(true) {
output = fmt.small_caps(output);
}
if rendering.vertical_align == Some(citum_schema::VerticalAlign::Superscript) {
output = fmt.superscript(output);
}
if rendering.quote == Some(true) {
output = fmt.quote(output);
}
if let Some(url) = &component.url {
output = fmt.link(url, output);
}
let total_inner_prefix = format!(
"{}{}",
inner_prefix,
component.prefix.as_deref().unwrap_or_default()
);
let total_inner_suffix = format!(
"{}{}",
component.suffix.as_deref().unwrap_or_default(),
inner_suffix
);
if !total_inner_prefix.is_empty() || !total_inner_suffix.is_empty() {
output = fmt.inner_affix(&total_inner_prefix, output, &total_inner_suffix);
}
if let Some(wrap_config) = rendering.wrap.as_ref() {
output = fmt.wrap_punctuation(&wrap_config.punctuation, output);
}
if !prefix.is_empty() || !suffix.is_empty() {
output = fmt.affix(prefix, output, suffix);
}
if show_semantics && let Some(class) = resolve_semantic_class(component) {
let semantic_attributes = component
.template_index
.map(|index| {
vec![SemanticAttribute {
name: "data-index",
value: index.to_string(),
}]
})
.unwrap_or_default();
output = fmt.semantic_with_attributes(&class, output, &semantic_attributes);
}
output
}
#[must_use]
pub fn get_effective_rendering(component: &ProcTemplateComponent) -> Rendering {
let mut effective = Rendering::default();
if let Some(config) = &component.config {
match &component.template_component {
TemplateComponent::Title(t) => {
if let Some(global_title) = get_title_category_rendering(
&t.title,
component.ref_type.as_deref(),
component.item_language.as_deref(),
config,
) {
effective.merge(&global_title);
}
}
TemplateComponent::Contributor(c) => {
if let Some(contributors_config) = &config.contributors
&& let Some(role_config) = &contributors_config.role
&& let Some(role_rendering) = role_config.role_rendering(&c.contributor)
{
effective.merge(&role_rendering.to_rendering());
}
}
_ => {}
}
}
effective.merge(component.template_component.rendering());
if component.ref_type.as_deref() == Some("dataset")
&& component.value.starts_with('[')
&& matches!(
component.template_component,
TemplateComponent::Title(TemplateTitle {
title: TitleType::Primary,
..
})
)
&& effective.suffix.as_deref() == Some(" [Dataset].")
{
effective.suffix = Some(".".to_string());
}
effective
}
#[must_use]
pub fn get_title_category_rendering(
title_type: &TitleType,
ref_type: Option<&str>,
language: Option<&str>,
config: &Config,
) -> Option<Rendering> {
let titles_config = config.titles.as_ref()?;
let mapped_category = ref_type.and_then(|rt| titles_config.type_mapping.get(rt));
let rendering = match title_type {
TitleType::ParentSerial => {
if let Some(cat) = mapped_category {
match cat.as_str() {
"periodical" => titles_config.periodical.as_ref(),
"serial" => titles_config.serial.as_ref(),
_ => titles_config.periodical.as_ref(),
}
} else if let Some(rt) = ref_type {
if matches!(
rt,
"article-journal" | "article-magazine" | "article-newspaper"
) {
titles_config.periodical.as_ref()
} else {
titles_config.serial.as_ref()
}
} else {
titles_config.periodical.as_ref()
}
}
TitleType::ParentMonograph => titles_config
.container_monograph
.as_ref()
.or(titles_config.monograph.as_ref()),
TitleType::Primary => {
if let Some(cat) = mapped_category {
match cat.as_str() {
"component" => titles_config.component.as_ref(),
"monograph" => titles_config.monograph.as_ref(),
_ => titles_config.default.as_ref(),
}
} else if let Some(rt) = ref_type {
if matches!(
rt,
"article-journal"
| "article-magazine"
| "article-newspaper"
| "chapter"
| "entry"
| "entry-dictionary"
| "entry-encyclopedia"
| "paper-conference"
| "post"
| "post-weblog"
) {
titles_config.component.as_ref()
} else if matches!(rt, "book" | "thesis" | "report") {
titles_config.monograph.as_ref()
} else {
titles_config.default.as_ref()
}
} else {
titles_config.default.as_ref()
}
}
_ => None,
};
let selected = rendering.or(titles_config.default.as_ref())?;
let mut effective = selected.to_rendering();
if let Some(override_rendering) = selected.locale_override(language) {
effective.merge(&override_rendering.to_rendering());
}
Some(effective)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
use citum_schema::template::{Rendering, TemplateComponent, TemplateTitle, TitleType};
#[test]
fn test_render_with_emphasis() {
let component = ProcTemplateComponent {
template_component: TemplateComponent::Title(TemplateTitle {
title: TitleType::Primary,
rendering: Rendering {
emph: Some(true),
..Default::default()
},
..Default::default()
}),
value: "The Structure of Scientific Revolutions".to_string(),
..Default::default()
};
let result = render_component(&component);
assert_eq!(result, "_The Structure of Scientific Revolutions_");
}
}