1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
//! Core style types: ComputedStyle and StyleId.
use super::properties::*;
/// Unique identifier for a style in the StylePool.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct StyleId(
/// Index into the owning `StylePool`; 0 is the default style.
pub u32,
);
impl StyleId {
/// The default style (always 0).
pub const DEFAULT: StyleId = StyleId(0);
}
/// Computed style for a node (all properties resolved).
///
/// One flat struct with every property boko tracks after the cascade.
/// Enum-typed and `Length` fields use their `Default` (usually the CSS
/// initial value; `Length::Auto` for lengths); `Option` fields are `None`
/// when the property was never set, letting exporters skip emitting them.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct ComputedStyle {
// Font properties
/// `font-family`; `None` means inherit the reader default.
pub font_family: Option<String>,
/// `font-size`; `Length::Auto` means unset (reader default size).
pub font_size: Length,
/// `font-weight` as a numeric weight (default 0 means unset; 400 normal, 700 bold).
pub font_weight: FontWeight,
/// `font-style` (normal, italic, oblique).
pub font_style: FontStyle,
// Colors
/// `color` (text foreground); `None` means unset.
pub color: Option<Color>,
/// `background-color`; `None` means unset (transparent).
pub background_color: Option<Color>,
// Text
/// `text-align` for block content.
pub text_align: TextAlign,
/// `text-indent` for the first line; `Length::Auto` means unset.
pub text_indent: Length,
/// `line-height`; unitless values are stored as em, `Auto` means unset.
pub line_height: Length,
/// Whether `text-decoration` includes an underline line.
pub text_decoration_underline: bool,
/// Whether `text-decoration` includes a line-through (strikethrough) line.
pub text_decoration_line_through: bool,
// Box model
/// `display` box mode (block, inline, none, list-item, ...).
pub display: Display,
/// `margin-top`; `Length::Auto` means unset.
pub margin_top: Length,
/// `margin-bottom`; `Length::Auto` means unset.
pub margin_bottom: Length,
/// `margin-left`; `Length::Auto` means unset.
pub margin_left: Length,
/// `margin-right`; `Length::Auto` means unset.
pub margin_right: Length,
/// `padding-top`; `Length::Auto` means unset.
pub padding_top: Length,
/// `padding-bottom`; `Length::Auto` means unset.
pub padding_bottom: Length,
/// `padding-left`; `Length::Auto` means unset.
pub padding_left: Length,
/// `padding-right`; `Length::Auto` means unset.
pub padding_right: Length,
/// `vertical-align` for inline and table-cell elements; `Super`/`Sub`
/// drive superscript/subscript detection in exporters.
pub vertical_align: VerticalAlign,
/// `list-style-type` (marker kind for list items).
pub list_style_type: ListStyleType,
/// `font-variant` (normal or small-caps).
pub font_variant: FontVariant,
// Text spacing
/// `letter-spacing`; `Length::Auto` means unset.
pub letter_spacing: Length,
/// `word-spacing`; `Length::Auto` means unset.
pub word_spacing: Length,
/// `text-transform` case transformation.
pub text_transform: TextTransform,
/// `hyphens` automatic hyphenation mode (defaults to `Manual`).
pub hyphens: Hyphens,
/// `white-space` collapsing/wrapping behavior.
pub white_space: WhiteSpace,
// Text decoration extensions
/// `text-decoration-style` for the underline line (solid, dotted, ...).
pub underline_style: DecorationStyle,
/// Whether `text-decoration` includes an overline line.
pub overline: bool,
/// `text-decoration-color`; `None` means the current text color.
pub underline_color: Option<Color>,
// Layout properties
/// `width`; `Length::Auto` means unset.
pub width: Length,
/// `height`; `Length::Auto` means unset.
pub height: Length,
/// `max-width`; `Length::Auto` means unset.
pub max_width: Length,
/// `min-height`; `Length::Auto` means unset.
pub min_height: Length,
/// `float` positioning (none, left, right).
pub float: Float,
// Page break properties
/// `break-before` / `page-break-before`.
pub break_before: BreakValue,
/// `break-after` / `page-break-after`.
pub break_after: BreakValue,
/// `break-inside` / `page-break-inside`.
pub break_inside: BreakValue,
// Border properties (4 sides)
/// `border-top-style`.
pub border_style_top: BorderStyle,
/// `border-right-style`.
pub border_style_right: BorderStyle,
/// `border-bottom-style`.
pub border_style_bottom: BorderStyle,
/// `border-left-style`.
pub border_style_left: BorderStyle,
/// `border-top-width`; `Length::Auto` means unset.
pub border_width_top: Length,
/// `border-right-width`; `Length::Auto` means unset.
pub border_width_right: Length,
/// `border-bottom-width`; `Length::Auto` means unset.
pub border_width_bottom: Length,
/// `border-left-width`; `Length::Auto` means unset.
pub border_width_left: Length,
/// `border-top-color`; `None` means the current text color.
pub border_color_top: Option<Color>,
/// `border-right-color`; `None` means the current text color.
pub border_color_right: Option<Color>,
/// `border-bottom-color`; `None` means the current text color.
pub border_color_bottom: Option<Color>,
/// `border-left-color`; `None` means the current text color.
pub border_color_left: Option<Color>,
// Border radius (corners)
/// `border-top-left-radius`; `Length::Auto` means unset.
pub border_radius_top_left: Length,
/// `border-top-right-radius`; `Length::Auto` means unset.
pub border_radius_top_right: Length,
/// `border-bottom-left-radius`; `Length::Auto` means unset.
pub border_radius_bottom_left: Length,
/// `border-bottom-right-radius`; `Length::Auto` means unset.
pub border_radius_bottom_right: Length,
/// `list-style-position` (marker inside or outside the item box).
pub list_style_position: ListStylePosition,
// Language & rendering
/// Content language (from `xml:lang`/`lang` attributes, not CSS); used by
/// KFX export for hyphenation dictionaries. `None` means unset.
pub language: Option<String>,
/// `visibility` (visible, hidden, collapse).
pub visibility: Visibility,
/// `box-sizing` (content-box or border-box).
pub box_sizing: BoxSizing,
// Additional layout properties
/// `max-height`; `Length::Auto` means unset.
pub max_height: Length,
/// `min-width`; `Length::Auto` means unset.
pub min_width: Length,
/// `clear` (which floated sides following content must clear).
pub clear: Clear,
// Pagination control
/// `orphans`: minimum lines kept at the bottom of a page (0 = unset).
pub orphans: u32,
/// `widows`: minimum lines carried to the next page (0 = unset).
pub widows: u32,
// Text wrapping
/// `word-break` (where lines may break within words).
pub word_break: WordBreak,
/// `overflow-wrap` (emergency breaking of long words).
pub overflow_wrap: OverflowWrap,
// Table properties
/// `border-collapse` for tables (separate or collapse).
pub border_collapse: BorderCollapse,
/// `border-spacing` between table cells; `Length::Auto` means unset.
pub border_spacing: Length,
}
impl ComputedStyle {
/// Check if this style differs from the default (has any non-default properties).
pub fn is_default(&self) -> bool {
*self == ComputedStyle::default()
}
// --- Modifier checks for exporters ---
/// Check if the style is bold (font-weight >= 700).
#[inline]
pub fn is_bold(&self) -> bool {
self.font_weight.0 >= 700
}
/// Check if the style is italic.
#[inline]
pub fn is_italic(&self) -> bool {
matches!(self.font_style, FontStyle::Italic | FontStyle::Oblique)
}
/// Check if the style has underline decoration.
#[inline]
pub fn is_underline(&self) -> bool {
self.text_decoration_underline
}
/// Check if the style has strikethrough decoration.
#[inline]
pub fn is_strikethrough(&self) -> bool {
self.text_decoration_line_through
}
/// Check if the style is superscript.
#[inline]
pub fn is_superscript(&self) -> bool {
self.vertical_align == VerticalAlign::Super
}
/// Check if the style is subscript.
#[inline]
pub fn is_subscript(&self) -> bool {
self.vertical_align == VerticalAlign::Sub
}
/// Check if the style uses a monospace font.
pub fn is_monospace(&self) -> bool {
self.font_family
.as_ref()
.map(|f| {
let lower = f.to_lowercase();
lower.contains("mono")
|| lower.contains("courier")
|| lower.contains("consolas")
|| lower.contains("menlo")
})
.unwrap_or(false)
}
/// Check if the list style is ordered (numbered).
pub fn is_ordered_list(&self) -> bool {
matches!(
self.list_style_type,
ListStyleType::Decimal
| ListStyleType::LowerAlpha
| ListStyleType::UpperAlpha
| ListStyleType::LowerRoman
| ListStyleType::UpperRoman
)
}
/// Check if the style uses small-caps font variant.
#[inline]
pub fn is_small_caps(&self) -> bool {
matches!(self.font_variant, FontVariant::SmallCaps)
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::style::ToCss;
#[test]
fn test_color_to_css_opaque() {
assert_eq!(Color::BLACK.to_css_string(), "#000000");
assert_eq!(Color::WHITE.to_css_string(), "#ffffff");
assert_eq!(Color::rgb(255, 0, 0).to_css_string(), "#ff0000");
assert_eq!(Color::rgb(0, 128, 255).to_css_string(), "#0080ff");
}
#[test]
fn test_color_to_css_transparent() {
assert_eq!(Color::TRANSPARENT.to_css_string(), "transparent");
}
#[test]
fn test_color_to_css_alpha() {
let color = Color::rgba(255, 0, 0, 128);
let css = color.to_css_string();
assert!(css.starts_with("rgba(255,0,0,"));
assert!(css.contains("0.50")); // ~128/255
}
#[test]
fn test_length_to_css() {
assert_eq!(Length::Auto.to_css_string(), "auto");
assert_eq!(Length::Px(0.0).to_css_string(), "0");
assert_eq!(Length::Px(16.0).to_css_string(), "16px");
assert_eq!(Length::Em(1.5).to_css_string(), "1.5em");
assert_eq!(Length::Rem(2.0).to_css_string(), "2rem");
assert_eq!(Length::Percent(50.0).to_css_string(), "50%");
}
#[test]
fn test_font_weight_to_css() {
assert_eq!(FontWeight::NORMAL.to_css_string(), "normal");
assert_eq!(FontWeight::BOLD.to_css_string(), "bold");
assert_eq!(FontWeight(300).to_css_string(), "300");
assert_eq!(FontWeight(600).to_css_string(), "600");
}
#[test]
fn test_font_style_to_css() {
assert_eq!(FontStyle::Normal.to_css_string(), "normal");
assert_eq!(FontStyle::Italic.to_css_string(), "italic");
assert_eq!(FontStyle::Oblique.to_css_string(), "oblique");
}
#[test]
fn test_text_align_to_css() {
assert_eq!(TextAlign::Left.to_css_string(), "left");
assert_eq!(TextAlign::Center.to_css_string(), "center");
assert_eq!(TextAlign::Justify.to_css_string(), "justify");
}
#[test]
fn test_display_to_css() {
assert_eq!(Display::Block.to_css_string(), "block");
assert_eq!(Display::Inline.to_css_string(), "inline");
assert_eq!(Display::None.to_css_string(), "none");
}
#[test]
fn test_computed_style_to_css_default() {
let style = ComputedStyle::default();
// Default style should produce empty CSS (no non-default properties)
assert_eq!(style.to_css_string(), "");
}
#[test]
fn test_computed_style_to_css_bold() {
let mut style = ComputedStyle::default();
style.font_weight = FontWeight::BOLD;
let css = style.to_css_string();
assert!(css.contains("font-weight: bold;"));
}
#[test]
fn test_computed_style_to_css_multiple() {
let mut style = ComputedStyle::default();
style.font_weight = FontWeight::BOLD;
style.font_style = FontStyle::Italic;
style.color = Some(Color::rgb(255, 0, 0));
style.text_align = TextAlign::Center;
let css = style.to_css_string();
assert!(css.contains("font-weight: bold;"));
assert!(css.contains("font-style: italic;"));
assert!(css.contains("color: #ff0000;"));
assert!(css.contains("text-align: center;"));
}
#[test]
fn test_computed_style_to_css_decorations() {
let mut style = ComputedStyle::default();
style.text_decoration_underline = true;
style.text_decoration_line_through = true;
let css = style.to_css_string();
assert!(css.contains("text-decoration: underline line-through;"));
}
}