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;
16use crate::processor::run_state::{FinalizedRun, RunState};
17
18fn render_bibliography_section_heading(heading: &str, format: DocumentFormat) -> String {
26 match format {
27 DocumentFormat::Html => {
28 let escaped = heading
29 .replace('&', "&")
30 .replace('<', "<")
31 .replace('>', ">");
32 format!("<h2>{escaped}</h2>\n\n")
33 }
34 DocumentFormat::Latex => format!("\\subsection*{{{heading}}}\n\n"),
35 DocumentFormat::Typst => format!("== {heading}\n\n"),
36 _ => format!("## {heading}\n\n"),
37 }
38}
39
40impl Processor {
41 #[allow(
53 clippy::string_slice,
54 reason = "parser-guaranteed boundaries and indices"
55 )]
56 pub fn process_document<P, F>(
57 &self,
58 content: &str,
59 parser: &P,
60 format: DocumentFormat,
61 ) -> Result<String, ProcessorError>
62 where
63 P: CitationParser,
64 F: crate::render::format::OutputFormat<Output = String>,
65 {
66 let mut parsed = parser.parse_document(content, &self.locale);
67
68 if let Some(err) = &parsed.frontmatter_error {
69 return Err(ProcessorError::FrontmatterParse(err.clone()));
70 }
71
72 let effective_integral_override = parsed
74 .frontmatter_options
75 .as_ref()
76 .and_then(|o| o.integral_name_memory.as_ref())
77 .or(parsed.frontmatter_integral_name_memory.as_ref());
78 let owned_integral =
79 self.processor_with_document_integral_name_override(effective_integral_override);
80
81 let effective_org_override = parsed
83 .frontmatter_options
84 .as_ref()
85 .and_then(|o| o.org_abbreviation_memory.as_ref())
86 .or(parsed.frontmatter_org_abbreviation_memory.as_ref());
87 let owned_org = {
88 let base = owned_integral.as_ref().unwrap_or(self);
89 base.processor_with_document_org_abbreviation_override(effective_org_override)
90 };
91
92 let owned_bib = parsed
94 .frontmatter_options
95 .as_ref()
96 .filter(|o| o.bibliography.is_some() || o.multilingual.is_some())
97 .map(|options| {
98 let base = owned_org
99 .as_ref()
100 .or(owned_integral.as_ref())
101 .unwrap_or(self);
102 base.processor_with_bibliography_override(options)
103 });
104
105 let processor = owned_bib
106 .as_ref()
107 .or(owned_org.as_ref())
108 .or(owned_integral.as_ref())
109 .unwrap_or(self);
110 let run = processor.begin_run();
111 let body = &content[parsed.body_start..];
112 if let Some(groups) = parsed.frontmatter_groups.take() {
113 return Ok(processor.process_document_with_frontmatter_groups::<P, F>(
114 body, parsed, groups, parser, format, run,
115 ));
116 }
117
118 if !parsed.bibliography_blocks.is_empty() {
119 return Ok(processor.process_document_with_bibliography_blocks::<P, F>(
120 body,
121 std::mem::take(&mut parsed.bibliography_blocks),
122 parser,
123 format,
124 run,
125 ));
126 }
127
128 Ok(processor
129 .process_document_with_default_bibliography::<P, F>(body, parsed, parser, format, run))
130 }
131
132 #[allow(
142 clippy::too_many_arguments,
143 reason = "internal helper, all params load-bearing"
144 )]
145 fn process_document_with_frontmatter_groups<P, F>(
146 &self,
147 body: &str,
148 parsed: ParsedDocument,
149 groups: Vec<citum_schema::grouping::BibliographyGroup>,
150 parser: &P,
151 format: DocumentFormat,
152 run: RunState,
153 ) -> String
154 where
155 P: CitationParser,
156 F: crate::render::format::OutputFormat<Output = String>,
157 {
158 self.render_document_with_trailing_bibliography::<P, F, _>(
159 body,
160 parsed,
161 parser,
162 format,
163 run,
164 |processor, run| {
165 let rendered_blocks =
166 processor.render_document_bibliography_blocks::<F>(&groups, None, None, run);
167 let mut output = String::new();
168 for block in rendered_blocks {
169 if block.entries.is_empty() {
170 continue;
171 }
172 if !output.is_empty() {
173 output.push_str("\n\n");
174 }
175 if let Some(heading) = block.heading {
176 output.push_str(&render_bibliography_section_heading(&heading, format));
177 }
178 output.push_str(&block.body);
179 }
180 output
181 },
182 )
183 }
184
185 fn process_document_with_bibliography_blocks<P, F>(
187 &self,
188 body: &str,
189 blocks: Vec<BibliographyBlock>,
190 parser: &P,
191 format: DocumentFormat,
192 mut run: RunState,
193 ) -> String
194 where
195 P: CitationParser,
196 F: crate::render::format::OutputFormat<Output = String>,
197 {
198 let staged = stage_document_bibliography_blocks(body, &blocks);
199 let parsed_staged = parser.parse_document(&staged, &self.locale);
200 let mut rendered = self.render_document_body::<F>(&staged, parsed_staged, format, &mut run);
201 let run = run.finalize();
202 self.replace_document_bibliography_blocks::<F>(&mut rendered, &blocks, format, &run);
203 self.finalize_document_output::<P, F>(parser, format, rendered)
204 }
205
206 pub fn process_document_with_caller_blocks<P, F>(
216 &self,
217 content: &str,
218 blocks: &[citum_schema::grouping::BibliographyGroup],
219 parser: &P,
220 format: DocumentFormat,
221 ) -> String
222 where
223 P: CitationParser,
224 F: crate::render::format::OutputFormat<Output = String>,
225 {
226 let mut run = self.begin_run();
227 let parsed = parser.parse_document(content, &self.locale);
228 let body = content.get(parsed.body_start..).unwrap_or(content);
229 let mut rendered = self.render_document_body::<F>(body, parsed, format, &mut run);
230 let run = run.finalize();
231 let rendered_groups =
233 self.render_document_bibliography_blocks::<F>(blocks, None, None, &run);
234 for rendered_group in rendered_groups {
235 let section = render_document_bibliography_block_replacement(
236 rendered.placeholders.as_mut(),
237 format,
238 rendered_group.heading,
239 rendered_group.body,
240 );
241 rendered.content.push_str("\n\n");
242 rendered.content.push_str(§ion);
243 }
244 self.finalize_document_output::<P, F>(parser, format, rendered)
245 }
246
247 fn process_document_with_default_bibliography<P, F>(
249 &self,
250 body: &str,
251 parsed: ParsedDocument,
252 parser: &P,
253 format: DocumentFormat,
254 run: RunState,
255 ) -> String
256 where
257 P: CitationParser,
258 F: crate::render::format::OutputFormat<Output = String>,
259 {
260 self.render_document_with_trailing_bibliography::<P, F, _>(
261 body,
262 parsed,
263 parser,
264 format,
265 run,
266 |p: &super::super::Processor, run| {
267 p.render_document_bibliography::<F>(true, None, None, run)
268 .content
269 },
270 )
271 }
272
273 fn render_document_with_trailing_bibliography<P, F, B>(
281 &self,
282 body: &str,
283 parsed: ParsedDocument,
284 parser: &P,
285 format: DocumentFormat,
286 mut run: RunState,
287 render_bibliography: B,
288 ) -> String
289 where
290 P: CitationParser,
291 F: crate::render::format::OutputFormat<Output = String>,
292 B: FnOnce(&Self, &FinalizedRun) -> String,
293 {
294 let mut rendered = self.render_document_body::<F>(body, parsed, format, &mut run);
295 let run = run.finalize();
296 let bibliography = render_bibliography(self, &run);
297 append_document_bibliography(&mut rendered, format, bibliography);
298 self.finalize_document_output::<P, F>(parser, format, rendered)
299 }
300
301 fn render_document_body<F>(
308 &self,
309 content: &str,
310 parsed: ParsedDocument,
311 format: DocumentFormat,
312 run: &mut RunState,
313 ) -> RenderedDocumentBody
314 where
315 F: crate::render::format::OutputFormat<Output = String>,
316 {
317 if matches!(format, DocumentFormat::Html) {
318 let mut placeholders = HtmlPlaceholderRegistry::default();
319 let content = if self.is_note_style() {
320 self.process_note_document_html(content, parsed, &mut placeholders, run)
321 } else {
322 self.process_inline_document_html(content, parsed, &mut placeholders, run)
323 };
324 return RenderedDocumentBody {
325 content,
326 placeholders: Some(placeholders),
327 trailing: None,
328 };
329 }
330
331 if matches!(format, DocumentFormat::Typst | DocumentFormat::Latex) {
336 let mut placeholders = HtmlPlaceholderRegistry::default();
337 let content = if self.is_note_style() {
341 self.process_note_document::<F>(content, parsed, run)
342 } else {
343 self.process_inline_document_with_placeholders::<F>(
344 content,
345 parsed,
346 &mut placeholders,
347 run,
348 )
349 };
350 return RenderedDocumentBody {
351 content,
352 placeholders: if self.is_note_style() {
353 None
354 } else {
355 Some(placeholders)
356 },
357 trailing: None,
358 };
359 }
360
361 let content = if self.is_note_style() {
362 self.process_note_document::<F>(content, parsed, run)
363 } else {
364 self.process_inline_document::<F>(content, parsed, run)
365 };
366
367 RenderedDocumentBody {
368 content,
369 placeholders: None,
370 trailing: None,
371 }
372 }
373
374 #[allow(
381 clippy::string_slice,
382 reason = "parser-guaranteed boundaries and indices"
383 )]
384 fn process_inline_document_with_placeholders<F>(
385 &self,
386 content: &str,
387 parsed: ParsedDocument,
388 placeholders: &mut HtmlPlaceholderRegistry,
389 run: &mut RunState,
390 ) -> String
391 where
392 F: crate::render::format::OutputFormat<Output = String>,
393 {
394 let mut result = String::new();
395 let mut last_idx = 0;
396 let normalized = self.normalize_integral_name_citations(&parsed, run);
397
398 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
399 debug_assert!(
400 parsed.end <= content.len(),
401 "citation offset {} exceeds body length {}; parser must emit \
402 body-relative offsets",
403 parsed.end,
404 content.len()
405 );
406 result.push_str(&content[last_idx..parsed.start]);
407 match self.process_citation_with_format::<F>(&citation, run) {
408 Ok(rendered) => result.push_str(&placeholders.push_inline(rendered)),
409 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
410 }
411 last_idx = parsed.end;
412 }
413
414 result.push_str(&content[last_idx..]);
415 result
416 }
417
418 #[allow(
420 clippy::string_slice,
421 reason = "parser-guaranteed boundaries and indices"
422 )]
423 fn process_inline_document<F>(
424 &self,
425 content: &str,
426 parsed: ParsedDocument,
427 run: &mut RunState,
428 ) -> String
429 where
430 F: crate::render::format::OutputFormat<Output = String>,
431 {
432 let mut result = String::new();
433 let mut last_idx = 0;
434 let normalized = self.normalize_integral_name_citations(&parsed, run);
435
436 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
437 debug_assert!(
438 parsed.end <= content.len(),
439 "citation offset {} exceeds body length {}; parser must emit \
440 body-relative offsets",
441 parsed.end,
442 content.len()
443 );
444 result.push_str(&content[last_idx..parsed.start]);
445 match self.process_citation_with_format::<F>(&citation, run) {
446 Ok(rendered) => result.push_str(&rendered),
447 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
448 }
449 last_idx = parsed.end;
450 }
451
452 result.push_str(&content[last_idx..]);
453 result
454 }
455
456 #[allow(
458 clippy::string_slice,
459 reason = "parser-guaranteed boundaries and indices"
460 )]
461 fn process_inline_document_html(
462 &self,
463 content: &str,
464 parsed: ParsedDocument,
465 placeholders: &mut HtmlPlaceholderRegistry,
466 run: &mut RunState,
467 ) -> String {
468 let mut result = String::new();
469 let mut last_idx = 0;
470 let normalized = self.normalize_integral_name_citations(&parsed, run);
471
472 for (parsed, citation) in parsed.citations.iter().zip(normalized) {
473 debug_assert!(
474 parsed.end <= content.len(),
475 "citation offset {} exceeds body length {}; parser must emit \
476 body-relative offsets",
477 parsed.end,
478 content.len()
479 );
480 result.push_str(&content[last_idx..parsed.start]);
481 match self.process_citation_with_format::<crate::render::html::Html>(&citation, run) {
482 Ok(rendered) => result.push_str(&placeholders.push_inline(rendered)),
483 Err(_) => result.push_str(&content[parsed.start..parsed.end]),
484 }
485 last_idx = parsed.end;
486 }
487
488 result.push_str(&content[last_idx..]);
489 result
490 }
491
492 fn replace_document_bibliography_blocks<F>(
494 &self,
495 rendered: &mut RenderedDocumentBody,
496 blocks: &[BibliographyBlock],
497 format: DocumentFormat,
498 run: &FinalizedRun,
499 ) where
500 F: crate::render::format::OutputFormat<Output = String>,
501 {
502 let groups: Vec<_> = blocks.iter().map(|b| b.group.clone()).collect();
503 let rendered_groups =
504 self.render_document_bibliography_blocks::<F>(&groups, None, None, run);
505 for (index, rendered_group) in rendered_groups.into_iter().enumerate() {
506 let placeholder = bibliography_block_placeholder(index);
507 let replacement = render_document_bibliography_block_replacement(
508 rendered.placeholders.as_mut(),
509 format,
510 rendered_group.heading,
511 rendered_group.body,
512 );
513 rendered.content = rendered.content.replace(&placeholder, &replacement);
514 }
515 }
516
517 fn finalize_document_output<P, F>(
525 &self,
526 parser: &P,
527 format: DocumentFormat,
528 rendered: RenderedDocumentBody,
529 ) -> String
530 where
531 P: CitationParser,
532 F: crate::render::format::OutputFormat<Output = String>,
533 {
534 let mut result = if let Some(placeholders) = rendered.placeholders {
535 let fmt = F::default();
536 let converted = match format {
537 DocumentFormat::Html => parser.finalize_html_output(&rendered.content),
538 DocumentFormat::Typst | DocumentFormat::Latex => {
539 parser.render_body_markup(&rendered.content, &fmt)
540 }
541 _ => rendered.content,
542 };
543 placeholders.apply(converted)
544 } else {
545 let content = rewrite_document_markup_for_typst(rendered.content, format);
550 match format {
551 DocumentFormat::Html => parser.finalize_html_output(&content),
552 _ => content,
553 }
554 };
555 if let Some(tail) = rendered.trailing {
561 let trimmed = result.trim_end_matches('\n');
562 result = format!("{trimmed}{tail}");
563 }
564 result
565 }
566}