normordis_pdf/elements/
section.rs1use serde::{Deserialize, Serialize};
2
3use super::{Element, RenderContext};
4use crate::{compliance::ua::StructTag, styles::StyleResolver};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Section {
16 pub title: String,
17 pub level: u8,
19 #[serde(default)]
22 pub style_ref: Option<String>,
23}
24
25impl Section {
26 pub fn new(title: impl Into<String>, level: u8) -> Self {
27 Self {
28 title: title.into(),
29 level: level.clamp(1, 3),
30 style_ref: None,
31 }
32 }
33
34 pub fn style(mut self, name: impl Into<String>) -> Self {
36 self.style_ref = Some(name.into());
37 self
38 }
39
40 fn default_style_name(&self) -> &'static str {
41 match self.level {
42 1 => "heading_1",
43 2 => "heading_2",
44 _ => "heading_3",
45 }
46 }
47}
48
49impl Section {
50 pub fn level(&self) -> u8 {
52 self.level
53 }
54
55 pub fn heading_text(&self) -> &str {
57 &self.title
58 }
59}
60
61impl Element for Section {
62 fn as_section_info(&self) -> Option<(u8, &str)> {
63 Some((self.level, &self.title))
64 }
65
66 fn estimated_height_mm(&self) -> f64 {
67 let pre = match self.level { 1 => 8.0, 2 => 6.0, _ => 4.0 };
68 let post = match self.level { 1 => 4.0, 2 => 3.0, _ => 2.0 };
69 pre + 7.0 + post
70 }
71
72 fn render(&self, ctx: &mut RenderContext) -> crate::Result<super::RenderResult> {
73 let style_name = self.style_ref.as_deref()
74 .unwrap_or_else(|| self.default_style_name());
75
76 let resolver = StyleResolver::new(&ctx.style.named_styles, &ctx.style);
77 let resolved = resolver.resolve(style_name)?;
78
79 let fs = resolved.font_size;
80
81 if resolved.space_before_mm > 0.0 && !ctx.flow.is_top_of_page() {
82 ctx.flow.advance(resolved.space_before_mm);
83 }
84
85 let color = resolved.color.clone()
86 .unwrap_or_else(|| ctx.style.text_color.clone());
87
88 let Some(font_ref) = ctx.get_font_ref(resolved.bold, resolved.italic) else {
89 ctx.flow.advance(self.estimated_height_mm() - resolved.space_before_mm);
90 return Ok(super::RenderResult::done());
91 };
92
93 let ua_tag = match self.level {
95 1 => StructTag::H1,
96 2 => StructTag::H2,
97 3 => StructTag::H3,
98 4 => StructTag::H4,
99 5 => StructTag::H5,
100 _ => StructTag::H6,
101 };
102 if ctx.ua_enabled() {
103 if let Some(prev) = ctx.last_heading_level {
104 if self.level > prev + 1 {
105 eprintln!(
106 "PDF/UA-2 WARNING: heading level skipped from H{} to H{}",
107 prev, self.level
108 );
109 }
110 }
111 ctx.last_heading_level = Some(self.level);
112 }
113
114 let mcid_opt = if ctx.ua_enabled() {
115 let mcid = ctx.ua_tag_element(ua_tag, None);
116 ctx.backend.begin_tagged_content(
117 match self.level { 1 => b"H1", 2 => b"H2", 3 => b"H3", 4 => b"H4", 5 => b"H5", _ => b"H6" },
118 mcid,
119 );
120 Some(mcid)
121 } else {
122 None
123 };
124
125 let x = ctx.layout.content_x_mm;
126 let y = ctx.flow.cursor_y_mm;
127 ctx.draw_text(&self.title, x, y, fs, font_ref, &color)?;
128
129 let page_idx = ctx.backend.current_page_idx();
131 ctx.backend.add_outline_entry(&self.title, self.level, page_idx, y);
132
133 let line_h = ctx.layout_engine.line_height_mm(&ctx.fonts, fs);
134 ctx.flow.advance(line_h + resolved.space_after_mm);
135
136 if mcid_opt.is_some() {
137 ctx.backend.end_tagged_content();
138 }
139
140 Ok(super::RenderResult::done())
141 }
142}