1use std::collections::BTreeMap;
58use std::ops::Range;
59
60use serde::{Deserialize, Serialize};
61
62mod anchor;
63
64pub use anchor::{Anchors, LinkTarget};
65
66#[derive(
71 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
72)]
73pub struct NodeId(u32);
74
75impl NodeId {
76 pub const UNASSIGNED: NodeId = NodeId(0);
78
79 pub fn get(self) -> u32 {
81 self.0
82 }
83
84 pub fn new(id: u32) -> NodeId {
89 NodeId(id)
90 }
91
92 pub(crate) fn shifted(self, step: i64) -> NodeId {
97 if self == NodeId::UNASSIGNED {
98 return self;
99 }
100 if let Some((element, which)) = self.pseudo_element() {
101 return element.shifted(step).pseudo(which);
102 }
103 NodeId((self.0 as i64 + step).max(0) as u32)
104 }
105
106 pub(crate) fn pseudo(self, which: PseudoElement) -> NodeId {
109 let kind = match which {
110 PseudoElement::Before => 0,
111 PseudoElement::After => 1,
112 PseudoElement::FirstLetter => 2,
113 PseudoElement::FirstLine => 3,
114 };
115 NodeId(PSEUDO | (self.0 << 2) | kind)
116 }
117
118 pub fn pseudo_element(self) -> Option<(NodeId, PseudoElement)> {
121 if self.0 & PSEUDO == 0 {
122 return None;
123 }
124 let which = match self.0 & 3 {
125 0 => PseudoElement::Before,
126 1 => PseudoElement::After,
127 2 => PseudoElement::FirstLetter,
128 _ => PseudoElement::FirstLine,
129 };
130 Some((NodeId((self.0 & !PSEUDO) >> 2), which))
131 }
132
133 pub fn element(self) -> NodeId {
136 self.pseudo_element().map_or(self, |(element, _)| element)
137 }
138}
139
140const PSEUDO: u32 = 1 << 31;
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147pub enum PseudoElement {
148 Before,
151 After,
154 FirstLetter,
156 FirstLine,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
167pub struct SourceRange {
168 pub node: NodeId,
170 pub range: Range<u32>,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub struct SourceSpan {
183 pub start: u32,
185 pub end: u32,
187}
188
189impl SourceSpan {
190 pub fn covers(self, byte: u32) -> bool {
194 (self.start..self.end).contains(&byte)
195 }
196
197 fn width(self) -> u32 {
199 self.end.saturating_sub(self.start)
200 }
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
210pub struct Attributes {
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub id: Option<String>,
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub classes: Vec<String>,
218}
219
220impl Attributes {
221 pub fn is_empty(&self) -> bool {
224 self.id.is_none() && self.classes.is_empty()
225 }
226}
227
228#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
231pub struct Names {
232 pub classes: Vec<String>,
234 pub ids: Vec<String>,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub struct SourcePos {
244 pub line: u32,
246 pub column: u32,
248}
249
250pub fn origin(source: Option<&str>, position: Option<SourcePos>) -> String {
254 match (source, position) {
255 (Some(file), Some(pos)) => format!("{file}:{}:{}", pos.line, pos.column),
256 (Some(file), None) => file.to_string(),
257 (None, Some(pos)) => format!("{}:{}", pos.line, pos.column),
258 (None, None) => String::new(),
259 }
260}
261
262#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
264pub struct Metadata {
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub title: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub author: Option<String>,
271 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
279 pub extra: BTreeMap<String, String>,
280}
281
282impl Metadata {
283 pub fn language(&self) -> Option<&str> {
287 self.extra
288 .get("language")
289 .map(|tag| tag.trim())
290 .filter(|tag| !tag.is_empty())
291 }
292}
293
294#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
296pub struct Book {
297 pub metadata: Metadata,
299 pub sections: Vec<Section>,
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306pub struct Section {
307 #[serde(skip)]
309 pub id: NodeId,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub source: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
316 pub title: Option<String>,
317 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
321 pub attributes: Attributes,
322 pub blocks: Vec<Block>,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub position: Option<SourcePos>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub span: Option<SourceSpan>,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334#[serde(tag = "type", rename_all = "snake_case")]
335pub enum Block {
336 Heading {
338 #[serde(skip)]
340 id: NodeId,
341 level: HeadingLevel,
343 inlines: Vec<Inline>,
345 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
347 attributes: Attributes,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 position: Option<SourcePos>,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 span: Option<SourceSpan>,
354 },
355 Paragraph {
357 #[serde(skip)]
359 id: NodeId,
360 inlines: Vec<Inline>,
362 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
364 attributes: Attributes,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 position: Option<SourcePos>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 span: Option<SourceSpan>,
371 },
372 Blockquote {
375 #[serde(skip)]
377 id: NodeId,
378 blocks: Vec<Block>,
380 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
382 attributes: Attributes,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 position: Option<SourcePos>,
386 #[serde(skip_serializing_if = "Option::is_none")]
388 span: Option<SourceSpan>,
389 },
390 CodeBlock {
396 #[serde(skip)]
398 id: NodeId,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
403 info: Option<String>,
404 text: String,
406 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
408 attributes: Attributes,
409 #[serde(skip_serializing_if = "Option::is_none")]
411 position: Option<SourcePos>,
412 #[serde(skip_serializing_if = "Option::is_none")]
414 span: Option<SourceSpan>,
415 },
416 ThematicBreak {
418 #[serde(skip)]
420 id: NodeId,
421 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
423 attributes: Attributes,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 position: Option<SourcePos>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 span: Option<SourceSpan>,
430 },
431 PageBreak {
434 #[serde(skip)]
436 id: NodeId,
437 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
439 attributes: Attributes,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 position: Option<SourcePos>,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 span: Option<SourceSpan>,
446 },
447 ColumnBreak {
450 #[serde(skip)]
452 id: NodeId,
453 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
455 attributes: Attributes,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 position: Option<SourcePos>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 span: Option<SourceSpan>,
462 },
463 Image {
465 #[serde(skip)]
467 id: NodeId,
468 url: String,
470 alt: String,
473 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
475 attributes: Attributes,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 position: Option<SourcePos>,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 span: Option<SourceSpan>,
482 },
483 List {
486 #[serde(skip)]
488 id: NodeId,
489 #[serde(default)]
491 ordered: bool,
492 #[serde(default = "first_number", skip_serializing_if = "is_first_number")]
494 start: u32,
495 #[serde(default)]
499 tight: bool,
500 #[serde(default)]
502 items: Vec<ListItem>,
503 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
505 attributes: Attributes,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 position: Option<SourcePos>,
509 #[serde(skip_serializing_if = "Option::is_none")]
511 span: Option<SourceSpan>,
512 },
513 Table {
515 #[serde(skip)]
517 id: NodeId,
518 #[serde(default)]
520 head: Vec<Row>,
521 #[serde(default)]
523 body: Vec<Row>,
524 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
526 attributes: Attributes,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 position: Option<SourcePos>,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 span: Option<SourceSpan>,
533 },
534}
535
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
538#[serde(into = "u8", try_from = "u8")]
539pub enum HeadingLevel {
540 H1,
542 H2,
544 H3,
546 H4,
548 H5,
550 H6,
552}
553
554impl From<HeadingLevel> for u8 {
555 fn from(level: HeadingLevel) -> u8 {
556 match level {
557 HeadingLevel::H1 => 1,
558 HeadingLevel::H2 => 2,
559 HeadingLevel::H3 => 3,
560 HeadingLevel::H4 => 4,
561 HeadingLevel::H5 => 5,
562 HeadingLevel::H6 => 6,
563 }
564 }
565}
566
567impl TryFrom<u8> for HeadingLevel {
568 type Error = InvalidHeadingLevel;
569
570 fn try_from(value: u8) -> Result<Self, Self::Error> {
571 match value {
572 1 => Ok(HeadingLevel::H1),
573 2 => Ok(HeadingLevel::H2),
574 3 => Ok(HeadingLevel::H3),
575 4 => Ok(HeadingLevel::H4),
576 5 => Ok(HeadingLevel::H5),
577 6 => Ok(HeadingLevel::H6),
578 _ => Err(InvalidHeadingLevel(value)),
579 }
580 }
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
585#[error("heading level must be 1-6, got {0}")]
586pub struct InvalidHeadingLevel(pub u8);
587
588fn first_number() -> u32 {
590 1
591}
592
593fn is_first_number(start: &u32) -> bool {
594 *start == 1
595}
596
597#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
599pub struct ListItem {
600 #[serde(skip)]
602 pub id: NodeId,
603 #[serde(default)]
605 pub blocks: Vec<Block>,
606 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
608 pub attributes: Attributes,
609 #[serde(skip_serializing_if = "Option::is_none")]
611 pub position: Option<SourcePos>,
612 #[serde(skip_serializing_if = "Option::is_none")]
614 pub span: Option<SourceSpan>,
615}
616
617pub fn item_blocks(items: &[ListItem]) -> impl Iterator<Item = &[Block]> {
619 items.iter().map(|item| item.blocks.as_slice())
620}
621
622#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
624pub struct Row {
625 #[serde(skip)]
627 pub id: NodeId,
628 #[serde(default)]
631 pub cells: Vec<Cell>,
632 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
634 pub attributes: Attributes,
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub position: Option<SourcePos>,
638 #[serde(skip_serializing_if = "Option::is_none")]
640 pub span: Option<SourceSpan>,
641}
642
643#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
645pub struct Cell {
646 #[serde(skip)]
648 pub id: NodeId,
649 #[serde(default)]
651 pub blocks: Vec<Block>,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub align: Option<Alignment>,
656 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
658 pub attributes: Attributes,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub position: Option<SourcePos>,
662 #[serde(skip_serializing_if = "Option::is_none")]
664 pub span: Option<SourceSpan>,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
669#[serde(rename_all = "snake_case")]
670pub enum Alignment {
671 Left,
673 Center,
675 Right,
677}
678
679pub fn rows<'a>(head: &'a [Row], body: &'a [Row]) -> impl Iterator<Item = &'a Row> {
681 head.iter().chain(body)
682}
683
684pub fn cell_blocks<'a>(head: &'a [Row], body: &'a [Row]) -> impl Iterator<Item = &'a [Block]> {
687 rows(head, body).flat_map(|row| row.cells.iter().map(|cell| cell.blocks.as_slice()))
688}
689
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
692#[serde(tag = "type", rename_all = "snake_case")]
693pub enum Inline {
694 Text {
697 #[serde(skip)]
699 id: NodeId,
700 value: String,
702 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
704 attributes: Attributes,
705 #[serde(skip_serializing_if = "Option::is_none")]
707 position: Option<SourcePos>,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 span: Option<SourceSpan>,
711 },
712 Break {
715 #[serde(skip)]
717 id: NodeId,
718 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
720 attributes: Attributes,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 position: Option<SourcePos>,
724 #[serde(skip_serializing_if = "Option::is_none")]
726 span: Option<SourceSpan>,
727 },
728 Emphasis {
730 #[serde(skip)]
732 id: NodeId,
733 children: Vec<Inline>,
735 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
737 attributes: Attributes,
738 #[serde(skip_serializing_if = "Option::is_none")]
740 position: Option<SourcePos>,
741 #[serde(skip_serializing_if = "Option::is_none")]
743 span: Option<SourceSpan>,
744 },
745 Strong {
747 #[serde(skip)]
749 id: NodeId,
750 children: Vec<Inline>,
752 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
754 attributes: Attributes,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 position: Option<SourcePos>,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 span: Option<SourceSpan>,
761 },
762 Code {
764 #[serde(skip)]
766 id: NodeId,
767 value: String,
769 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
771 attributes: Attributes,
772 #[serde(skip_serializing_if = "Option::is_none")]
774 position: Option<SourcePos>,
775 #[serde(skip_serializing_if = "Option::is_none")]
777 span: Option<SourceSpan>,
778 },
779 Link {
782 #[serde(skip)]
784 id: NodeId,
785 url: String,
787 children: Vec<Inline>,
789 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
791 attributes: Attributes,
792 #[serde(skip_serializing_if = "Option::is_none")]
794 position: Option<SourcePos>,
795 #[serde(skip_serializing_if = "Option::is_none")]
797 span: Option<SourceSpan>,
798 },
799 Note {
808 #[serde(skip)]
810 id: NodeId,
811 blocks: Vec<Block>,
813 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
815 attributes: Attributes,
816 #[serde(skip_serializing_if = "Option::is_none")]
818 position: Option<SourcePos>,
819 #[serde(skip_serializing_if = "Option::is_none")]
821 span: Option<SourceSpan>,
822 },
823 Strikethrough {
825 #[serde(skip)]
827 id: NodeId,
828 children: Vec<Inline>,
830 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
832 attributes: Attributes,
833 #[serde(skip_serializing_if = "Option::is_none")]
835 position: Option<SourcePos>,
836 #[serde(skip_serializing_if = "Option::is_none")]
838 span: Option<SourceSpan>,
839 },
840 Span {
844 #[serde(skip)]
846 id: NodeId,
847 children: Vec<Inline>,
849 #[serde(default, skip_serializing_if = "Attributes::is_empty")]
851 attributes: Attributes,
852 #[serde(skip_serializing_if = "Option::is_none")]
854 position: Option<SourcePos>,
855 #[serde(skip_serializing_if = "Option::is_none")]
857 span: Option<SourceSpan>,
858 },
859}
860
861pub fn text(inlines: &[Inline]) -> String {
865 let mut out = String::new();
866 push_text(inlines, &mut out);
867 out
868}
869
870fn push_text(inlines: &[Inline], out: &mut String) {
871 for inline in inlines {
872 match inline {
873 Inline::Text { value, .. } | Inline::Code { value, .. } => out.push_str(value),
874 Inline::Break { .. } => out.push('\n'),
875 Inline::Note { .. } => {}
878 Inline::Emphasis { children, .. }
879 | Inline::Strong { children, .. }
880 | Inline::Link { children, .. }
881 | Inline::Strikethrough { children, .. }
882 | Inline::Span { children, .. } => push_text(children, out),
883 }
884 }
885}
886
887impl Book {
888 pub fn assign_node_ids(&mut self) {
892 let mut next = 1u32;
893 for section in &mut self.sections {
894 section.id = next_id(&mut next);
895 for block in &mut section.blocks {
896 assign_block(block, &mut next);
897 }
898 }
899 }
900
901 pub fn node_at(&self, source: &str, byte: u32) -> Option<NodeId> {
911 self.sections
912 .iter()
913 .filter(|section| section.source.as_deref() == Some(source))
914 .find_map(|section| {
915 let span = section.span.filter(|span| span.covers(byte))?;
916 let inner = node_in_blocks(§ion.blocks, byte);
917 let inner = match node_in_notes(§ion.blocks, byte) {
918 Some(note) => Some(narrowest(note, inner)),
919 None => inner,
920 };
921 Some(narrowest((section.id, span), inner))
922 })
923 .map(|(node, _)| node)
924 }
925
926 pub fn source_of(&self, node: NodeId) -> Option<(&str, SourceSpan)> {
932 if node == NodeId::UNASSIGNED {
933 return None;
934 }
935 let at = self
939 .sections
940 .partition_point(|section| section.id.get() <= node.get())
941 .checked_sub(1)?;
942 let section = &self.sections[at];
943 let source = section.source.as_deref()?;
944 let span = if section.id == node {
945 section.span?
946 } else {
947 span_in_blocks(§ion.blocks, node)?
948 };
949 Some((source, span))
950 }
951
952 pub fn names(&self, source: Option<&str>) -> Names {
960 let mut classes = std::collections::BTreeSet::new();
961 let mut ids = std::collections::BTreeSet::new();
962 let mut add = |attributes: &Attributes| {
963 classes.extend(attributes.classes.iter().cloned());
964 ids.extend(attributes.id.iter().cloned());
965 };
966 for section in &self.sections {
967 if source.is_none_or(|source| section.source.as_deref() == Some(source)) {
968 names_in_blocks(§ion.blocks, &mut add);
969 }
970 }
971 Names {
972 classes: classes.into_iter().collect(),
973 ids: ids.into_iter().collect(),
974 }
975 }
976
977 pub fn subtree(&self, node: NodeId) -> Option<Range<u32>> {
986 if node == NodeId::UNASSIGNED {
987 return None;
988 }
989 let at = self
990 .sections
991 .partition_point(|section| section.id.get() <= node.get())
992 .checked_sub(1)?;
993 let section = &self.sections[at];
994 let first = section.id.get();
995 let count = 1 + section.blocks.iter().map(block_nodes).sum::<u32>();
996 if section.id == node {
997 return Some(first..first + count);
998 }
999 if !(first..first + count).contains(&node.get()) {
1000 return None;
1001 }
1002 subtree_in_blocks(§ion.blocks, node)
1003 }
1004}
1005
1006fn names_in_blocks(blocks: &[Block], add: &mut impl FnMut(&Attributes)) {
1009 for block in blocks {
1010 add(block_attributes(block));
1011 match block {
1012 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1013 names_in_inlines(inlines, add)
1014 }
1015 Block::Blockquote { blocks, .. } => names_in_blocks(blocks, add),
1016 Block::List { items, .. } => {
1017 for item in items {
1018 add(&item.attributes);
1019 names_in_blocks(&item.blocks, add);
1020 }
1021 }
1022 Block::Table { head, body, .. } => {
1023 for row in rows(head, body) {
1024 add(&row.attributes);
1025 for cell in &row.cells {
1026 add(&cell.attributes);
1027 names_in_blocks(&cell.blocks, add);
1028 }
1029 }
1030 }
1031 Block::CodeBlock { .. }
1032 | Block::ThematicBreak { .. }
1033 | Block::PageBreak { .. }
1034 | Block::ColumnBreak { .. }
1035 | Block::Image { .. } => {}
1036 }
1037 }
1038}
1039
1040fn names_in_inlines(inlines: &[Inline], add: &mut impl FnMut(&Attributes)) {
1042 for inline in inlines {
1043 add(inline_attributes(inline));
1044 match inline {
1045 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1046 Inline::Note { blocks, .. } => names_in_blocks(blocks, add),
1047 Inline::Emphasis { children, .. }
1048 | Inline::Strong { children, .. }
1049 | Inline::Link { children, .. }
1050 | Inline::Strikethrough { children, .. }
1051 | Inline::Span { children, .. } => names_in_inlines(children, add),
1052 }
1053 }
1054}
1055
1056fn subtree_in_blocks(blocks: &[Block], node: NodeId) -> Option<Range<u32>> {
1060 for block in blocks {
1061 let first = block_id(block).get();
1062 let held = first..first + block_nodes(block);
1063 if !held.contains(&node.get()) {
1064 continue;
1065 }
1066 if block_id(block) == node {
1067 return Some(held);
1068 }
1069 return match block {
1070 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1071 subtree_in_inlines(inlines, node)
1072 }
1073 Block::Blockquote { blocks, .. } => subtree_in_blocks(blocks, node),
1074 Block::List { items, .. } => subtree_in_items(items, node),
1075 Block::Table { head, body, .. } => subtree_in_rows(rows(head, body), node),
1076 Block::CodeBlock { .. }
1077 | Block::ThematicBreak { .. }
1078 | Block::PageBreak { .. }
1079 | Block::ColumnBreak { .. }
1080 | Block::Image { .. } => None,
1081 };
1082 }
1083 None
1084}
1085
1086fn subtree_in_items(items: &[ListItem], node: NodeId) -> Option<Range<u32>> {
1088 let item = items
1089 .iter()
1090 .find(|item| (item.id.get()..item.id.get() + item_nodes(item)).contains(&node.get()))?;
1091 if item.id == node {
1092 return Some(item.id.get()..item.id.get() + item_nodes(item));
1093 }
1094 subtree_in_blocks(&item.blocks, node)
1095}
1096
1097fn item_nodes(item: &ListItem) -> u32 {
1099 1 + item.blocks.iter().map(block_nodes).sum::<u32>()
1100}
1101
1102fn subtree_in_inlines(inlines: &[Inline], node: NodeId) -> Option<Range<u32>> {
1104 for inline in inlines {
1105 let first = inline_id(inline).get();
1106 let held = first..first + inline_nodes(inline);
1107 if !held.contains(&node.get()) {
1108 continue;
1109 }
1110 if inline_id(inline) == node {
1111 return Some(held);
1112 }
1113 return match inline {
1114 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => None,
1115 Inline::Note { blocks, .. } => subtree_in_blocks(blocks, node),
1116 Inline::Emphasis { children, .. }
1117 | Inline::Strong { children, .. }
1118 | Inline::Link { children, .. }
1119 | Inline::Strikethrough { children, .. }
1120 | Inline::Span { children, .. } => subtree_in_inlines(children, node),
1121 };
1122 }
1123 None
1124}
1125
1126fn block_nodes(block: &Block) -> u32 {
1128 1 + match block {
1129 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1130 inlines.iter().map(inline_nodes).sum()
1131 }
1132 Block::Blockquote { blocks, .. } => blocks.iter().map(block_nodes).sum(),
1133 Block::List { items, .. } => items.iter().map(item_nodes).sum(),
1134 Block::Table { head, body, .. } => rows(head, body).map(row_nodes).sum(),
1135 Block::CodeBlock { .. }
1136 | Block::ThematicBreak { .. }
1137 | Block::PageBreak { .. }
1138 | Block::ColumnBreak { .. }
1139 | Block::Image { .. } => 0,
1140 }
1141}
1142
1143fn row_nodes(row: &Row) -> u32 {
1145 1 + row.cells.iter().map(cell_nodes).sum::<u32>()
1146}
1147
1148fn cell_nodes(cell: &Cell) -> u32 {
1150 1 + cell.blocks.iter().map(block_nodes).sum::<u32>()
1151}
1152
1153fn subtree_in_rows<'a>(rows: impl Iterator<Item = &'a Row>, node: NodeId) -> Option<Range<u32>> {
1155 for row in rows {
1156 let held = row.id.get()..row.id.get() + row_nodes(row);
1157 if !held.contains(&node.get()) {
1158 continue;
1159 }
1160 if row.id == node {
1161 return Some(held);
1162 }
1163 for cell in &row.cells {
1164 let held = cell.id.get()..cell.id.get() + cell_nodes(cell);
1165 if !held.contains(&node.get()) {
1166 continue;
1167 }
1168 if cell.id == node {
1169 return Some(held);
1170 }
1171 return subtree_in_blocks(&cell.blocks, node);
1172 }
1173 return None;
1174 }
1175 None
1176}
1177
1178pub(crate) fn inline_nodes(inline: &Inline) -> u32 {
1180 1 + match inline {
1181 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => 0,
1182 Inline::Note { blocks, .. } => blocks.iter().map(block_nodes).sum(),
1183 Inline::Emphasis { children, .. }
1184 | Inline::Strong { children, .. }
1185 | Inline::Link { children, .. }
1186 | Inline::Strikethrough { children, .. }
1187 | Inline::Span { children, .. } => children.iter().map(inline_nodes).sum(),
1188 }
1189}
1190
1191fn narrowest(
1194 node: (NodeId, SourceSpan),
1195 inner: Option<(NodeId, SourceSpan)>,
1196) -> (NodeId, SourceSpan) {
1197 match inner {
1198 Some(inner) if inner.1.width() <= node.1.width() => inner,
1199 _ => node,
1200 }
1201}
1202
1203fn node_in_blocks(blocks: &[Block], byte: u32) -> Option<(NodeId, SourceSpan)> {
1209 let mut found: Option<(NodeId, SourceSpan)> = None;
1210 for block in blocks {
1211 let Some(span) = block_span(block) else {
1212 continue;
1213 };
1214 if span.start > byte {
1215 break;
1216 }
1217 if !span.covers(byte) {
1218 continue;
1219 }
1220 let inner = match block {
1221 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1222 node_in_inlines(inlines, byte)
1223 }
1224 Block::Blockquote { blocks, .. } => node_in_blocks(blocks, byte),
1225 Block::List { items, .. } => node_in_items(items, byte),
1226 Block::Table { head, body, .. } => node_in_rows(rows(head, body), byte),
1227 Block::CodeBlock { .. }
1228 | Block::ThematicBreak { .. }
1229 | Block::PageBreak { .. }
1230 | Block::ColumnBreak { .. }
1231 | Block::Image { .. } => None,
1232 };
1233 let hit = narrowest((block_id(block), span), inner);
1234 if found.is_none_or(|found| hit.1.width() < found.1.width()) {
1235 found = Some(hit);
1236 }
1237 }
1238 found
1239}
1240
1241fn node_in_inlines(inlines: &[Inline], byte: u32) -> Option<(NodeId, SourceSpan)> {
1243 for inline in inlines {
1244 let Some(span) = inline_span(inline) else {
1245 continue;
1246 };
1247 if span.start > byte {
1248 break;
1249 }
1250 if !span.covers(byte) {
1251 continue;
1252 }
1253 let inner = match inline {
1254 Inline::Text { .. }
1258 | Inline::Code { .. }
1259 | Inline::Break { .. }
1260 | Inline::Note { .. } => None,
1261 Inline::Emphasis { children, .. }
1262 | Inline::Strong { children, .. }
1263 | Inline::Link { children, .. }
1264 | Inline::Strikethrough { children, .. }
1265 | Inline::Span { children, .. } => node_in_inlines(children, byte),
1266 };
1267 return Some(narrowest((inline_id(inline), span), inner));
1268 }
1269 None
1270}
1271
1272pub fn notes_in_blocks(blocks: &[Block]) -> Vec<&Inline> {
1275 let mut out = Vec::new();
1276 gather_notes_in_blocks(blocks, &mut out);
1277 out
1278}
1279
1280pub fn notes_in_inlines(inlines: &[Inline]) -> Vec<&Inline> {
1283 let mut out = Vec::new();
1284 shallow_notes_in_inlines(inlines, &mut out);
1285 out
1286}
1287
1288fn shallow_notes_in_inlines<'b>(inlines: &'b [Inline], out: &mut Vec<&'b Inline>) {
1289 for inline in inlines {
1290 match inline {
1291 Inline::Note { .. } => out.push(inline),
1292 Inline::Emphasis { children, .. }
1293 | Inline::Strong { children, .. }
1294 | Inline::Link { children, .. }
1295 | Inline::Strikethrough { children, .. }
1296 | Inline::Span { children, .. } => shallow_notes_in_inlines(children, out),
1297 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1298 }
1299 }
1300}
1301
1302fn gather_notes_in_blocks<'b>(blocks: &'b [Block], out: &mut Vec<&'b Inline>) {
1303 for block in blocks {
1304 match block {
1305 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1306 gather_notes_in_inlines(inlines, out)
1307 }
1308 Block::Blockquote { blocks, .. } => gather_notes_in_blocks(blocks, out),
1309 Block::List { items, .. } => {
1310 for item in items {
1311 gather_notes_in_blocks(&item.blocks, out);
1312 }
1313 }
1314 Block::Table { head, body, .. } => {
1315 for blocks in cell_blocks(head, body) {
1316 gather_notes_in_blocks(blocks, out);
1317 }
1318 }
1319 Block::CodeBlock { .. }
1320 | Block::ThematicBreak { .. }
1321 | Block::PageBreak { .. }
1322 | Block::ColumnBreak { .. }
1323 | Block::Image { .. } => {}
1324 }
1325 }
1326}
1327
1328fn gather_notes_in_inlines<'b>(inlines: &'b [Inline], out: &mut Vec<&'b Inline>) {
1329 for inline in inlines {
1330 match inline {
1331 Inline::Note { blocks, .. } => {
1332 out.push(inline);
1333 gather_notes_in_blocks(blocks, out);
1334 }
1335 Inline::Emphasis { children, .. }
1336 | Inline::Strong { children, .. }
1337 | Inline::Link { children, .. }
1338 | Inline::Strikethrough { children, .. }
1339 | Inline::Span { children, .. } => gather_notes_in_inlines(children, out),
1340 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1341 }
1342 }
1343}
1344
1345fn node_in_notes(blocks: &[Block], byte: u32) -> Option<(NodeId, SourceSpan)> {
1353 let mut found: Option<(NodeId, SourceSpan)> = None;
1354 for note in notes_in_blocks(blocks) {
1355 let Inline::Note { id, blocks, .. } = note else {
1356 continue;
1357 };
1358 let Some(hit) = node_in_blocks(blocks, byte) else {
1359 continue;
1360 };
1361 let hit = match inline_span(note) {
1362 Some(span) if span.covers(byte) => narrowest((*id, span), Some(hit)),
1363 _ => hit,
1364 };
1365 if found.is_none_or(|found| hit.1.width() < found.1.width()) {
1366 found = Some(hit);
1367 }
1368 }
1369 found
1370}
1371
1372fn span_in_blocks(blocks: &[Block], node: NodeId) -> Option<SourceSpan> {
1374 for block in blocks {
1375 if block_id(block) == node {
1376 return block_span(block);
1377 }
1378 let found = match block {
1379 Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1380 span_in_inlines(inlines, node)
1381 }
1382 Block::Blockquote { blocks, .. } => span_in_blocks(blocks, node),
1383 Block::List { items, .. } => span_in_items(items, node),
1384 Block::Table { head, body, .. } => span_in_rows(rows(head, body), node),
1385 Block::CodeBlock { .. }
1386 | Block::ThematicBreak { .. }
1387 | Block::PageBreak { .. }
1388 | Block::ColumnBreak { .. }
1389 | Block::Image { .. } => None,
1390 };
1391 if found.is_some() {
1392 return found;
1393 }
1394 }
1395 None
1396}
1397
1398fn node_in_items(items: &[ListItem], byte: u32) -> Option<(NodeId, SourceSpan)> {
1401 items.iter().find_map(|item| {
1402 let span = item.span.filter(|span| span.covers(byte))?;
1403 Some(narrowest(
1404 (item.id, span),
1405 node_in_blocks(&item.blocks, byte),
1406 ))
1407 })
1408}
1409
1410fn span_in_items(items: &[ListItem], node: NodeId) -> Option<SourceSpan> {
1412 items.iter().find_map(|item| {
1413 if item.id == node {
1414 item.span
1415 } else {
1416 span_in_blocks(&item.blocks, node)
1417 }
1418 })
1419}
1420
1421fn span_in_inlines(inlines: &[Inline], node: NodeId) -> Option<SourceSpan> {
1423 for inline in inlines {
1424 if inline_id(inline) == node {
1425 return inline_span(inline);
1426 }
1427 let found = match inline {
1428 Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => None,
1429 Inline::Note { blocks, .. } => span_in_blocks(blocks, node),
1430 Inline::Emphasis { children, .. }
1431 | Inline::Strong { children, .. }
1432 | Inline::Link { children, .. }
1433 | Inline::Strikethrough { children, .. }
1434 | Inline::Span { children, .. } => span_in_inlines(children, node),
1435 };
1436 if found.is_some() {
1437 return found;
1438 }
1439 }
1440 None
1441}
1442
1443fn node_in_rows<'a>(
1446 rows: impl Iterator<Item = &'a Row>,
1447 byte: u32,
1448) -> Option<(NodeId, SourceSpan)> {
1449 for row in rows {
1450 let Some(span) = row.span.filter(|span| span.covers(byte)) else {
1451 continue;
1452 };
1453 let inner = row.cells.iter().find_map(|cell| {
1454 let span = cell.span.filter(|span| span.covers(byte))?;
1455 Some(narrowest(
1456 (cell.id, span),
1457 node_in_blocks(&cell.blocks, byte),
1458 ))
1459 });
1460 return Some(narrowest((row.id, span), inner));
1461 }
1462 None
1463}
1464
1465fn span_in_rows<'a>(rows: impl Iterator<Item = &'a Row>, node: NodeId) -> Option<SourceSpan> {
1467 for row in rows {
1468 if row.id == node {
1469 return row.span;
1470 }
1471 for cell in &row.cells {
1472 if cell.id == node {
1473 return cell.span;
1474 }
1475 let found = span_in_blocks(&cell.blocks, node);
1476 if found.is_some() {
1477 return found;
1478 }
1479 }
1480 }
1481 None
1482}
1483
1484pub fn block_attributes(block: &Block) -> &Attributes {
1486 match block {
1487 Block::Heading { attributes, .. }
1488 | Block::Paragraph { attributes, .. }
1489 | Block::Blockquote { attributes, .. }
1490 | Block::ThematicBreak { attributes, .. }
1491 | Block::PageBreak { attributes, .. }
1492 | Block::ColumnBreak { attributes, .. }
1493 | Block::Image { attributes, .. }
1494 | Block::List { attributes, .. }
1495 | Block::CodeBlock { attributes, .. }
1496 | Block::Table { attributes, .. } => attributes,
1497 }
1498}
1499
1500pub fn inline_attributes(inline: &Inline) -> &Attributes {
1502 match inline {
1503 Inline::Text { attributes, .. }
1504 | Inline::Code { attributes, .. }
1505 | Inline::Break { attributes, .. }
1506 | Inline::Emphasis { attributes, .. }
1507 | Inline::Strong { attributes, .. }
1508 | Inline::Link { attributes, .. }
1509 | Inline::Note { attributes, .. }
1510 | Inline::Strikethrough { attributes, .. }
1511 | Inline::Span { attributes, .. } => attributes,
1512 }
1513}
1514
1515pub fn block_position(block: &Block) -> Option<SourcePos> {
1517 match block {
1518 Block::Heading { position, .. }
1519 | Block::Paragraph { position, .. }
1520 | Block::Blockquote { position, .. }
1521 | Block::ThematicBreak { position, .. }
1522 | Block::PageBreak { position, .. }
1523 | Block::ColumnBreak { position, .. }
1524 | Block::Image { position, .. }
1525 | Block::List { position, .. }
1526 | Block::CodeBlock { position, .. }
1527 | Block::Table { position, .. } => *position,
1528 }
1529}
1530
1531pub fn inline_position(inline: &Inline) -> Option<SourcePos> {
1533 match inline {
1534 Inline::Text { position, .. }
1535 | Inline::Code { position, .. }
1536 | Inline::Break { position, .. }
1537 | Inline::Emphasis { position, .. }
1538 | Inline::Strong { position, .. }
1539 | Inline::Link { position, .. }
1540 | Inline::Note { position, .. }
1541 | Inline::Strikethrough { position, .. }
1542 | Inline::Span { position, .. } => *position,
1543 }
1544}
1545
1546pub fn block_id(block: &Block) -> NodeId {
1548 match block {
1549 Block::Heading { id, .. }
1550 | Block::Paragraph { id, .. }
1551 | Block::Blockquote { id, .. }
1552 | Block::ThematicBreak { id, .. }
1553 | Block::PageBreak { id, .. }
1554 | Block::ColumnBreak { id, .. }
1555 | Block::Image { id, .. }
1556 | Block::List { id, .. }
1557 | Block::CodeBlock { id, .. }
1558 | Block::Table { id, .. } => *id,
1559 }
1560}
1561
1562pub fn block_span(block: &Block) -> Option<SourceSpan> {
1564 match block {
1565 Block::Heading { span, .. }
1566 | Block::Paragraph { span, .. }
1567 | Block::Blockquote { span, .. }
1568 | Block::ThematicBreak { span, .. }
1569 | Block::PageBreak { span, .. }
1570 | Block::ColumnBreak { span, .. }
1571 | Block::Image { span, .. }
1572 | Block::List { span, .. }
1573 | Block::CodeBlock { span, .. }
1574 | Block::Table { span, .. } => *span,
1575 }
1576}
1577
1578pub fn inline_id(inline: &Inline) -> NodeId {
1580 match inline {
1581 Inline::Text { id, .. }
1582 | Inline::Code { id, .. }
1583 | Inline::Break { id, .. }
1584 | Inline::Emphasis { id, .. }
1585 | Inline::Strong { id, .. }
1586 | Inline::Link { id, .. }
1587 | Inline::Note { id, .. }
1588 | Inline::Strikethrough { id, .. }
1589 | Inline::Span { id, .. } => *id,
1590 }
1591}
1592
1593pub fn inline_span(inline: &Inline) -> Option<SourceSpan> {
1595 match inline {
1596 Inline::Text { span, .. }
1597 | Inline::Code { span, .. }
1598 | Inline::Break { span, .. }
1599 | Inline::Emphasis { span, .. }
1600 | Inline::Strong { span, .. }
1601 | Inline::Link { span, .. }
1602 | Inline::Note { span, .. }
1603 | Inline::Strikethrough { span, .. }
1604 | Inline::Span { span, .. } => *span,
1605 }
1606}
1607
1608fn next_id(next: &mut u32) -> NodeId {
1609 let id = NodeId(*next);
1610 *next += 1;
1611 id
1612}
1613
1614fn assign_block(block: &mut Block, next: &mut u32) {
1615 match block {
1616 Block::Heading { id, inlines, .. } | Block::Paragraph { id, inlines, .. } => {
1617 *id = next_id(next);
1618 for inline in inlines {
1619 assign_inline(inline, next);
1620 }
1621 }
1622 Block::Blockquote { id, blocks, .. } => {
1623 *id = next_id(next);
1624 for nested in blocks {
1625 assign_block(nested, next);
1626 }
1627 }
1628 Block::CodeBlock { id, .. }
1629 | Block::ThematicBreak { id, .. }
1630 | Block::PageBreak { id, .. }
1631 | Block::ColumnBreak { id, .. }
1632 | Block::Image { id, .. } => {
1633 *id = next_id(next);
1634 }
1635 Block::List { id, items, .. } => {
1636 *id = next_id(next);
1637 for item in items {
1638 item.id = next_id(next);
1639 for nested in &mut item.blocks {
1640 assign_block(nested, next);
1641 }
1642 }
1643 }
1644 Block::Table { id, head, body, .. } => {
1645 *id = next_id(next);
1646 for row in head.iter_mut().chain(body.iter_mut()) {
1647 row.id = next_id(next);
1648 for cell in &mut row.cells {
1649 cell.id = next_id(next);
1650 for nested in &mut cell.blocks {
1651 assign_block(nested, next);
1652 }
1653 }
1654 }
1655 }
1656 }
1657}
1658
1659fn assign_inline(inline: &mut Inline, next: &mut u32) {
1660 match inline {
1661 Inline::Text { id, .. } | Inline::Code { id, .. } | Inline::Break { id, .. } => {
1662 *id = next_id(next);
1663 }
1664 Inline::Note { id, blocks, .. } => {
1665 *id = next_id(next);
1666 for block in blocks {
1667 assign_block(block, next);
1668 }
1669 }
1670 Inline::Emphasis { id, children, .. }
1671 | Inline::Strong { id, children, .. }
1672 | Inline::Link { id, children, .. }
1673 | Inline::Strikethrough { id, children, .. }
1674 | Inline::Span { id, children, .. } => {
1675 *id = next_id(next);
1676 for child in children {
1677 assign_inline(child, next);
1678 }
1679 }
1680 }
1681}
1682
1683#[cfg(test)]
1684mod tests {
1685 use super::*;
1686
1687 #[test]
1691 fn a_pseudo_element_maps_to_its_element_and_moves_with_it() {
1692 let element = NodeId::new(40);
1693 let kinds = [
1694 PseudoElement::Before,
1695 PseudoElement::After,
1696 PseudoElement::FirstLetter,
1697 PseudoElement::FirstLine,
1698 ];
1699 let ids: Vec<NodeId> = kinds.iter().map(|which| element.pseudo(*which)).collect();
1700 for (id, which) in ids.iter().zip(kinds) {
1701 assert_eq!(id.pseudo_element(), Some((element, which)));
1702 assert_eq!(id.element(), element);
1703 assert!(id.get() >= 1 << 31, "{id:?} is in the range of content ids");
1704 assert_eq!(
1705 id.shifted(-3).pseudo_element(),
1706 Some((NodeId::new(37), which))
1707 );
1708 assert_eq!(ids.iter().filter(|other| *other == id).count(), 1);
1709 }
1710 assert_eq!(element.pseudo_element(), None);
1711 assert_eq!(element.element(), element);
1712 assert_eq!(element.shifted(-3).pseudo_element(), None);
1713 }
1714
1715 const SOURCE: &str = "\
1718# Chapter One
1719
1720It was the kind of morning that made you suspicious — too *clean*, too quiet.
1721
1722> \"Nobody's early here.\"
1723
1724---
1725
1726
1727
17281. A watch
17292. A purse
1730
1731| Pocket | Found |
1732|:---|---:|
1733| Right | A handkerchief |
1734";
1735
1736 fn span(of: &str) -> Option<SourceSpan> {
1738 let start = SOURCE.find(of).expect("the sample source has it");
1739 Some(SourceSpan {
1740 start: start as u32,
1741 end: (start + of.len()) as u32,
1742 })
1743 }
1744
1745 fn text(value: &str) -> Inline {
1748 Inline::Text {
1749 id: NodeId::UNASSIGNED,
1750 value: value.into(),
1751 attributes: Attributes::default(),
1752 position: None,
1753 span: span(value),
1754 }
1755 }
1756
1757 fn item(line: &str, value: &str) -> ListItem {
1760 ListItem {
1761 blocks: vec![Block::Paragraph {
1762 id: NodeId::UNASSIGNED,
1763 inlines: vec![text(value)],
1764 attributes: Attributes::default(),
1765 position: None,
1766 span: span(value),
1767 }],
1768 span: span(line),
1769 ..ListItem::default()
1770 }
1771 }
1772
1773 #[test]
1776 fn a_list_serializes_as_items_of_blocks() {
1777 let list = sample_book().sections[0].blocks[5].clone();
1778 let json = serde_json::to_value(&list).unwrap();
1779 assert_eq!(json["type"], "list");
1780 assert_eq!(json["ordered"], true);
1781 assert_eq!(json["tight"], true);
1782 assert!(json.get("start").is_none(), "{json}");
1783 assert_eq!(
1784 json["items"][1]["blocks"][0]["inlines"][0]["value"],
1785 "A purse"
1786 );
1787
1788 let Block::List {
1789 ordered,
1790 tight,
1791 items,
1792 attributes,
1793 position,
1794 span,
1795 ..
1796 } = list
1797 else {
1798 panic!("the sixth block is a list");
1799 };
1800 let seventh = Block::List {
1801 id: NodeId::UNASSIGNED,
1802 ordered,
1803 start: 7,
1804 tight,
1805 items,
1806 attributes,
1807 position,
1808 span,
1809 };
1810 let json = serde_json::to_value(&seventh).unwrap();
1811 assert_eq!(json["start"], 7);
1812 let read: Block = serde_json::from_value(json).unwrap();
1813 assert_eq!(read, seventh);
1814 }
1815
1816 #[test]
1821 fn a_list_numbers_its_items_and_a_byte_of_one_answers_with_its_run() {
1822 let mut book = sample_book();
1823 book.assign_node_ids();
1824 let Block::List { id, items, .. } = &book.sections[0].blocks[5] else {
1825 panic!("the sixth block is a list");
1826 };
1827 assert!(*id < items[0].id);
1828 assert!(items[0].id < block_id(&items[0].blocks[0]));
1829 assert!(block_id(&items[0].blocks[0]) < items[1].id);
1830 let held = book.subtree(*id).expect("the book holds the list");
1831 assert!(held.contains(&block_id(&items[1].blocks[0]).get()));
1832
1833 let Block::Paragraph { inlines, .. } = &items[1].blocks[0] else {
1834 panic!("the item holds a paragraph");
1835 };
1836 let byte = SOURCE.find("purse").unwrap() as u32;
1837 assert_eq!(
1838 book.node_at("chapter-01.md", byte),
1839 Some(inline_id(&inlines[0]))
1840 );
1841 let marker = SOURCE.find("2. A purse").unwrap() as u32;
1842 assert_eq!(book.node_at("chapter-01.md", marker), Some(items[1].id));
1843 }
1844
1845 fn row(line: &str, cells: Vec<Cell>) -> Row {
1847 Row {
1848 cells,
1849 span: span(line),
1850 ..Row::default()
1851 }
1852 }
1853
1854 fn cell(value: &str, align: Alignment) -> Cell {
1857 Cell {
1858 blocks: vec![Block::Paragraph {
1859 id: NodeId::UNASSIGNED,
1860 inlines: vec![text(value)],
1861 attributes: Attributes::default(),
1862 position: None,
1863 span: span(value),
1864 }],
1865 align: Some(align),
1866 span: span(value),
1867 ..Cell::default()
1868 }
1869 }
1870
1871 fn table(book: &Book) -> (&[Row], &[Row]) {
1873 let Some(Block::Table { head, body, .. }) = book.sections[0].blocks.last() else {
1874 panic!("the sample book ends with a table");
1875 };
1876 (head, body)
1877 }
1878
1879 #[test]
1883 fn a_table_serializes_as_rows_of_cells() {
1884 let json = serde_json::to_value(sample_book().sections[0].blocks.last()).unwrap();
1885 assert_eq!(json["type"], "table");
1886 assert_eq!(json["head"][0]["cells"][1]["align"], "right");
1887 assert_eq!(
1888 json["body"][0]["cells"][1]["blocks"][0],
1889 serde_json::json!({
1890 "type": "paragraph",
1891 "inlines": [{"type": "text", "value": "A handkerchief", "span": {
1892 "start": SOURCE.find("A handkerchief").unwrap(),
1893 "end": SOURCE.find("A handkerchief").unwrap() + "A handkerchief".len(),
1894 }}],
1895 "span": {
1896 "start": SOURCE.find("A handkerchief").unwrap(),
1897 "end": SOURCE.find("A handkerchief").unwrap() + "A handkerchief".len(),
1898 },
1899 }),
1900 );
1901 }
1902
1903 #[test]
1907 fn a_table_numbers_its_rows_and_cells_in_reading_order() {
1908 let mut book = sample_book();
1909 book.assign_node_ids();
1910 let table_id = block_id(book.sections[0].blocks.last().unwrap());
1911 let (head, body) = table(&book);
1912 let header = &head[0];
1913 let first = &body[0];
1914 assert!(table_id < header.id);
1915 assert!(header.id < header.cells[0].id);
1916 assert!(header.cells[0].id < block_id(&header.cells[0].blocks[0]));
1917 assert!(block_id(&header.cells[1].blocks[0]) < first.id);
1918 assert!(first.id < first.cells[1].id);
1919
1920 let held = book.subtree(first.id).expect("the book holds the row");
1922 assert_eq!(held.start, first.id.get());
1923 assert!(held.contains(&block_id(&first.cells[1].blocks[0]).get()));
1924 assert_eq!(
1925 book.subtree(table_id).map(|held| held.end),
1926 book.subtree(book.sections[0].id).map(|held| held.end),
1927 "the table is the last node of the book",
1928 );
1929 }
1930
1931 #[test]
1934 fn a_byte_of_a_cell_answers_with_the_run_written_there() {
1935 let mut book = sample_book();
1936 book.assign_node_ids();
1937 let (_, body) = table(&book);
1938 let Block::Paragraph { inlines, .. } = &body[0].cells[1].blocks[0] else {
1939 panic!("the cell holds a paragraph");
1940 };
1941 let byte = SOURCE.find("handkerchief").unwrap() as u32;
1942 assert_eq!(
1943 book.node_at("chapter-01.md", byte),
1944 Some(inline_id(&inlines[0]))
1945 );
1946 let pipe = SOURCE.find("| Right").unwrap() as u32;
1947 assert_eq!(book.node_at("chapter-01.md", pipe), Some(body[0].id));
1948 }
1949
1950 #[test]
1955 fn a_tree_the_engine_wrote_reads_back() {
1956 for book in [sample_book(), Book::default()] {
1957 let json = serde_json::to_string(&book).unwrap();
1958 let read: Book = serde_json::from_str(&json).unwrap();
1959 assert_eq!(read, book, "{json}");
1960 }
1961 }
1962
1963 fn sample_book() -> Book {
1964 Book {
1965 metadata: Metadata {
1966 title: Some("The Fixture Book".into()),
1967 author: Some("A. Author".into()),
1968 extra: [("language".to_string(), "en".to_string())]
1969 .into_iter()
1970 .collect(),
1971 },
1972 sections: vec![Section {
1973 attributes: Default::default(),
1974 id: NodeId::UNASSIGNED,
1975 source: Some("chapter-01.md".into()),
1976 title: Some("Chapter One".into()),
1977 blocks: vec![
1978 Block::Heading {
1979 id: NodeId::UNASSIGNED,
1980 level: HeadingLevel::H1,
1981 inlines: vec![text("Chapter One")],
1982 attributes: Attributes::default(),
1983 position: Some(SourcePos { line: 1, column: 1 }),
1984 span: span("# Chapter One\n"),
1985 },
1986 Block::Paragraph {
1987 id: NodeId::UNASSIGNED,
1988 inlines: vec![
1989 text("It was the kind of morning that made you suspicious — too "),
1990 Inline::Emphasis {
1991 id: NodeId::UNASSIGNED,
1992 children: vec![text("clean")],
1993 attributes: Attributes::default(),
1994 position: None,
1995 span: span("*clean*"),
1996 },
1997 text(", too quiet."),
1998 ],
1999 attributes: Attributes::default(),
2000 position: Some(SourcePos { line: 3, column: 1 }),
2001 span: span(
2002 "It was the kind of morning that made you suspicious — too *clean*, too quiet.\n",
2003 ),
2004 },
2005 Block::Blockquote {
2006 id: NodeId::UNASSIGNED,
2007 blocks: vec![Block::Paragraph {
2008 id: NodeId::UNASSIGNED,
2009 inlines: vec![text("\"Nobody's early here.\"")],
2010 attributes: Attributes::default(),
2011 position: None,
2012 span: span("\"Nobody's early here.\"\n"),
2013 }],
2014 attributes: Attributes::default(),
2015 position: Some(SourcePos { line: 5, column: 1 }),
2016 span: span("> \"Nobody's early here.\"\n"),
2017 },
2018 Block::ThematicBreak {
2019 id: NodeId::UNASSIGNED,
2020 attributes: Attributes::default(),
2021 position: Some(SourcePos { line: 7, column: 1 }),
2022 span: span("---\n"),
2023 },
2024 Block::Image {
2025 id: NodeId::UNASSIGNED,
2026 url: "images/drawer.png".into(),
2027 alt: "The drawer of knives".into(),
2028 attributes: Attributes::default(),
2029 position: Some(SourcePos { line: 9, column: 1 }),
2030 span: span(""),
2031 },
2032 Block::List {
2033 id: NodeId::UNASSIGNED,
2034 ordered: true,
2035 start: 1,
2036 tight: true,
2037 items: vec![
2038 item("1. A watch\n", "A watch"),
2039 item("2. A purse\n", "A purse"),
2040 ],
2041 attributes: Attributes::default(),
2042 position: Some(SourcePos {
2043 line: 11,
2044 column: 1,
2045 }),
2046 span: span("1. A watch\n2. A purse\n"),
2047 },
2048 Block::Table {
2049 id: NodeId::UNASSIGNED,
2050 head: vec![row(
2051 "| Pocket | Found |\n",
2052 vec![
2053 cell("Pocket", Alignment::Left),
2054 cell("Found", Alignment::Right),
2055 ],
2056 )],
2057 body: vec![row(
2058 "| Right | A handkerchief |\n",
2059 vec![
2060 cell("Right", Alignment::Left),
2061 cell("A handkerchief", Alignment::Right),
2062 ],
2063 )],
2064 attributes: Attributes::default(),
2065 position: Some(SourcePos {
2066 line: 14,
2067 column: 1,
2068 }),
2069 span: span("| Pocket | Found |\n|:---|---:|\n| Right | A handkerchief |\n"),
2070 },
2071 ],
2072 position: Some(SourcePos { line: 1, column: 1 }),
2073 span: Some(SourceSpan {
2074 start: 0,
2075 end: SOURCE.len() as u32,
2076 }),
2077 }],
2078 }
2079 }
2080
2081 fn collect_ids(book: &Book) -> Vec<NodeId> {
2083 fn walk_block(ids: &mut Vec<NodeId>, block: &Block) {
2084 match block {
2085 Block::Heading { id, inlines, .. } | Block::Paragraph { id, inlines, .. } => {
2086 ids.push(*id);
2087 ids.extend(inlines.iter().flat_map(walk_inline_ids));
2088 }
2089 Block::Blockquote { id, blocks, .. } => {
2090 ids.push(*id);
2091 for nested in blocks {
2092 walk_block(ids, nested);
2093 }
2094 }
2095 Block::CodeBlock { id, .. }
2096 | Block::ThematicBreak { id, .. }
2097 | Block::PageBreak { id, .. }
2098 | Block::ColumnBreak { id, .. }
2099 | Block::Image { id, .. } => ids.push(*id),
2100 Block::List { id, items, .. } => {
2101 ids.push(*id);
2102 for item in items {
2103 ids.push(item.id);
2104 for nested in &item.blocks {
2105 walk_block(ids, nested);
2106 }
2107 }
2108 }
2109 Block::Table { id, head, body, .. } => {
2110 ids.push(*id);
2111 for row in rows(head, body) {
2112 ids.push(row.id);
2113 for cell in &row.cells {
2114 ids.push(cell.id);
2115 for nested in &cell.blocks {
2116 walk_block(ids, nested);
2117 }
2118 }
2119 }
2120 }
2121 }
2122 }
2123
2124 fn walk_inline_ids(inline: &Inline) -> Vec<NodeId> {
2125 match inline {
2126 Inline::Text { id, .. } | Inline::Code { id, .. } | Inline::Break { id, .. } => {
2127 vec![*id]
2128 }
2129 Inline::Note { id, blocks, .. } => {
2130 let mut ids = vec![*id];
2131 for block in blocks {
2132 walk_block(&mut ids, block);
2133 }
2134 ids
2135 }
2136 Inline::Emphasis { id, children, .. }
2137 | Inline::Strong { id, children, .. }
2138 | Inline::Link { id, children, .. }
2139 | Inline::Strikethrough { id, children, .. }
2140 | Inline::Span { id, children, .. } => {
2141 let mut ids = vec![*id];
2142 ids.extend(children.iter().flat_map(walk_inline_ids));
2143 ids
2144 }
2145 }
2146 }
2147
2148 let mut ids = Vec::new();
2149 for section in &book.sections {
2150 ids.push(section.id);
2151 for block in §ion.blocks {
2152 walk_block(&mut ids, block);
2153 }
2154 }
2155 ids
2156 }
2157
2158 #[test]
2161 fn serialization_is_stable() {
2162 let mut book = sample_book();
2163 book.assign_node_ids();
2164 let once = serde_json::to_string_pretty(&book).unwrap();
2165 assert_eq!(once, serde_json::to_string_pretty(&book).unwrap());
2166 }
2167
2168 #[test]
2171 fn ids_are_never_serialized() {
2172 let mut book = sample_book();
2173 book.assign_node_ids();
2174 let json = serde_json::to_string(&book).unwrap();
2175 assert!(!json.contains("\"id\""));
2176 }
2177
2178 #[test]
2181 fn a_serialized_tree_is_internally_tagged() {
2182 let block = Block::Paragraph {
2183 id: NodeId::UNASSIGNED,
2184 inlines: vec![
2185 Inline::Text {
2186 id: NodeId::UNASSIGNED,
2187 value: "plain ".into(),
2188 attributes: Attributes::default(),
2189 position: None,
2190 span: None,
2191 },
2192 Inline::Strong {
2193 id: NodeId::UNASSIGNED,
2194 children: vec![Inline::Text {
2195 id: NodeId::UNASSIGNED,
2196 value: "bold".into(),
2197 attributes: Attributes::default(),
2198 position: None,
2199 span: None,
2200 }],
2201 attributes: Attributes::default(),
2202 position: None,
2203 span: None,
2204 },
2205 ],
2206 attributes: Attributes::default(),
2207 position: Some(SourcePos { line: 4, column: 1 }),
2208 span: Some(SourceSpan { start: 40, end: 58 }),
2209 };
2210 assert_eq!(
2211 serde_json::to_value(&block).unwrap(),
2212 serde_json::json!({
2213 "type": "paragraph",
2214 "inlines": [
2215 {"type": "text", "value": "plain "},
2216 {"type": "strong", "children": [{"type": "text", "value": "bold"}]},
2217 ],
2218 "position": {"line": 4, "column": 1},
2219 "span": {"start": 40, "end": 58},
2220 }),
2221 );
2222 }
2223
2224 #[test]
2228 fn a_hard_break_is_an_inline_of_its_own() {
2229 let words = |value: &str| Inline::Text {
2230 id: NodeId::UNASSIGNED,
2231 value: value.into(),
2232 attributes: Attributes::default(),
2233 position: None,
2234 span: None,
2235 };
2236 let inlines = vec![
2237 words("Chapter One"),
2238 Inline::Break {
2239 id: NodeId::UNASSIGNED,
2240 attributes: Attributes::default(),
2241 position: None,
2242 span: None,
2243 },
2244 words("The Voyage to Lilliput"),
2245 ];
2246 let json = serde_json::to_value(&inlines).unwrap();
2247 assert_eq!(json[1], serde_json::json!({"type": "break"}));
2248 let read: Vec<Inline> = serde_json::from_value(json).unwrap();
2249 assert_eq!(read, inlines);
2250 assert_eq!(super::text(&inlines), "Chapter One\nThe Voyage to Lilliput");
2251 }
2252
2253 #[test]
2256 fn heading_levels_run_one_to_six() {
2257 for level in 1..=6u8 {
2258 let heading = HeadingLevel::try_from(level).expect("1-6 is a heading level");
2259 assert_eq!(u8::from(heading), level);
2260 }
2261 for outside in [0u8, 7, 255] {
2262 assert_eq!(
2263 HeadingLevel::try_from(outside),
2264 Err(InvalidHeadingLevel(outside)),
2265 );
2266 }
2267 }
2268
2269 #[test]
2272 fn node_ids_are_dense_pre_order_and_deterministic() {
2273 let mut book = sample_book();
2274 book.assign_node_ids();
2275 let ids = collect_ids(&book);
2276
2277 let mut sorted = ids.clone();
2279 sorted.sort_by_key(|id| id.get());
2280 sorted.dedup();
2281 assert_eq!(sorted.len(), ids.len());
2282 assert_eq!(sorted.first().unwrap().get(), 1);
2283 assert_eq!(sorted.last().unwrap().get(), ids.len() as u32);
2284
2285 let section = book.sections[0].id;
2288 let Block::Heading { id: heading, .. } = &book.sections[0].blocks[0] else {
2289 panic!("fixture starts with a heading");
2290 };
2291 let Block::Paragraph {
2292 id: paragraph,
2293 inlines,
2294 ..
2295 } = &book.sections[0].blocks[1]
2296 else {
2297 panic!("second block is a paragraph");
2298 };
2299 assert!(section.get() < heading.get());
2300 assert!(heading.get() < paragraph.get());
2301 let Inline::Emphasis {
2302 id: emphasis,
2303 children,
2304 ..
2305 } = &inlines[1]
2306 else {
2307 panic!("second inline is emphasis");
2308 };
2309 let Inline::Text { id: child, .. } = &children[0] else {
2310 panic!("emphasis child is text");
2311 };
2312 assert!(paragraph.get() < emphasis.get());
2313 assert!(emphasis.get() < child.get());
2314
2315 let first = collect_ids(&book);
2318 book.assign_node_ids();
2319 assert_eq!(first, collect_ids(&book));
2320 }
2321
2322 #[test]
2326 fn a_byte_answers_with_the_innermost_node_written_there() {
2327 let mut book = sample_book();
2328 book.assign_node_ids();
2329
2330 let letter = SOURCE.find("clean").unwrap() as u32;
2331 let Some(Inline::Emphasis {
2332 id: emphasis,
2333 children,
2334 ..
2335 }) = paragraph(&book).get(1)
2336 else {
2337 panic!("the sample paragraph holds an emphasis");
2338 };
2339 assert_eq!(
2340 book.node_at("chapter-01.md", letter),
2341 Some(inline_id(&children[0]))
2342 );
2343 assert_eq!(book.node_at("chapter-01.md", letter - 1), Some(*emphasis));
2346 let blank = SOURCE.find("\n\n").unwrap() as u32 + 1;
2349 assert_eq!(
2350 book.node_at("chapter-01.md", blank),
2351 Some(book.sections[0].id)
2352 );
2353 assert_eq!(book.node_at("chapter-01.md", SOURCE.len() as u32), None);
2354 }
2355
2356 #[test]
2360 fn a_question_is_answered_from_one_source() {
2361 let mut book = sample_book();
2362 let mut second = book.sections[0].clone();
2363 second.source = Some("chapter-02.md".into());
2364 book.sections.push(second);
2365 book.assign_node_ids();
2366
2367 let letter = SOURCE.find("clean").unwrap() as u32;
2368 let first = book
2369 .node_at("chapter-01.md", letter)
2370 .expect("the first chapter");
2371 let second = book.node_at("chapter-02.md", letter).expect("the second");
2372 assert_ne!(first, second);
2373 assert_eq!(
2374 book.source_of(first).map(|(name, _)| name),
2375 Some("chapter-01.md")
2376 );
2377 assert_eq!(
2378 book.source_of(second).map(|(name, _)| name),
2379 Some("chapter-02.md")
2380 );
2381 assert_eq!(book.node_at("chapter-03.md", letter), None);
2382 }
2383
2384 #[test]
2387 fn a_node_taken_to_its_source_and_back_is_the_same_node() {
2388 let mut book = sample_book();
2389 book.assign_node_ids();
2390
2391 for id in collect_ids(&book) {
2392 let (source, span) = book
2393 .source_of(id)
2394 .expect("a parsed node was read from a file");
2395 assert_eq!(source, "chapter-01.md");
2396 assert!(
2397 span.end as usize <= SOURCE.len(),
2398 "node {} covers {span:?}, which is past the source",
2399 id.get(),
2400 );
2401 let there = book
2402 .node_at(source, span.start)
2403 .expect("a node was read there");
2404 let (_, back) = book.source_of(there).expect("and it says so");
2405 assert_eq!(back.start, span.start, "node {} starts elsewhere", id.get());
2406 }
2407 }
2408
2409 #[test]
2412 fn a_tree_built_rather_than_parsed_answers_with_nothing() {
2413 let mut book = Book {
2414 metadata: Metadata::default(),
2415 sections: vec![Section {
2416 attributes: Default::default(),
2417 source: Some("chapter-01.md".into()),
2418 blocks: vec![Block::Paragraph {
2419 id: NodeId::UNASSIGNED,
2420 inlines: vec![Inline::Text {
2421 id: NodeId::UNASSIGNED,
2422 value: "Built by hand.".into(),
2423 attributes: Attributes::default(),
2424 position: None,
2425 span: None,
2426 }],
2427 attributes: Attributes::default(),
2428 position: None,
2429 span: None,
2430 }],
2431 ..Section::default()
2432 }],
2433 };
2434 book.assign_node_ids();
2435
2436 assert_eq!(book.node_at("chapter-01.md", 0), None);
2437 for id in collect_ids(&book) {
2438 assert_eq!(
2439 book.source_of(id),
2440 None,
2441 "node {} was read from nothing",
2442 id.get()
2443 );
2444 }
2445 assert_eq!(book.source_of(NodeId::UNASSIGNED), None);
2446 }
2447
2448 #[test]
2453 fn a_subtree_is_a_run_of_ids() {
2454 let mut book = sample_book();
2455 book.assign_node_ids();
2456 let ids = collect_ids(&book);
2457 let last = ids.last().expect("the sample book has nodes").get();
2458
2459 let section = book.sections[0].id;
2460 assert_eq!(book.subtree(section), Some(section.get()..last + 1));
2461
2462 let quote = book.sections[0].blocks[2].clone();
2463 let Block::Blockquote { id, blocks, .. } = "e else {
2464 panic!("the third block is a blockquote");
2465 };
2466 let quoted = block_id(&blocks[0]);
2467 assert_eq!(book.subtree(*id), Some(id.get()..quoted.get() + 2));
2468
2469 for id in ids {
2470 let held = book.subtree(id).expect("the book holds it");
2471 assert_eq!(held.start, id.get(), "a node opens its own run");
2472 assert!(held.end <= last + 1, "a run stops inside the book");
2473 }
2474 }
2475
2476 #[test]
2479 fn an_id_the_book_does_not_hold_covers_nothing() {
2480 let mut book = sample_book();
2481 book.assign_node_ids();
2482 let past = collect_ids(&book).last().expect("nodes").get() + 1;
2483
2484 assert_eq!(book.subtree(NodeId::UNASSIGNED), None);
2485 assert_eq!(book.subtree(NodeId::new(past)), None);
2486 assert_eq!(Book::default().subtree(NodeId::new(1)), None);
2487 }
2488
2489 fn paragraph(book: &Book) -> &[Inline] {
2491 let Block::Paragraph { inlines, .. } = &book.sections[0].blocks[1] else {
2492 panic!("the second block is a paragraph");
2493 };
2494 inlines
2495 }
2496
2497 #[test]
2500 fn a_break_serializes_under_its_own_tag() {
2501 let page = Block::PageBreak {
2502 id: NodeId::UNASSIGNED,
2503 attributes: Attributes::default(),
2504 position: None,
2505 span: None,
2506 };
2507 let column = Block::ColumnBreak {
2508 id: NodeId::UNASSIGNED,
2509 attributes: Attributes::default(),
2510 position: None,
2511 span: None,
2512 };
2513 assert_eq!(
2514 serde_json::to_value(&page).expect("a break serializes"),
2515 serde_json::json!({ "type": "page_break" })
2516 );
2517 assert_eq!(
2518 serde_json::to_value(&column).expect("a break serializes"),
2519 serde_json::json!({ "type": "column_break" })
2520 );
2521 }
2522
2523 #[test]
2525 fn origin_formats_file_line_column() {
2526 let pos = SourcePos {
2527 line: 12,
2528 column: 3,
2529 };
2530 assert_eq!(
2531 origin(Some("chapter-01.md"), Some(pos)),
2532 "chapter-01.md:12:3"
2533 );
2534 assert_eq!(origin(Some("chapter-01.md"), None), "chapter-01.md");
2535 assert_eq!(origin(None, Some(pos)), "12:3");
2536 assert_eq!(origin(None, None), "");
2537 }
2538
2539 #[test]
2544 fn a_source_reports_the_names_its_blocks_and_inlines_carry() {
2545 let text = |value: &str| serde_json::json!({"type": "text", "value": value});
2546 let book: Book = serde_json::from_value(serde_json::json!({
2547 "metadata": {},
2548 "sections": [
2549 {
2550 "source": "one.md",
2551 "attributes": {"id": "front", "classes": ["chapter"]},
2552 "blocks": [
2553 {"type": "heading", "level": 1, "inlines": [text("One")],
2554 "attributes": {"id": "ch1", "classes": ["opening"]}},
2555 {"type": "paragraph", "inlines": [
2556 {"type": "span", "children": [text("aside")],
2557 "attributes": {"classes": ["smallcaps"]}},
2558 {"type": "note", "blocks": [
2559 {"type": "paragraph", "inlines": [text("n")],
2560 "attributes": {"classes": ["gloss"]}}
2561 ]}
2562 ]},
2563 {"type": "list", "items": [
2564 {"blocks": [], "attributes": {"id": "first-item", "classes": ["opening"]}}
2565 ]},
2566 {"type": "table", "body": [
2567 {"cells": [{"blocks": [], "attributes": {"classes": ["total"]}}],
2568 "attributes": {"classes": ["sum"]}}
2569 ]}
2570 ]
2571 },
2572 {
2573 "source": "two.md",
2574 "blocks": [
2575 {"type": "blockquote", "blocks": [], "attributes": {"classes": ["epigraph"]}}
2576 ]
2577 }
2578 ]
2579 }))
2580 .unwrap();
2581
2582 let one = book.names(Some("one.md"));
2583 assert_eq!(
2584 one.classes,
2585 ["gloss", "opening", "smallcaps", "sum", "total"]
2586 );
2587 assert_eq!(one.ids, ["ch1", "first-item"]);
2588 assert_eq!(book.names(Some("two.md")).classes, ["epigraph"]);
2589 assert_eq!(book.names(Some("three.md")), Names::default());
2590 let all = book.names(None);
2591 assert_eq!(
2592 all.classes,
2593 ["epigraph", "gloss", "opening", "smallcaps", "sum", "total"]
2594 );
2595 assert_eq!(all.ids, ["ch1", "first-item"]);
2596 }
2597}