1use citum_schema::options::{Config, bibliography::BibliographyConfig};
7use citum_schema::template::{Rendering, TemplateComponent, TemplateTitle, TitleType};
8
9#[derive(Debug, Clone, Default, PartialEq)]
11pub struct ProcTemplateComponent {
12 pub template_component: TemplateComponent,
14 pub template_index: Option<usize>,
16 pub value: String,
18 pub prefix: Option<String>,
20 pub suffix: Option<String>,
22 pub url: Option<String>,
24 pub ref_type: Option<String>,
26 pub config: Option<Config>,
28 pub bibliography_config: Option<BibliographyConfig>,
30 pub item_language: Option<String>,
32 pub sentence_initial: bool,
34 pub pre_formatted: bool,
36}
37
38pub type ProcTemplate = Vec<ProcTemplateComponent>;
40
41#[derive(Debug, Clone, Default, PartialEq)]
43pub struct ProcEntry {
44 pub id: String,
46 pub template: ProcTemplate,
48 pub metadata: super::format::ProcEntryMetadata,
50}
51
52use super::format::{OutputFormat, SemanticAttribute};
53use super::plain::PlainText;
54
55fn resolve_semantic_class(component: &ProcTemplateComponent) -> Option<String> {
57 use citum_schema::template::{DateVariable, SimpleVariable};
58 match &component.template_component {
59 TemplateComponent::Title(t) => match t.title {
60 TitleType::Primary => Some("citum-title".to_string()),
61 TitleType::ParentMonograph | TitleType::ParentSerial => {
62 Some("citum-container-title".to_string())
63 }
64 _ => Some("citum-title".to_string()),
65 },
66 TemplateComponent::Contributor(c) => Some(format!("citum-{}", c.contributor.as_str())),
67 TemplateComponent::Date(d) => Some(format!(
68 "citum-{}",
69 match d.date {
70 DateVariable::Issued => "issued",
71 DateVariable::Accessed => "accessed",
72 DateVariable::OriginalPublished => "original-published",
73 DateVariable::Submitted => "submitted",
74 DateVariable::EventDate => "event-date",
75 }
76 )),
77 TemplateComponent::Number(n) => Some(format!("citum-{}", n.number.as_key())),
78 TemplateComponent::Variable(v) => Some(format!(
79 "citum-{}",
80 match v.variable {
81 SimpleVariable::Doi => "doi",
82 SimpleVariable::Url => "url",
83 SimpleVariable::Isbn => "isbn",
84 SimpleVariable::Issn => "issn",
85 SimpleVariable::Pmid => "pmid",
86 SimpleVariable::Note => "note",
87 SimpleVariable::Publisher => "publisher",
88 SimpleVariable::PublisherPlace => "publisher-place",
89 SimpleVariable::ContainerTitleShort => "container-title-short",
90 SimpleVariable::Archive => "archive",
91 _ => "variable",
92 }
93 )),
94 _ => None,
95 }
96}
97
98#[must_use]
100pub fn render_component(component: &ProcTemplateComponent) -> String {
101 PlainText.finish(render_component_with_format::<PlainText>(component))
102}
103
104#[must_use]
106pub fn render_component_with_format<F: OutputFormat<Output = String>>(
107 component: &ProcTemplateComponent,
108) -> F::Output {
109 render_component_with_format_and_renderer::<F>(component, &F::default(), true)
110}
111
112pub fn render_component_with_format_and_renderer<F: OutputFormat<Output = String>>(
114 component: &ProcTemplateComponent,
115 fmt: &F,
116 show_semantics: bool,
117) -> F::Output {
118 let rendering = get_effective_rendering(component);
120
121 if rendering.suppress == Some(true) {
123 return fmt.text("");
124 }
125
126 let prefix = rendering.prefix.as_deref().unwrap_or_default();
127 let suffix = rendering.suffix.as_deref().unwrap_or_default();
128 let inner_prefix = rendering
129 .wrap
130 .as_ref()
131 .and_then(|w| w.inner_prefix.as_deref())
132 .unwrap_or_default();
133 let inner_suffix = rendering
134 .wrap
135 .as_ref()
136 .and_then(|w| w.inner_suffix.as_deref())
137 .unwrap_or_default();
138
139 let mut output = if component.pre_formatted {
140 fmt.join(vec![component.value.clone()], "")
143 } else {
144 fmt.text(&component.value)
145 };
146
147 if rendering.emph == Some(true) {
157 output = fmt.emph(output);
158 }
159 if rendering.strong == Some(true) {
160 output = fmt.strong(output);
161 }
162 if rendering.small_caps == Some(true) {
163 output = fmt.small_caps(output);
164 }
165 if rendering.vertical_align == Some(citum_schema::VerticalAlign::Superscript) {
166 output = fmt.superscript(output);
167 }
168 if rendering.quote == Some(true) {
169 output = fmt.quote(output);
170 }
171
172 if let Some(url) = &component.url {
174 output = fmt.link(url, output);
175 }
176
177 let total_inner_prefix = format!(
179 "{}{}",
180 inner_prefix,
181 component.prefix.as_deref().unwrap_or_default()
182 );
183 let total_inner_suffix = format!(
184 "{}{}",
185 component.suffix.as_deref().unwrap_or_default(),
186 inner_suffix
187 );
188
189 if !total_inner_prefix.is_empty() || !total_inner_suffix.is_empty() {
190 output = fmt.inner_affix(&total_inner_prefix, output, &total_inner_suffix);
191 }
192
193 if let Some(wrap_config) = rendering.wrap.as_ref() {
195 output = fmt.wrap_punctuation(&wrap_config.punctuation, output);
196 }
197
198 if !prefix.is_empty() || !suffix.is_empty() {
200 output = fmt.affix(prefix, output, suffix);
201 }
202
203 if show_semantics && let Some(class) = resolve_semantic_class(component) {
205 let semantic_attributes = component
206 .template_index
207 .map(|index| {
208 vec![SemanticAttribute {
209 name: "data-index",
210 value: index.to_string(),
211 }]
212 })
213 .unwrap_or_default();
214 output = fmt.semantic_with_attributes(&class, output, &semantic_attributes);
215 }
216
217 output
218}
219
220#[must_use]
222pub fn get_effective_rendering(component: &ProcTemplateComponent) -> Rendering {
223 let mut effective = Rendering::default();
224
225 if let Some(config) = &component.config {
227 match &component.template_component {
228 TemplateComponent::Title(t) => {
229 if let Some(global_title) = get_title_category_rendering(
230 &t.title,
231 component.ref_type.as_deref(),
232 component.item_language.as_deref(),
233 config,
234 ) {
235 effective.merge(&global_title);
236 }
237 }
238 TemplateComponent::Contributor(c) => {
239 if let Some(contributors_config) = &config.contributors
240 && let Some(role_config) = &contributors_config.role
241 && let Some(role_rendering) = role_config.role_rendering(&c.contributor)
242 {
243 effective.merge(&role_rendering.to_rendering());
244 }
245 }
246 _ => {}
248 }
249 }
250
251 effective.merge(component.template_component.rendering());
253
254 if component.ref_type.as_deref() == Some("dataset")
255 && component.value.starts_with('[')
256 && matches!(
257 component.template_component,
258 TemplateComponent::Title(TemplateTitle {
259 title: TitleType::Primary,
260 ..
261 })
262 )
263 && effective.suffix.as_deref() == Some(" [Dataset].")
264 {
265 effective.suffix = Some(".".to_string());
266 }
267
268 effective
269}
270
271#[must_use]
276pub fn get_title_category_rendering(
277 title_type: &TitleType,
278 ref_type: Option<&str>,
279 language: Option<&str>,
280 config: &Config,
281) -> Option<Rendering> {
282 let titles_config = config.titles.as_ref()?;
283
284 let mapped_category = ref_type.and_then(|rt| titles_config.type_mapping.get(rt));
286
287 let rendering = match title_type {
288 TitleType::ParentSerial => {
289 if let Some(cat) = mapped_category {
290 match cat.as_str() {
291 "periodical" => titles_config.periodical.as_ref(),
292 "serial" => titles_config.serial.as_ref(),
293 _ => titles_config.periodical.as_ref(),
294 }
295 } else if let Some(rt) = ref_type {
296 if matches!(
297 rt,
298 "article-journal" | "article-magazine" | "article-newspaper"
299 ) {
300 titles_config.periodical.as_ref()
301 } else {
302 titles_config.serial.as_ref()
303 }
304 } else {
305 titles_config.periodical.as_ref()
306 }
307 }
308 TitleType::ParentMonograph => titles_config
309 .container_monograph
310 .as_ref()
311 .or(titles_config.monograph.as_ref()),
312 TitleType::Primary => {
313 if let Some(cat) = mapped_category {
314 match cat.as_str() {
315 "component" => titles_config.component.as_ref(),
316 "monograph" => titles_config.monograph.as_ref(),
317 _ => titles_config.default.as_ref(),
318 }
319 } else if let Some(rt) = ref_type {
320 if matches!(
323 rt,
324 "article-journal"
325 | "article-magazine"
326 | "article-newspaper"
327 | "chapter"
328 | "entry"
329 | "entry-dictionary"
330 | "entry-encyclopedia"
331 | "paper-conference"
332 | "post"
333 | "post-weblog"
334 ) {
335 titles_config.component.as_ref()
336 } else if matches!(rt, "book" | "thesis" | "report") {
337 titles_config.monograph.as_ref()
338 } else {
339 titles_config.default.as_ref()
340 }
341 } else {
342 titles_config.default.as_ref()
343 }
344 }
345 _ => None,
346 };
347
348 let selected = rendering.or(titles_config.default.as_ref())?;
349 let mut effective = selected.to_rendering();
350 if let Some(override_rendering) = selected.locale_override(language) {
351 effective.merge(&override_rendering.to_rendering());
352 }
353 Some(effective)
354}
355
356#[cfg(test)]
357#[allow(
358 clippy::unwrap_used,
359 clippy::expect_used,
360 clippy::panic,
361 clippy::indexing_slicing,
362 clippy::todo,
363 clippy::unimplemented,
364 clippy::unreachable,
365 clippy::get_unwrap,
366 reason = "Panicking is acceptable and often desired in tests."
367)]
368mod tests {
369 use super::*;
370 use citum_schema::template::{Rendering, TemplateComponent, TemplateTitle, TitleType};
371
372 #[test]
373 fn test_render_with_emphasis() {
374 let component = ProcTemplateComponent {
375 template_component: TemplateComponent::Title(TemplateTitle {
376 title: TitleType::Primary,
377 rendering: Rendering {
378 emph: Some(true),
379 ..Default::default()
380 },
381 ..Default::default()
382 }),
383 value: "The Structure of Scientific Revolutions".to_string(),
384 ..Default::default()
385 };
386
387 let result = render_component(&component);
388 assert_eq!(result, "_The Structure of Scientific Revolutions_");
389 }
390}