1use std::collections::{HashMap, HashSet};
8
9use crate::nodes::*;
10use crate::parser::norm_name;
11
12pub fn apply_transforms(doc: &mut Document) {
14 dedupe_section_ids(doc);
15 let mut cx = Collect::default();
16 cx.collect_blocks(&doc.children);
17 if expand_target_note_markers(doc) {
18 cx = Collect::default();
20 cx.collect_blocks(&doc.children);
21 }
22 cx.assign_footnote_numbers(doc);
23 let loose = resolve_inlines(doc, &cx);
24 rewrite_target_note_refs(doc, &cx);
25 expand_contents(doc);
26 if !loose.is_empty() {
27 doc.children.push(Block::Section(Section {
31 title: vec![Inline::Text("Docutils System Messages".into())],
32 level: 1,
33 ids: Vec::new(),
34 children: loose,
35 classes: vec!["system-messages".into()],
36 }));
37 }
38}
39
40fn expand_target_note_markers(doc: &mut Document) -> bool {
47 let mut seen_uris: std::collections::HashSet<String> = Default::default();
50 let mut notes: Vec<(String, String)> = Vec::new();
51 fn walk(blocks: &[Block], notes: &mut Vec<(String, String)>) {
52 for b in blocks {
53 match b {
54 Block::HyperlinkTarget(t)
55 if !t.anonymous && !t.uri.is_empty() && !is_indirect_uri(&t.uri) =>
56 {
57 notes.push((t.name.clone(), t.uri.clone()));
58 }
59 Block::Section(s) => walk(&s.children, notes),
60 Block::Topic(t) => walk(&t.children, notes),
61 Block::Container(c) => walk(&c.children, notes),
62 _ => {}
63 }
64 }
65 }
66 walk(&doc.children, &mut notes);
67 let mut expanded = false;
68 let mut footnotes: Vec<Block> = Vec::new();
69 for (name, uri) in notes {
70 if seen_uris.insert(uri.clone()) {
71 footnotes.push(Block::Footnote(Footnote {
72 name: format!("target-note-{name}"),
73 auto_numbered: true,
74 symbol: false,
75 number: None,
76 children: vec![Block::Paragraph(vec![Inline::Reference(
77 RefTarget::External(uri.clone()),
78 vec![Inline::Text(uri)],
79 )])],
80 }));
81 }
82 }
83 fn splice(blocks: &mut Vec<Block>, footnotes: &[Block], expanded: &mut bool) {
85 let mut out: Vec<Block> = Vec::with_capacity(blocks.len());
86 for b in blocks.drain(..) {
87 match b {
88 Block::Topic(t) if t.classes.iter().any(|c| c == "__target_notes__") => {
89 *expanded = true;
90 out.extend(footnotes.iter().cloned());
91 }
92 Block::Section(mut s) => {
93 splice(&mut s.children, footnotes, expanded);
94 out.push(Block::Section(s));
95 }
96 Block::Topic(mut t) => {
97 splice(&mut t.children, footnotes, expanded);
98 out.push(Block::Topic(t));
99 }
100 Block::Container(mut c) => {
101 splice(&mut c.children, footnotes, expanded);
102 out.push(Block::Container(c));
103 }
104 other => out.push(other),
105 }
106 }
107 *blocks = out;
108 }
109 splice(&mut doc.children, &footnotes, &mut expanded);
110 expanded
111}
112
113fn rewrite_target_note_refs(doc: &mut Document, cx: &Collect) {
116 let mut fn_of_uri: std::collections::HashMap<String, String> = Default::default();
120 fn collect(blocks: &[Block], fn_of_uri: &mut std::collections::HashMap<String, String>) {
121 for b in blocks {
122 match b {
123 Block::Footnote(f) => {
124 if f.name.starts_with("target-note-")
125 && let Some(Block::Paragraph(inlines)) = f.children.first()
126 && let Some(Inline::Reference(RefTarget::External(uri), _)) =
127 inlines.first()
128 {
129 fn_of_uri.insert(uri.clone(), f.name.clone());
130 }
131 collect(&f.children, fn_of_uri);
132 }
133 Block::Section(s) => collect(&s.children, fn_of_uri),
134 Block::Topic(t) => collect(&t.children, fn_of_uri),
135 Block::Container(c) => collect(&c.children, fn_of_uri),
136 _ => {}
137 }
138 }
139 }
140 collect(&doc.children, &mut fn_of_uri);
141 fn walk_inlines(
142 inlines: &mut Vec<Inline>,
143 fn_of_uri: &std::collections::HashMap<String, String>,
144 cx: &Collect,
145 ) {
146 let mut out: Vec<Inline> = Vec::with_capacity(inlines.len() + 2);
149 for node in inlines.drain(..) {
150 match node {
151 Inline::Reference(RefTarget::External(uri), text) => {
152 let footnote = fn_of_uri.get(&uri).map(|fn_name| {
153 Inline::FootnoteRef(FootnoteRef {
154 label: display_number(fn_name, cx),
155 auto: true,
156 symbol: false,
157 raw: String::new(),
158 })
159 });
160 out.push(Inline::Reference(RefTarget::External(uri), text));
161 if footnote.is_some() {
162 out.push(Inline::Text(" ".into()));
163 }
164 if let Some(f) = footnote {
165 out.push(f);
166 }
167 }
168 other => out.push(other),
169 }
170 }
171 *inlines = out;
172 for node in inlines.iter_mut() {
174 match node {
175 Inline::Emphasis(c) | Inline::Strong(c) | Inline::Cite(c) => {
176 walk_inlines(c, fn_of_uri, cx);
177 }
178 _ => {}
179 }
180 }
181 }
182 fn walk(
183 blocks: &mut [Block],
184 fn_of_uri: &std::collections::HashMap<String, String>,
185 cx: &Collect,
186 ) {
187 for b in blocks.iter_mut() {
188 match b {
189 Block::Paragraph(inlines) => walk_inlines(inlines, fn_of_uri, cx),
190 Block::Section(s) => walk(&mut s.children, fn_of_uri, cx),
191 Block::Topic(t) => walk(&mut t.children, fn_of_uri, cx),
192 Block::Container(c) => walk(&mut c.children, fn_of_uri, cx),
193 Block::BulletList(l) => {
194 for item in &mut l.items {
195 walk(item, fn_of_uri, cx);
196 }
197 }
198 Block::EnumList(l) => {
199 for item in &mut l.items {
200 walk(item, fn_of_uri, cx);
201 }
202 }
203 Block::BlockQuote(q) => walk(&mut q.children, fn_of_uri, cx),
204 Block::Admonition(a) => walk(&mut a.children, fn_of_uri, cx),
205 _ => {}
206 }
207 }
208 }
209 walk(&mut doc.children, &fn_of_uri, cx);
210}
211
212#[derive(Default)]
213struct Collect {
214 targets: HashMap<String, String>,
216 duplicate_targets: HashSet<String>,
218 anonymous_targets: Vec<String>,
219 footnotes: Vec<(String, bool, bool)>,
221 citations: HashSet<String>,
222 substitutions: HashMap<String, Vec<Block>>,
223 footnote_numbers: HashMap<String, u32>,
225 auto_footnotes: Vec<String>,
227 symbol_footnotes: Vec<String>,
228}
229
230impl Collect {
231 fn collect_blocks(&mut self, blocks: &[Block]) {
232 for b in blocks {
233 match b {
234 Block::HyperlinkTarget(t) => {
235 if t.anonymous {
236 self.anonymous_targets.push(t.uri.clone());
237 } else if !t.name.is_empty() {
238 if self.targets.contains_key(&t.name) {
239 self.duplicate_targets.insert(t.name.clone());
240 } else {
241 self.targets.insert(t.name.clone(), t.uri.clone());
242 }
243 }
244 }
245 Block::Footnote(f) => {
246 self.footnotes
247 .push((f.name.clone(), f.auto_numbered, f.symbol));
248 if f.auto_numbered && !f.symbol {
249 self.auto_footnotes.push(f.name.clone());
250 }
251 if f.symbol {
252 self.symbol_footnotes.push(f.name.clone());
253 }
254 }
255 Block::Citation(c) => {
256 self.citations.insert(c.name.clone());
257 }
258 Block::SubstitutionDef(s) => {
259 self.substitutions
260 .entry(s.name.clone())
261 .or_insert_with(|| s.replacement.clone());
262 }
263 Block::Section(s) => self.collect_blocks(&s.children),
264 Block::Topic(t) => self.collect_blocks(&t.children),
265 Block::Container(c) => self.collect_blocks(&c.children),
266 Block::BulletList(l) => {
267 for item in &l.items {
268 self.collect_blocks(item);
269 }
270 }
271 Block::EnumList(l) => {
272 for item in &l.items {
273 self.collect_blocks(item);
274 }
275 }
276 Block::DefinitionList(items) => {
277 for it in items {
278 self.collect_blocks(&it.definition);
279 }
280 }
281 Block::FieldList(fields) => {
282 for f in fields {
283 self.collect_blocks(&f.body);
284 }
285 }
286 Block::OptionList(groups) => {
287 for g in groups {
288 self.collect_blocks(&g.body);
289 }
290 }
291 Block::BlockQuote(q) => self.collect_blocks(&q.children),
292 Block::Admonition(a) => self.collect_blocks(&a.children),
293 Block::Figure(f) => self.collect_blocks(&f.caption),
294 Block::Table(t) => {
295 for row in t.head.iter().chain(t.body.iter()) {
296 for cell in row {
297 self.collect_blocks(&cell.blocks);
298 }
299 }
300 }
301 _ => {}
302 }
303 }
304 }
305
306 fn assign_footnote_numbers(&mut self, doc: &mut Document) {
307 let mut next: u32 = 0;
308 for (name, auto, symbol) in &self.footnotes {
310 if !auto
311 && !symbol
312 && let Ok(n) = name.parse::<u32>()
313 {
314 next = next.max(n);
315 }
316 }
317 let mut numbers: HashMap<String, u32> = HashMap::new();
318 for (name, auto, symbol) in &self.footnotes {
319 if *auto && !*symbol && !numbers.contains_key(name) {
320 next += 1;
321 numbers.insert(name.clone(), next);
322 }
323 }
324 self.footnote_numbers = numbers;
325 fn walk(blocks: &mut [Block], numbers: &HashMap<String, u32>) {
327 for b in blocks {
328 match b {
329 Block::Footnote(f) => {
330 f.number = numbers.get(&f.name).copied();
331 }
332 Block::Section(s) => walk(&mut s.children, numbers),
333 Block::Topic(t) => walk(&mut t.children, numbers),
334 Block::Container(c) => walk(&mut c.children, numbers),
335 Block::BulletList(l) => {
336 for item in &mut l.items {
337 walk(item, numbers);
338 }
339 }
340 Block::EnumList(l) => {
341 for item in &mut l.items {
342 walk(item, numbers);
343 }
344 }
345 Block::DefinitionList(items) => {
346 for it in items.iter_mut() {
347 walk(&mut it.definition, numbers);
348 }
349 }
350 Block::FieldList(fields) => {
351 for f in fields.iter_mut() {
352 walk(&mut f.body, numbers);
353 }
354 }
355 Block::OptionList(groups) => {
356 for g in groups.iter_mut() {
357 walk(&mut g.body, numbers);
358 }
359 }
360 Block::BlockQuote(q) => walk(&mut q.children, numbers),
361 Block::Admonition(a) => walk(&mut a.children, numbers),
362 Block::Figure(f) => walk(&mut f.caption, numbers),
363 Block::Table(t) => {
364 for row in t.head.iter_mut().chain(t.body.iter_mut()) {
365 for cell in row {
366 walk(&mut cell.blocks, numbers);
367 }
368 }
369 }
370 _ => {}
371 }
372 }
373 }
374 walk(&mut doc.children, &self.footnote_numbers);
375 }
376}
377
378fn resolve_target(name: &str, cx: &Collect) -> Option<RefTarget> {
380 let mut key = norm_name(name);
381 let mut seen = HashSet::new();
382 for _ in 0..10 {
383 if !seen.insert(key.clone()) {
384 return None; }
386 let uri = cx.targets.get(&key)?;
387 if is_indirect_uri(uri) {
388 key = norm_name(uri.trim_end_matches('_'));
389 continue;
390 }
391 if uri.is_empty() {
392 return Some(RefTarget::Internal(slugify(&key)));
393 }
394 return Some(RefTarget::External(uri.clone()));
395 }
396 None
397}
398
399fn is_indirect_uri(uri: &str) -> bool {
401 uri.ends_with('_')
402 && !uri.ends_with("__")
403 && !uri.contains("://")
404 && !uri.starts_with("mailto:")
405 && !uri.starts_with('#')
406}
407
408fn indirect_final(name: &str, cx: &Collect) -> Option<String> {
412 let mut key = norm_name(name);
413 let first = cx.targets.get(&key)?;
414 if !is_indirect_uri(first) {
415 return None;
416 }
417 let mut seen = HashSet::new();
418 for _ in 0..10 {
419 if !seen.insert(key.clone()) {
420 return None;
421 }
422 let uri = cx.targets.get(&key)?;
423 if !is_indirect_uri(uri) {
424 return None; }
426 key = norm_name(uri.trim_end_matches('_'));
427 if !cx.targets.contains_key(&key) {
428 return Some(key);
429 }
430 }
431 None
432}
433
434fn display_number(name: &str, cx: &Collect) -> String {
437 cx.footnote_numbers
438 .get(name)
439 .map(|n| n.to_string())
440 .unwrap_or_else(|| name.to_string())
441}
442
443struct Resolver<'a> {
444 cx: &'a Collect,
445 anon_targets: Vec<String>,
446 anon_footnotes: Vec<String>,
447 anon_symbols: Vec<String>,
448 anon_refs: usize,
449 anon_link: Option<usize>,
450 next_id: usize,
451 messages: Vec<SystemMessage>,
452 loose: Vec<Block>,
455}
456
457fn resolve_inlines(doc: &mut Document, cx: &Collect) -> Vec<Block> {
458 let mut r = Resolver {
459 cx,
460 anon_targets: cx.anonymous_targets.clone(),
461 anon_footnotes: cx.auto_footnotes.clone(),
462 anon_symbols: cx.symbol_footnotes.clone(),
463 anon_refs: 0,
464 anon_link: None,
465 next_id: doc.next_id,
466 messages: Vec::new(),
467 loose: Vec::new(),
468 };
469 r.walk_blocks(&mut doc.children);
470 if r.anon_refs > cx.anonymous_targets.len() {
472 let text = format!(
473 "Anonymous hyperlink mismatch: {} references but {} targets.",
474 r.anon_refs,
475 cx.anonymous_targets.len()
476 );
477 r.messages.push(SystemMessage {
478 level: 3,
479 text: text.clone(),
480 line: None,
481 id: r.anon_link,
482 });
483 r.loose.push(Block::SystemMessage(SystemMessage {
484 level: 3,
485 text,
486 line: None,
487 id: r.anon_link,
488 }));
489 }
490 doc.next_id = r.next_id;
491 doc.messages.append(&mut r.messages);
492 std::mem::take(&mut r.loose)
493}
494
495impl Resolver<'_> {
496 fn walk_blocks(&mut self, blocks: &mut [Block]) {
497 for b in blocks.iter_mut() {
500 match b {
501 Block::Paragraph(inlines) => self.walk_inlines(inlines),
502 Block::Section(s) => {
503 self.walk_inlines(&mut s.title);
504 self.walk_blocks(&mut s.children);
505 }
506 Block::Rubric(inlines) => self.walk_inlines(inlines),
507 Block::Topic(t) => {
508 self.walk_inlines(&mut t.title);
509 self.walk_blocks(&mut t.children);
510 }
511 Block::Container(c) => self.walk_blocks(&mut c.children),
512 Block::BulletList(l) => {
513 for item in &mut l.items {
514 self.walk_blocks(item);
515 }
516 }
517 Block::EnumList(l) => {
518 for item in &mut l.items {
519 self.walk_blocks(item);
520 }
521 }
522 Block::DefinitionList(items) => {
523 for it in items.iter_mut() {
524 self.walk_inlines(&mut it.term);
525 self.walk_blocks(&mut it.definition);
526 }
527 }
528 Block::FieldList(fields) => {
529 for f in fields.iter_mut() {
530 self.walk_inlines(&mut f.name);
531 self.walk_blocks(&mut f.body);
532 }
533 }
534 Block::OptionList(groups) => {
535 for g in groups.iter_mut() {
536 self.walk_blocks(&mut g.body);
537 }
538 }
539 Block::BlockQuote(q) => {
540 self.walk_blocks(&mut q.children);
541 if let Some(a) = &mut q.attribution {
542 self.walk_inlines(a);
543 }
544 }
545 Block::LiteralBlock(_) => {}
546 Block::LineBlock(rows) => {
547 let mut stack: Vec<&mut [LineRow]> = vec![rows];
548 while let Some(rs) = stack.pop() {
549 for r in rs.iter_mut() {
550 match r {
551 LineRow::Line(l) => self.walk_inlines(l),
552 LineRow::Nested(n) => stack.push(n),
553 }
554 }
555 }
556 }
557 Block::ParsedLiteral(rows) => {
558 for row in rows {
559 self.walk_inlines(row);
560 }
561 }
562 Block::Table(t) => {
563 for row in t.head.iter_mut().chain(t.body.iter_mut()) {
564 for cell in row {
565 self.walk_blocks(&mut cell.blocks);
566 }
567 }
568 }
569 Block::Admonition(a) => {
570 if let Some(t) = &mut a.title {
571 self.walk_inlines(t);
572 }
573 self.walk_blocks(&mut a.children);
574 }
575 Block::Footnote(f) => self.walk_blocks(&mut f.children),
576 Block::Citation(c) => self.walk_blocks(&mut c.children),
577 Block::SubstitutionDef(s) => {
578 self.walk_blocks(&mut s.replacement);
579 }
580 Block::Figure(f) => self.walk_blocks(&mut f.caption),
581 Block::HyperlinkTarget(_)
582 | Block::Comment(_)
583 | Block::Transition
584 | Block::SystemMessage(_)
585 | Block::Raw(_)
586 | Block::Image(_)
587 | Block::PendingContents { .. } => {}
588 }
589 }
590 }
591 fn fail(&mut self, level: u8, text: String, raw: String) -> Inline {
595 let n = self.next_id;
596 self.next_id += 1;
597 let message = SystemMessage {
598 level,
599 text,
600 line: None,
601 id: Some(n),
602 };
603 self.messages.push(message.clone());
604 self.loose.push(Block::SystemMessage(SystemMessage {
605 level,
606 text: message.text.clone(),
607 line: None,
608 id: Some(n),
609 }));
610 Inline::Problematic(Problematic {
611 text: raw,
612 id: Some(n),
613 })
614 }
615
616 fn walk_inlines(&mut self, inlines: &mut Vec<Inline>) {
617 let mut out: Vec<Inline> = Vec::with_capacity(inlines.len());
619 for node in inlines.drain(..) {
620 match node {
621 Inline::PendingRef(p) => {
622 if p.anonymous {
623 self.anon_refs += 1;
624 match self.anon_targets.first().cloned() {
625 Some(uri) => {
626 self.anon_targets.remove(0);
627 out.push(Inline::Reference(RefTarget::External(uri), p.text));
628 }
629 None => {
630 let n = self.next_id;
634 self.next_id += 1;
635 if self.anon_link.is_none() {
636 self.anon_link = Some(n);
637 }
638 out.push(Inline::Problematic(Problematic {
639 text: p.raw,
640 id: Some(n),
641 }));
642 }
643 }
644 } else {
645 let key = norm_name(&p.target);
646 if self.cx.duplicate_targets.contains(&key) {
647 out.push(self.fail(
648 3,
649 format!(
650 "Duplicate target name, cannot be used as a unique reference: \"{key}\"."
651 ),
652 p.raw,
653 ));
654 } else {
655 match resolve_target(&p.target, self.cx) {
656 Some(t) => out.push(Inline::Reference(t, p.text)),
657 None => {
658 if let Some(final_name) = indirect_final(&p.target, self.cx) {
660 out.push(self.fail(
661 3,
662 format!(
663 "Indirect hyperlink target \"{key}\" (id=\"{key}\") refers to target \"{final_name}\", which does not exist."
664 ),
665 p.raw,
666 ));
667 } else {
668 out.push(self.fail(
669 3,
670 format!(
671 "Unknown target name: \"{}\".",
672 p.target.to_lowercase()
673 ),
674 p.raw,
675 ));
676 }
677 }
678 }
679 }
680 }
681 }
682 Inline::FootnoteRef(f) => {
683 if f.symbol {
684 match self.anon_symbols.first().cloned() {
685 Some(_) => {
686 self.anon_symbols.remove(0);
687 out.push(Inline::FootnoteRef(f));
691 }
692 None => {
693 out.push(self.fail(
694 3,
695 "too many symbol footnote references".into(),
696 f.raw,
697 ));
698 }
699 }
700 } else if f.auto && f.label == "#" {
701 match self.anon_footnotes.first().cloned() {
702 Some(name) => {
703 self.anon_footnotes.remove(0);
704 out.push(Inline::FootnoteRef(FootnoteRef {
705 label: display_number(&name, self.cx),
706 auto: true,
707 symbol: false,
708 raw: f.raw,
709 }));
710 }
711 None => {
712 out.push(self.fail(
713 3,
714 "too many autonumbered footnote references".into(),
715 f.raw,
716 ));
717 }
718 }
719 } else if self
720 .cx
721 .footnotes
722 .iter()
723 .any(|(n, _, _)| n == &f.label.to_lowercase())
724 {
725 let key = f.label.to_lowercase();
727 out.push(Inline::FootnoteRef(FootnoteRef {
728 label: display_number(&key, self.cx),
729 auto: f.auto,
730 symbol: false,
731 raw: f.raw,
732 }));
733 } else {
734 out.push(self.fail(
735 3,
736 format!("Unknown footnote: \"{}\".", f.label),
737 f.raw,
738 ));
739 }
740 }
741 Inline::CitationRef(c) => {
742 if self.cx.citations.contains(&c.label.to_lowercase()) {
743 out.push(Inline::CitationRef(c));
744 } else {
745 out.push(self.fail(
746 3,
747 format!("Unknown citation: \"{}\".", c.label),
748 c.raw,
749 ));
750 }
751 }
752 Inline::SubstitutionRef(s) => {
753 match self.cx.substitutions.get(&s.name.to_lowercase()) {
754 Some(repl) => out.extend(substitution_inlines(repl, &mut self.messages)),
755 None => {
756 out.push(self.fail(
757 3,
758 format!("Undefined substitution referenced: \"{}\".", s.name),
759 s.raw,
760 ));
761 }
762 }
763 }
764 Inline::Emphasis(inner) => {
765 let mut inner = inner;
766 self.walk_inlines(&mut inner);
767 out.push(Inline::Emphasis(inner));
768 }
769 Inline::Strong(inner) => {
770 let mut inner = inner;
771 self.walk_inlines(&mut inner);
772 out.push(Inline::Strong(inner));
773 }
774 Inline::Cite(inner) => {
775 let mut inner = inner;
776 self.walk_inlines(&mut inner);
777 out.push(Inline::Cite(inner));
778 }
779 Inline::Reference(t, inner) => {
780 let mut inner = inner;
781 self.walk_inlines(&mut inner);
782 out.push(Inline::Reference(t, inner));
783 }
784 other => out.push(other),
785 }
786 }
787 *inlines = out;
788 }
789}
790
791fn substitution_inlines(repl: &[Block], messages: &mut Vec<SystemMessage>) -> Vec<Inline> {
795 let mut out = Vec::new();
796 for b in repl {
797 match b {
798 Block::Paragraph(inlines) => out.extend(inlines.clone()),
799 Block::Image(img) => out.push(Inline::Image {
800 uri: img.uri.clone(),
801 alt: img.alt.clone(),
802 }),
803 _ => {
804 messages.push(SystemMessage {
805 level: 3,
806 text: "substitution replacement must be a paragraph or image".into(),
807 line: None,
808 id: None,
809 });
810 }
811 }
812 }
813 out
814}
815
816fn dedupe_section_ids(doc: &mut Document) {
818 let mut seen: HashSet<String> = HashSet::new();
819 fn walk(blocks: &mut [Block], seen: &mut HashSet<String>) {
820 for b in blocks {
821 match b {
822 Block::Section(s) => {
823 let base = s.ids.first().cloned().unwrap_or_default();
824 let mut id = base.clone();
825 let mut n = 1;
826 while !seen.insert(id.clone()) {
827 id = format!("{base}-{n}");
828 n += 1;
829 }
830 s.ids = vec![id];
831 walk(&mut s.children, seen);
832 }
833 Block::Topic(t) => walk(&mut t.children, seen),
834 Block::Container(c) => walk(&mut c.children, seen),
835 _ => {}
836 }
837 }
838 }
839 walk(&mut doc.children, &mut seen);
840}
841
842fn expand_contents(doc: &mut Document) {
844 let mut entries: Vec<(u8, String, Vec<Inline>)> = Vec::new();
846 fn walk(blocks: &[Block], entries: &mut Vec<(u8, String, Vec<Inline>)>) {
847 for b in blocks {
848 if let Block::Section(s) = b {
849 entries.push((
850 s.level,
851 s.ids.first().cloned().unwrap_or_default(),
852 s.title.clone(),
853 ));
854 walk(&s.children, entries);
855 }
856 }
857 }
858 walk(&doc.children, &mut entries);
859 if entries.is_empty() {
860 }
862 fn build(blocks: &mut [Block], entries: &[(u8, String, Vec<Inline>)], depth: Option<u32>) {
863 for b in blocks.iter_mut() {
864 match b {
865 Block::PendingContents { depth: d, title: t } => {
866 let start = entries.iter().map(|e| e.0).min().unwrap_or(1);
867 let max_depth = d
868 .or(depth)
869 .map(|dd| start as u32 + dd - 1)
870 .unwrap_or(u32::MAX);
871 let items = contents_list(entries, start, max_depth);
872 let title = t
875 .clone()
876 .or_else(|| {
877 if items.is_empty() {
878 None
879 } else {
880 Some(vec![Inline::Text("Contents".into())])
881 }
882 })
883 .unwrap_or_default();
884 *b = Block::Topic(Topic {
885 title,
886 children: if items.is_empty() {
887 vec![]
888 } else {
889 vec![Block::BulletList(BulletList { bullet: '-', items })]
890 },
891 classes: vec!["contents".into(), "topic".into()],
892 });
893 }
894 Block::Section(s) => build(&mut s.children, entries, depth),
895 Block::Topic(t) => build(&mut t.children, entries, depth),
896 Block::Container(c) => build(&mut c.children, entries, depth),
897 _ => {}
898 }
899 }
900 }
901 build(&mut doc.children, &entries, None);
902}
903
904fn contents_list(
906 entries: &[(u8, String, Vec<Inline>)],
907 level: u8,
908 max_depth: u32,
909) -> Vec<Vec<Block>> {
910 let mut items: Vec<Vec<Block>> = Vec::new();
911 let mut i = 0;
912 while i < entries.len() {
913 let (lv, id, title) = &entries[i];
914 if *lv < level {
915 break;
916 }
917 if *lv > level {
918 if let Some(last) = items.last_mut() {
921 let sub = contents_list(&entries[i..], *lv, max_depth);
922 let skip = contents_span(&entries[i..], *lv);
923 last.push(Block::BulletList(BulletList {
924 bullet: '-',
925 items: sub,
926 }));
927 i += skip;
928 continue;
929 }
930 i += 1;
931 continue;
932 }
933 if (*lv as u32) > max_depth {
934 i += 1 + contents_span(&entries[i + 1..], (*lv) + 1);
935 continue;
936 }
937 let mut blocks = vec![Block::Paragraph(vec![Inline::Reference(
938 RefTarget::Internal(id.clone()),
939 title.clone(),
940 )])];
941 let span = contents_span(&entries[i + 1..], level + 1);
942 if span > 0 {
943 let sub = contents_list(&entries[i + 1..i + 1 + span], level + 1, max_depth);
944 if !sub.is_empty() {
945 blocks.push(Block::BulletList(BulletList {
946 bullet: '-',
947 items: sub,
948 }));
949 }
950 }
951 items.push(blocks);
952 i += 1 + span;
953 }
954 items
955}
956
957fn contents_span(entries: &[(u8, String, Vec<Inline>)], level: u8) -> usize {
959 let mut n = 0;
960 for (lv, _, _) in entries {
961 if *lv < level {
962 break;
963 }
964 n += 1;
965 }
966 n
967}