1use super::output::{
9 HtmlPlaceholderRegistry, RenderedDocumentBody, append_document_bibliography,
10 bibliography_block_placeholder, render_document_bibliography_block_replacement,
11 rewrite_document_markup_for_typst, stage_document_bibliography_blocks,
12};
13use super::{BibliographyBlock, CitationParser, DocumentFormat, ParsedDocument};
14use crate::error::ProcessorError;
15use crate::processor::Processor;
16
17fn render_bibliography_section_heading(heading: &str, format: DocumentFormat) -> String {
25 match format {
26 DocumentFormat::Html => {
27 let escaped = heading
28 .replace('&', "&")
29 .replace('<', "<")
30 .replace('>', ">");
31 format!("<h2>{escaped}</h2>\n\n")
32 }
33 DocumentFormat::Latex => format!("\\subsection*{{{heading}}}\n\n"),
34 DocumentFormat::Typst => format!("== {heading}\n\n"),
35 _ => format!("## {heading}\n\n"),
36 }
37}
38
39impl Processor {
40 #[allow(
52 clippy::string_slice,
53 reason = "parser-guaranteed boundaries and indices"
54 )]
55 pub fn process_document<P, F>(
56 &self,
57 content: &str,
58 parser: &P,
59 format: DocumentFormat,
60 ) -> Result<String, ProcessorError>
61 where
62 P: CitationParser,
63 F: crate::render::format::OutputFormat<Output = String>,
64 {
65 let mut parsed = parser.parse_document(content, &self.locale);
66
67 if let Some(err) = &parsed.frontmatter_error {
68 return Err(ProcessorError::FrontmatterParse(err.clone()));
69 }
70
71 let effective_integral_override = parsed
73 .frontmatter_options
74 .as_ref()
75 .and_then(|o| o.integral_name_memory.as_ref())
76 .or(parsed.frontmatter_integral_name_memory.as_ref());
77 let owned_integral =
78 self.processor_with_document_integral_name_override(effective_integral_override);
79
80 let effective_org_override = parsed
82 .frontmatter_options
83 .as_ref()
84 .and_then(|o| o.org_abbreviation_memory.as_ref())
85 .or(parsed.frontmatter_org_abbreviation_memory.as_ref());
86 let owned_org = {
87 let base = owned_integral.as_ref().unwrap_or(self);
88 base.processor_with_document_org_abbreviation_override(effective_org_override)
89 };
90
91 let owned_bib = parsed
93 .frontmatter_options
94 .as_ref()
95 .filter(|o| o.bibliography.is_some())
96 .map(|options| {
97 let base = owned_org
98 .as_ref()
99 .or(owned_integral.as_ref())
100 .unwrap_or(self);
101 base.processor_with_bibliography_override(options)
102 });
103
104 let processor = owned_bib
105 .as_ref()
106 .or(owned_org.as_ref())
107 .or(owned_integral.as_ref())
108 .unwrap_or(self);
109 let body = &content[parsed.body_start..];
110 if let Some(groups) = parsed.frontmatter_groups.take() {
111 return Ok(processor.process_document_with_frontmatter_groups::<P, F>(
112 body, parsed, groups, parser, format,
113 ));
114 }
115
116 if !parsed.bibliography_blocks.is_empty() {
117 return Ok(processor.process_document_with_bibliography_blocks::<P, F>(
118 body,
119 std::mem::take(&mut parsed.bibliography_blocks),
120 parser,
121 format,
122 ));
123 }
124
125 Ok(processor
126 .process_document_with_default_bibliography::<P, F>(body, parsed, parser, format))
127 }
128
129 fn process_document_with_frontmatter_groups<P, F>(
139 &self,
140 body: &str,
141 parsed: ParsedDocument,
142 groups: Vec<citum_schema::grouping::BibliographyGroup>,
143 parser: &P,
144 format: DocumentFormat,
145 ) -> String
146 where
147 P: CitationParser,
148 F: crate::render::format::OutputFormat<Output = String>,
149 {
150 self.render_document_with_trailing_bibliography::<P, F, _>(
151 body,
152 parsed,
153 parser,
154 format,
155 |processor| {
156 let rendered_blocks =
157 processor.render_document_bibliography_blocks::<F>(&groups, None, None);
158 let mut output = String::new();
159 for block in rendered_blocks {
160 if block.entries.is_empty() {
161 continue;
162 }
163 if !output.is_empty() {
164 output.push_str("\n\n");
165 }
166 if let Some(heading) = block.heading {
167 output.push_str(&render_bibliography_section_heading(&heading, format));
168 }
169 output.push_str(&block.body);
170 }
171 output
172 },
173 )
174 }
175
176 fn process_document_with_bibliography_blocks<P, F>(
178 &self,
179 body: &str,
180 blocks: Vec<BibliographyBlock>,
181 parser: &P,
182 format: DocumentFormat,
183 ) -> String
184 where
185 P: CitationParser,
186 F: crate::render::format::OutputFormat<Output = String>,
187 {
188 let staged = stage_document_bibliography_blocks(body, &blocks);
189 let parsed_staged = parser.parse_document(&staged, &self.locale);
190 let mut rendered = self.render_document_body::<F>(&staged, parsed_staged, format);
191 self.replace_document_bibliography_blocks::<F>(&mut rendered, &blocks, format);
192 self.finalize_document_output::<P, F>(parser, format, rendered)
193 }
194
195 pub fn process_document_with_caller_blocks<P, F>(
205 &self,
206 content: &str,
207 blocks: &[citum_schema::grouping::BibliographyGroup],
208 parser: &P,
209 format: DocumentFormat,
210 ) -> String
211 where
212 P: CitationParser,
213 F: crate::render::format::OutputFormat<Output = String>,
214 {
215 let parsed = parser.parse_document(content, &self.locale);
216 let body = content.get(parsed.body_start..).unwrap_or(content);
217 let mut rendered = self.render_document_body::<F>(body, parsed, format);
218 let rendered_groups = self.render_document_bibliography_blocks::<F>(blocks, None, None);
220 for rendered_group in rendered_groups {
221 let section = render_document_bibliography_block_replacement(
222 rendered.placeholders.as_mut(),
223 format,
224 rendered_group.heading,
225 rendered_group.body,
226 );
227 rendered.content.push_str("\n\n");
228 rendered.content.push_str(§ion);
229 }
230 self.finalize_document_output::<P, F>(parser, format, rendered)
231 }
232
233 fn process_document_with_default_bibliography<P, F>(
235 &self,
236 body: &str,
237 parsed: ParsedDocument,
238 parser: &P,
239 format: DocumentFormat,
240 ) -> String
241 where
242 P: CitationParser,
243 F: crate::render::format::OutputFormat<Output = String>,
244 {
245 self.render_document_with_trailing_bibliography::<P, F, _>(
246 body,
247 parsed,
248 parser,
249 format,
250 |p: &super::super::Processor| {
251 p.render_document_bibliography::<F>(true, None, None)
252 .content
253 },
254 )
255 }
256
257 fn render_document_with_trailing_bibliography<P, F, B>(
259 &self,
260 body: &str,
261 parsed: ParsedDocument,
262 parser: &P,
263 format: DocumentFormat,
264 render_bibliography: B,
265 ) -> String
266 where
267 P: CitationParser,
268 F: crate::render::format::OutputFormat<Output = String>,
269 B: FnOnce(&Self) -> String,
270 {
271 let mut rendered = self.render_document_body::<F>(body, parsed, format);
272 let bibliography = render_bibliography(self);
273 append_document_bibliography(&mut rendered, format, bibliography);
274 self.finalize_document_output::<P, F>(parser, format, rendered)
275 }
276
277 fn render_document_body<F>(
284 &self,
285 content: &str,
286 parsed: ParsedDocument,
287 format: DocumentFormat,
288 ) -> RenderedDocumentBody
289 where
290 F: crate::render::format::OutputFormat<Output = String>,
291 {
292 if matches!(format, DocumentFormat::Html) {
293 let mut placeholders = HtmlPlaceholderRegistry::default();
294 let content = if self.is_note_style() {
295 self.process_note_document_html(content, parsed, &mut placeholders)
296 } else {
297 self.process_inline_document_html(content, parsed, &mut placeholders)
298 };
299 return RenderedDocumentBody {
300 content,
301 placeholders: Some(placeholders),
302 trailing: None,
303 };
304 }
305
306 if matches!(format, DocumentFormat::Typst | DocumentFormat::Latex) {
311 let mut placeholders = HtmlPlaceholderRegistry::default();
312 let content = if self.is_note_style() {
316 self.process_note_document::<F>(content, parsed)
317 } else {
318 self.process_inline_document_with_placeholders::<F>(
319 content,
320 parsed,
321 &mut placeholders,
322 )
323 };
324 return RenderedDocumentBody {
325 content,
326 placeholders: if self.is_note_style() {
327 None
328 } else {
329 Some(placeholders)
330 },
331 trailing: None,
332 };
333 }
334
335 let content = if self.is_note_style() {
336 self.process_note_document::<F>(content, parsed)
337 } else {
338 self.process_inline_document::<F>(content, parsed)
339 };
340
341 RenderedDocumentBody {
342 content,
343 placeholders: None,
344 trailing: None,
345 }
346 }
347
348 #[allow(
355 clippy::string_slice,
356 reason = "parser-guaranteed boundaries and indices"
357 )]
358 fn process_inline_document_with_placeholders<F>(
359 &self,
360 content: &str,
361 parsed: ParsedDocument,
362 placeholders: &mut HtmlPlaceholderRegistry,
363 ) -> String
364 where
365 F: crate::render::format::OutputFormat<Output = String>,
366 {
367 let mut result = String::new();
368 let mut last_idx = 0;
369 let normalized = self.normalize_integral_name_citations(&parsed);
370
371 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
372 debug_assert!(
373 parsed.end <= content.len(),
374 "citation offset {} exceeds body length {}; parser must emit \
375 body-relative offsets",
376 parsed.end,
377 content.len()
378 );
379 result.push_str(&content[last_idx..parsed.start]);
380 match self.process_citation_with_format::<F>(&citation) {
381 Ok(rendered) => result.push_str(&placeholders.push_inline(rendered)),
382 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
383 }
384 last_idx = parsed.end;
385 }
386
387 result.push_str(&content[last_idx..]);
388 result
389 }
390
391 #[allow(
393 clippy::string_slice,
394 reason = "parser-guaranteed boundaries and indices"
395 )]
396 fn process_inline_document<F>(&self, content: &str, parsed: ParsedDocument) -> String
397 where
398 F: crate::render::format::OutputFormat<Output = String>,
399 {
400 let mut result = String::new();
401 let mut last_idx = 0;
402 let normalized = self.normalize_integral_name_citations(&parsed);
403
404 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
405 debug_assert!(
406 parsed.end <= content.len(),
407 "citation offset {} exceeds body length {}; parser must emit \
408 body-relative offsets",
409 parsed.end,
410 content.len()
411 );
412 result.push_str(&content[last_idx..parsed.start]);
413 match self.process_citation_with_format::<F>(&citation) {
414 Ok(rendered) => result.push_str(&rendered),
415 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
416 }
417 last_idx = parsed.end;
418 }
419
420 result.push_str(&content[last_idx..]);
421 result
422 }
423
424 #[allow(
426 clippy::string_slice,
427 reason = "parser-guaranteed boundaries and indices"
428 )]
429 fn process_inline_document_html(
430 &self,
431 content: &str,
432 parsed: ParsedDocument,
433 placeholders: &mut HtmlPlaceholderRegistry,
434 ) -> String {
435 let mut result = String::new();
436 let mut last_idx = 0;
437 let normalized = self.normalize_integral_name_citations(&parsed);
438
439 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
440 debug_assert!(
441 parsed.end <= content.len(),
442 "citation offset {} exceeds body length {}; parser must emit \
443 body-relative offsets",
444 parsed.end,
445 content.len()
446 );
447 result.push_str(&content[last_idx..parsed.start]);
448 match self.process_citation_with_format::<crate::render::html::Html>(&citation) {
449 Ok(rendered) => result.push_str(&placeholders.push_inline(rendered)),
450 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
451 }
452 last_idx = parsed.end;
453 }
454
455 result.push_str(&content[last_idx..]);
456 result
457 }
458
459 fn replace_document_bibliography_blocks<F>(
461 &self,
462 rendered: &mut RenderedDocumentBody,
463 blocks: &[BibliographyBlock],
464 format: DocumentFormat,
465 ) where
466 F: crate::render::format::OutputFormat<Output = String>,
467 {
468 let groups: Vec<_> = blocks.iter().map(|b| b.group.clone()).collect();
469 let rendered_groups = self.render_document_bibliography_blocks::<F>(&groups, None, None);
470 for (index, rendered_group) in rendered_groups.into_iter().enumerate() {
471 let placeholder = bibliography_block_placeholder(index);
472 let replacement = render_document_bibliography_block_replacement(
473 rendered.placeholders.as_mut(),
474 format,
475 rendered_group.heading,
476 rendered_group.body,
477 );
478 rendered.content = rendered.content.replace(&placeholder, &replacement);
479 }
480 }
481
482 fn finalize_document_output<P, F>(
490 &self,
491 parser: &P,
492 format: DocumentFormat,
493 rendered: RenderedDocumentBody,
494 ) -> String
495 where
496 P: CitationParser,
497 F: crate::render::format::OutputFormat<Output = String>,
498 {
499 let mut result = if let Some(placeholders) = rendered.placeholders {
500 let fmt = F::default();
501 let converted = match format {
502 DocumentFormat::Html => parser.finalize_html_output(&rendered.content),
503 DocumentFormat::Typst | DocumentFormat::Latex => {
504 parser.render_body_markup(&rendered.content, &fmt)
505 }
506 _ => rendered.content,
507 };
508 placeholders.apply(converted)
509 } else {
510 let content = rewrite_document_markup_for_typst(rendered.content, format);
515 match format {
516 DocumentFormat::Html => parser.finalize_html_output(&content),
517 _ => content,
518 }
519 };
520 if let Some(tail) = rendered.trailing {
526 let trimmed = result.trim_end_matches('\n');
527 result = format!("{trimmed}{tail}");
528 }
529 result
530 }
531}