use uzor_text::Paragraph;
use crate::compose::keep_break::BreakControl;
use crate::master::page_master::HeaderPlaceholder;
use crate::scene::figure_block::FigureBlock;
use crate::scene::footnote::Footnote;
use crate::scene::image_block::ImageBlock;
use crate::scene::island::AnchoredIsland;
use crate::scene::list::ListBlock;
use crate::scene::table::TableBlock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BlockId(pub u64);
impl BlockId {
const STRUCTURAL_TAG: u64 = 1 << 63;
pub fn structural(ordinal: u64) -> Self {
BlockId(Self::STRUCTURAL_TAG | ordinal)
}
pub fn is_structural(&self) -> bool {
self.0 & Self::STRUCTURAL_TAG != 0
}
}
pub enum Block<'a> {
Paragraph(Paragraph<'a>),
Figure(FigureBlock<'a>),
Image(ImageBlock<'a>),
Island(AnchoredIsland<'a>),
Table(TableBlock<'a>),
List(ListBlock<'a>),
Spacer(f64),
}
#[derive(Debug, Clone, PartialEq)]
pub struct OutlineTag {
pub level: u8,
pub title: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CaptionKind {
Figure,
Table,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Caption {
pub kind: CaptionKind,
pub text: String,
}
pub struct BlockNode<'a> {
pub id: Option<BlockId>,
pub kind: Block<'a>,
pub break_control: BreakControl,
pub outline: Option<OutlineTag>,
pub link_target: Option<u32>,
pub caption: Option<Caption>,
pub footnotes: &'a [Footnote<'a>],
pub header_placeholder: Option<HeaderPlaceholder>,
}
impl<'a> BlockNode<'a> {
pub fn new(kind: Block<'a>) -> Self {
Self {
id: None,
kind,
break_control: BreakControl::Auto,
outline: None,
link_target: None,
caption: None,
footnotes: &[],
header_placeholder: None,
}
}
pub fn with_id(mut self, id: BlockId) -> Self {
self.id = Some(id);
self
}
pub fn with_break_control(mut self, break_control: BreakControl) -> Self {
self.break_control = break_control;
self
}
pub fn with_outline(mut self, level: u8, title: impl Into<String>) -> Self {
self.outline = Some(OutlineTag { level, title: title.into() });
self
}
pub fn with_link_target(mut self, page_index: u32) -> Self {
self.link_target = Some(page_index);
self
}
pub fn with_caption(mut self, kind: CaptionKind, text: impl Into<String>) -> Self {
self.caption = Some(Caption { kind, text: text.into() });
self
}
pub fn with_footnotes(mut self, footnotes: &'a [Footnote<'a>]) -> Self {
self.footnotes = footnotes;
self
}
pub fn with_header_placeholder(mut self, placeholder: HeaderPlaceholder) -> Self {
self.header_placeholder = Some(placeholder);
self
}
}
pub fn resolve_block_ids(flow: &[BlockNode<'_>]) -> Vec<BlockId> {
let mut next_ordinal = 0u64;
flow.iter()
.map(|node| match node.id {
Some(id) => id,
None => {
let id = BlockId::structural(next_ordinal);
next_ordinal += 1;
id
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use uzor::fonts::FontFamily;
use uzor_text::{FontSpec, StyledRun};
fn spacer(gap: f64) -> BlockNode<'static> {
BlockNode::new(Block::Spacer(gap))
}
#[test]
fn structural_ids_never_collide_with_the_author_id_space() {
let author = BlockId(3);
let structural = BlockId::structural(3);
assert_ne!(author, structural);
assert!(!author.is_structural());
assert!(structural.is_structural());
}
#[test]
fn author_id_is_preserved_verbatim() {
let flow = [spacer(1.0).with_id(BlockId(42)), spacer(2.0)];
let ids = resolve_block_ids(&flow);
assert_eq!(ids[0], BlockId(42));
assert!(ids[1].is_structural());
}
#[test]
fn unlabeled_blocks_get_stable_structural_ordinals_across_two_runs() {
let font = FontSpec::new(FontFamily::Roboto, 16.0);
let runs = [StyledRun::new("hello", font)];
let flow = [
BlockNode::new(Block::Paragraph(Paragraph::new(&runs, 200.0))).with_id(BlockId(7)),
spacer(4.0),
spacer(8.0),
];
let first_run = resolve_block_ids(&flow);
let second_run = resolve_block_ids(&flow);
assert_eq!(first_run, second_run, "same tree shape must resolve to the same ids every compose run");
assert_eq!(first_run[0], BlockId(7), "author id preserved");
assert_eq!(first_run[1], BlockId::structural(0));
assert_eq!(first_run[2], BlockId::structural(1));
}
}