use super::NodeId;
use crate::units::Pt;
#[derive(Debug, Clone, PartialEq)]
pub struct TextContent {
pub text: String,
}
impl TextContent {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ImageFormat {
Jpeg,
Png,
Webp,
}
#[derive(Debug, Clone)]
pub struct ImageContent {
pub data: Vec<u8>,
pub format: ImageFormat,
pub intrinsic_width: Option<Pt>,
pub intrinsic_height: Option<Pt>,
pub alt_text: Option<String>,
}
impl ImageContent {
#[must_use]
pub fn new(data: Vec<u8>, format: ImageFormat) -> Self {
Self {
data,
format,
intrinsic_width: None,
intrinsic_height: None,
alt_text: None,
}
}
#[must_use]
pub fn with_dimensions(data: Vec<u8>, format: ImageFormat, width: Pt, height: Pt) -> Self {
Self {
data,
format,
intrinsic_width: Some(width),
intrinsic_height: Some(height),
alt_text: None,
}
}
#[must_use]
pub fn with_alt_text(mut self, alt: impl Into<String>) -> Self {
self.alt_text = Some(alt.into());
self
}
}
#[derive(Debug, Clone)]
pub struct SvgContent {
pub data: Vec<u8>,
pub intrinsic_width: Option<Pt>,
pub intrinsic_height: Option<Pt>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MathFormat {
MathMl,
Latex,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MathContent {
pub markup: String,
pub format: MathFormat,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LinkTarget {
External(String),
Internal(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct LinkContent {
pub target: LinkTarget,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum FormField {
TextInput {
name: String,
default_value: Option<String>,
},
Checkbox {
name: String,
checked: bool,
},
Radio {
name: String,
value: String,
selected: bool,
},
Dropdown {
name: String,
options: Vec<String>,
selected: Option<usize>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct FormContent {
pub field: FormField,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IndexEntryContent {
pub term: String,
pub sort_key: Option<String>,
pub see_also: Option<String>,
}
impl IndexEntryContent {
#[must_use]
pub fn new(term: impl Into<String>) -> Self {
Self {
term: term.into(),
sort_key: None,
see_also: None,
}
}
#[must_use]
pub fn with_sort_key(mut self, key: impl Into<String>) -> Self {
self.sort_key = Some(key.into());
self
}
#[must_use]
pub fn with_see_also(mut self, term: impl Into<String>) -> Self {
self.see_also = Some(term.into());
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FootnoteContent {
pub marker: Option<String>,
}
impl FootnoteContent {
#[must_use]
pub fn auto() -> Self {
Self { marker: None }
}
#[must_use]
pub fn with_marker(marker: impl Into<String>) -> Self {
Self {
marker: Some(marker.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FootnoteConfig {
pub separator_width: Pt,
pub separator_fraction: f64,
pub separator_gap: Pt,
pub footnote_gap: Pt,
pub numbering_style: FootnoteNumberingStyle,
pub position: FootnotePosition,
}
impl Default for FootnoteConfig {
fn default() -> Self {
Self {
separator_width: Pt::new(0.5),
separator_fraction: 0.33,
separator_gap: Pt::new(6.0),
footnote_gap: Pt::new(4.0),
numbering_style: FootnoteNumberingStyle::Numeric,
position: FootnotePosition::PageBottom,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FootnoteNumberingStyle {
#[default]
Numeric,
AlphaLower,
AlphaUpper,
Symbol,
}
impl FootnoteNumberingStyle {
#[must_use]
pub fn format(&self, n: u32) -> String {
match self {
Self::Numeric => n.to_string(),
Self::AlphaLower => format_alpha(n, b'a'),
Self::AlphaUpper => format_alpha(n, b'A'),
Self::Symbol => format_symbol(n),
}
}
}
fn format_alpha(n: u32, base: u8) -> String {
if n == 0 {
return String::new();
}
let mut result = String::new();
let mut val = n - 1;
loop {
result.insert(0, (base + (val % 26) as u8) as char);
if val < 26 {
break;
}
val = val / 26 - 1;
}
result
}
fn format_symbol(n: u32) -> String {
const SYMBOLS: [char; 6] = ['*', '†', '‡', '§', '‖', '¶'];
if n == 0 {
return String::new();
}
let idx = ((n - 1) % 6) as usize;
let repeat = ((n - 1) / 6) as usize + 1;
SYMBOLS[idx].to_string().repeat(repeat)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FootnotePosition {
#[default]
PageBottom,
DocumentEnd,
SectionEnd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BorderCollapse {
#[default]
Separate,
Collapse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TableLayoutMode {
#[default]
Auto,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TableColumnWidth {
Auto,
Fixed(Pt),
Percent(f64),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TableColumn {
pub width: TableColumnWidth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TableRowGroupKind {
Header,
Body,
Footer,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableRowGroup {
pub kind: TableRowGroupKind,
pub rows: Vec<TableRow>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableRow {
pub cells: Vec<TableCell>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableCell {
pub content_node: NodeId,
pub colspan: u32,
pub rowspan: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableContent {
pub columns: Vec<TableColumn>,
pub row_groups: Vec<TableRowGroup>,
pub border_collapse: BorderCollapse,
pub cell_spacing_h: Pt,
pub cell_spacing_v: Pt,
pub table_layout: TableLayoutMode,
}
impl TableContent {
pub fn all_rows(&self) -> impl Iterator<Item = &TableRow> {
self.row_groups.iter().flat_map(|rg| rg.rows.iter())
}
pub fn total_rows(&self) -> usize {
self.row_groups.iter().map(|rg| rg.rows.len()).sum()
}
pub fn header_rows(&self) -> impl Iterator<Item = &TableRow> {
self.row_groups
.iter()
.filter(|rg| rg.kind == TableRowGroupKind::Header)
.flat_map(|rg| rg.rows.iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_content_creation() {
let tc = TextContent::new("Hello, world!");
assert_eq!(tc.text, "Hello, world!");
}
#[test]
fn link_target_variants() {
let ext = LinkTarget::External("https://example.com".into());
let int = LinkTarget::Internal("section-1".into());
assert_ne!(ext, int);
}
}