use crate::gcpm::GcpmContext;
use crate::gcpm::ParsedSelector;
use crate::gcpm::running::{RunningElementStore, serialize_node};
use crate::pageable::{
BlockPageable, BlockStyle, BorderStyleValue, ListItemPageable, Pageable, PositionedChild, Size,
SpacerPageable, TablePageable,
};
use crate::paragraph::{
ParagraphPageable, ShapedGlyph, ShapedGlyphRun, ShapedLine, TextDecoration, TextDecorationLine,
TextDecorationStyle,
};
use blitz_dom::{Node, NodeData};
use blitz_html::HtmlDocument;
use std::ops::Deref;
use std::sync::Arc;
pub fn dom_to_pageable(
doc: &HtmlDocument,
gcpm: Option<&GcpmContext>,
running_store: &mut RunningElementStore,
) -> Box<dyn Pageable> {
let root = doc.root_element();
if std::env::var("FULGUR_DEBUG").is_ok() {
debug_print_tree(doc.deref(), root.id, 0);
}
convert_node(doc.deref(), root.id, gcpm, running_store)
}
fn debug_print_tree(doc: &blitz_dom::BaseDocument, node_id: usize, depth: usize) {
let Some(node) = doc.get_node(node_id) else {
return;
};
let layout = node.final_layout;
let indent = " ".repeat(depth);
let tag = match &node.data {
NodeData::Element(e) => e.name.local.to_string(),
NodeData::Text(_) => "#text".to_string(),
NodeData::Comment => "#comment".to_string(),
_ => "#other".to_string(),
};
eprintln!(
"{indent}{tag} id={} pos=({},{}) size={}x{} inline_root={}",
node_id,
layout.location.x,
layout.location.y,
layout.size.width,
layout.size.height,
node.flags.is_inline_root()
);
for &child_id in &node.children {
debug_print_tree(doc, child_id, depth + 1);
}
}
fn convert_node(
doc: &blitz_dom::BaseDocument,
node_id: usize,
gcpm: Option<&GcpmContext>,
running_store: &mut RunningElementStore,
) -> Box<dyn Pageable> {
let node = doc.get_node(node_id).unwrap();
let layout = node.final_layout;
let height = layout.size.height;
let width = layout.size.width;
if let Some(elem_data) = node.element_data()
&& elem_data.list_item_data.is_some()
{
let (marker_lines, marker_width) = extract_marker_lines(doc, node);
let style = extract_block_style(node);
let body: Box<dyn Pageable> = if node.flags.is_inline_root()
&& let Some(paragraph) = extract_paragraph(doc, node)
{
if style.has_visual_style() {
let (child_x, child_y) = style.content_inset();
let child = PositionedChild {
child: Box::new(paragraph),
x: child_x,
y: child_y,
};
let mut block =
BlockPageable::with_positioned_children(vec![child]).with_style(style);
block.wrap(width, height);
block.layout_size = Some(Size { width, height });
Box::new(block)
} else {
Box::new(paragraph)
}
} else {
let children: &[usize] = &node.children;
let positioned_children =
collect_positioned_children(doc, children, gcpm, running_store);
let mut block =
BlockPageable::with_positioned_children(positioned_children).with_style(style);
block.wrap(width, 10000.0);
Box::new(block)
};
let mut item = ListItemPageable {
marker_lines,
marker_width,
body,
style: BlockStyle::default(),
width,
height: 0.0,
};
item.wrap(width, 10000.0);
return Box::new(item);
}
if let Some(elem_data) = node.element_data() {
let tag = elem_data.name.local.as_ref();
if tag == "table" {
return convert_table(doc, node, gcpm, running_store);
}
}
if node.flags.is_inline_root()
&& let Some(paragraph) = extract_paragraph(doc, node)
{
let style = extract_block_style(node);
if style.has_visual_style() {
let (child_x, child_y) = style.content_inset();
let child = PositionedChild {
child: Box::new(paragraph),
x: child_x,
y: child_y,
};
let mut block = BlockPageable::with_positioned_children(vec![child]).with_style(style);
block.wrap(width, height);
block.layout_size = Some(Size { width, height });
return Box::new(block);
}
return Box::new(paragraph);
}
let children: &[usize] = &node.children;
if children.is_empty() {
let style = extract_block_style(node);
if style.has_visual_style() || style.has_radius() {
let mut block = BlockPageable::with_positioned_children(vec![]).with_style(style);
block.wrap(width, height);
block.layout_size = Some(Size { width, height });
return Box::new(block);
}
let mut spacer = SpacerPageable::new(height);
spacer.wrap(width, height);
return Box::new(spacer);
}
let positioned_children = collect_positioned_children(doc, children, gcpm, running_store);
let style = extract_block_style(node);
let mut block = BlockPageable::with_positioned_children(positioned_children).with_style(style);
block.wrap(width, 10000.0);
Box::new(block)
}
fn collect_positioned_children(
doc: &blitz_dom::BaseDocument,
child_ids: &[usize],
gcpm: Option<&GcpmContext>,
running_store: &mut RunningElementStore,
) -> Vec<PositionedChild> {
let mut result = Vec::new();
for &child_id in child_ids {
let Some(child_node) = doc.get_node(child_id) else {
continue;
};
if matches!(&child_node.data, NodeData::Comment) {
continue;
}
if is_non_visual_element(child_node) {
continue;
}
if let Some(ctx) = gcpm {
if is_running_element(child_node, ctx) {
let html = serialize_node(doc, child_id);
if let Some(name) = get_running_name(child_node, ctx) {
running_store.register(name, html);
}
continue;
}
}
let child_layout = child_node.final_layout;
if child_layout.size.height == 0.0
&& child_layout.size.width == 0.0
&& child_node.children.is_empty()
{
continue;
}
if child_layout.size.height == 0.0
&& child_layout.size.width == 0.0
&& !child_node.children.is_empty()
{
let nested =
collect_positioned_children(doc, &child_node.children, gcpm, running_store);
result.extend(nested);
continue;
}
let child_pageable = convert_node(doc, child_id, gcpm, running_store);
result.push(PositionedChild {
child: child_pageable,
x: child_layout.location.x,
y: child_layout.location.y,
});
}
result
}
fn is_running_element(node: &Node, ctx: &GcpmContext) -> bool {
if ctx.running_mappings.is_empty() {
return false;
}
let Some(elem) = node.element_data() else {
return false;
};
ctx.running_mappings
.iter()
.any(|m| matches_selector(&m.parsed, elem))
}
fn get_class_attr(elem: &blitz_dom::node::ElementData) -> Option<&str> {
elem.attrs()
.iter()
.find(|a| a.name.local.as_ref() == "class")
.map(|a| a.value.as_ref())
}
fn get_id_attr(elem: &blitz_dom::node::ElementData) -> Option<&str> {
elem.attrs()
.iter()
.find(|a| a.name.local.as_ref() == "id")
.map(|a| a.value.as_ref())
}
fn get_tag_name(elem: &blitz_dom::node::ElementData) -> &str {
elem.name.local.as_ref()
}
fn matches_selector(selector: &ParsedSelector, elem: &blitz_dom::node::ElementData) -> bool {
match selector {
ParsedSelector::Class(name) => get_class_attr(elem)
.map(|cls| cls.split_whitespace().any(|c| c == name))
.unwrap_or(false),
ParsedSelector::Id(name) => get_id_attr(elem).map(|id| id == name).unwrap_or(false),
ParsedSelector::Tag(name) => get_tag_name(elem).eq_ignore_ascii_case(name),
}
}
fn get_running_name(node: &Node, ctx: &GcpmContext) -> Option<String> {
let elem = node.element_data()?;
ctx.running_mappings
.iter()
.find(|m| matches_selector(&m.parsed, elem))
.map(|m| m.running_name.clone())
}
fn convert_table(
doc: &blitz_dom::BaseDocument,
node: &Node,
gcpm: Option<&GcpmContext>,
running_store: &mut RunningElementStore,
) -> Box<dyn Pageable> {
let layout = node.final_layout;
let width = layout.size.width;
let height = layout.size.height;
let style = extract_block_style(node);
let mut header_cells: Vec<PositionedChild> = Vec::new();
let mut body_cells: Vec<PositionedChild> = Vec::new();
for &child_id in &node.children {
let Some(child_node) = doc.get_node(child_id) else {
continue;
};
let is_thead = is_table_section(child_node, "thead");
collect_table_cells(
doc,
child_id,
is_thead,
&mut header_cells,
&mut body_cells,
gcpm,
running_store,
);
}
let header_height = header_cells
.iter()
.fold(0.0f32, |max_h, pc| max_h.max(pc.y + pc.child.height()));
let table = TablePageable {
header_cells,
body_cells,
header_height,
style,
layout_size: Some(Size { width, height }),
width,
cached_height: height,
};
Box::new(table)
}
fn is_table_section(node: &Node, section_name: &str) -> bool {
if let Some(elem) = node.element_data() {
elem.name.local.as_ref() == section_name
} else {
false
}
}
fn collect_table_cells(
doc: &blitz_dom::BaseDocument,
node_id: usize,
is_header: bool,
header_cells: &mut Vec<PositionedChild>,
body_cells: &mut Vec<PositionedChild>,
gcpm: Option<&GcpmContext>,
running_store: &mut RunningElementStore,
) {
let Some(node) = doc.get_node(node_id) else {
return;
};
for &child_id in &node.children {
let Some(child_node) = doc.get_node(child_id) else {
continue;
};
if matches!(&child_node.data, NodeData::Comment) {
continue;
}
if is_non_visual_element(child_node) {
continue;
}
if let Some(ctx) = gcpm {
if is_running_element(child_node, ctx) {
let html = serialize_node(doc, child_id);
if let Some(name) = get_running_name(child_node, ctx) {
running_store.register(name, html);
}
continue;
}
}
let child_layout = child_node.final_layout;
if child_layout.size.height == 0.0
&& child_layout.size.width == 0.0
&& !child_node.children.is_empty()
{
let child_is_header = is_header || is_table_section(child_node, "thead");
collect_table_cells(
doc,
child_id,
child_is_header,
header_cells,
body_cells,
gcpm,
running_store,
);
continue;
}
if child_layout.size.height == 0.0 && child_layout.size.width == 0.0 {
continue;
}
let cell_pageable = convert_node(doc, child_id, gcpm, running_store);
let positioned = PositionedChild {
child: cell_pageable,
x: child_layout.location.x,
y: child_layout.location.y,
};
if is_header {
header_cells.push(positioned);
} else {
body_cells.push(positioned);
}
}
}
fn extract_paragraph(doc: &blitz_dom::BaseDocument, node: &Node) -> Option<ParagraphPageable> {
let elem_data = node.element_data()?;
let text_layout = elem_data.inline_layout_data.as_ref()?;
let parley_layout = &text_layout.layout;
let text = &text_layout.text;
let mut shaped_lines = Vec::new();
for line in parley_layout.lines() {
let metrics = line.metrics();
let mut glyph_runs = Vec::new();
for item in line.items() {
if let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item {
let run = glyph_run.run();
let font_data = run.font();
let font_bytes: Vec<u8> = font_data.data.data().to_vec();
let font_index = font_data.index;
let font_size = run.font_size();
let brush = &glyph_run.style().brush;
let color = get_text_color(doc, brush.id);
let decoration = get_text_decoration(doc, brush.id);
let text_len = text.len();
let mut glyphs = Vec::new();
for g in glyph_run.glyphs() {
glyphs.push(ShapedGlyph {
id: g.id,
x_advance: g.advance / font_size,
x_offset: g.x / font_size,
y_offset: g.y / font_size,
text_range: 0..text_len,
});
}
if !glyphs.is_empty() {
let run_text = text.clone();
glyph_runs.push(ShapedGlyphRun {
font_data: Arc::new(font_bytes),
font_index,
font_size,
color,
decoration,
glyphs,
text: run_text,
x_offset: glyph_run.offset(),
});
}
}
}
shaped_lines.push(ShapedLine {
height: metrics.line_height,
baseline: metrics.baseline,
glyph_runs,
});
}
if shaped_lines.is_empty() {
return None;
}
Some(ParagraphPageable::new(shaped_lines))
}
fn extract_block_style(node: &Node) -> BlockStyle {
let layout = node.final_layout;
let mut style = BlockStyle {
border_widths: [
layout.border.top,
layout.border.right,
layout.border.bottom,
layout.border.left,
],
padding: [
layout.padding.top,
layout.padding.right,
layout.padding.bottom,
layout.padding.left,
],
..Default::default()
};
if let Some(styles) = node.primary_styles() {
let current_color = styles.clone_color();
let bg = styles.clone_background_color();
let bg_abs = bg.resolve_to_absolute(¤t_color);
let r = (bg_abs.components.0.clamp(0.0, 1.0) * 255.0) as u8;
let g = (bg_abs.components.1.clamp(0.0, 1.0) * 255.0) as u8;
let b = (bg_abs.components.2.clamp(0.0, 1.0) * 255.0) as u8;
let a = (bg_abs.alpha.clamp(0.0, 1.0) * 255.0) as u8;
if a > 0 {
style.background_color = Some([r, g, b, a]);
}
let bc = styles.clone_border_top_color();
let bc_abs = bc.resolve_to_absolute(¤t_color);
style.border_color = [
(bc_abs.components.0.clamp(0.0, 1.0) * 255.0) as u8,
(bc_abs.components.1.clamp(0.0, 1.0) * 255.0) as u8,
(bc_abs.components.2.clamp(0.0, 1.0) * 255.0) as u8,
(bc_abs.alpha.clamp(0.0, 1.0) * 255.0) as u8,
];
let width = layout.size.width;
let height = layout.size.height;
let resolve_radius =
|r: &style::values::computed::length_percentage::NonNegativeLengthPercentage,
basis: f32|
-> f32 {
r.0.resolve(style::values::computed::Length::new(basis))
.px()
};
let tl = styles.clone_border_top_left_radius();
let tr = styles.clone_border_top_right_radius();
let br = styles.clone_border_bottom_right_radius();
let bl = styles.clone_border_bottom_left_radius();
style.border_radii = [
[
resolve_radius(&tl.0.width, width),
resolve_radius(&tl.0.height, height),
],
[
resolve_radius(&tr.0.width, width),
resolve_radius(&tr.0.height, height),
],
[
resolve_radius(&br.0.width, width),
resolve_radius(&br.0.height, height),
],
[
resolve_radius(&bl.0.width, width),
resolve_radius(&bl.0.height, height),
],
];
let convert_border_style = |bs: style::values::specified::BorderStyle| -> BorderStyleValue {
use style::values::specified::BorderStyle as BS;
match bs {
BS::None | BS::Hidden => BorderStyleValue::None,
BS::Dashed => BorderStyleValue::Dashed,
BS::Dotted => BorderStyleValue::Dotted,
BS::Double => BorderStyleValue::Double,
BS::Groove => BorderStyleValue::Groove,
BS::Ridge => BorderStyleValue::Ridge,
BS::Inset => BorderStyleValue::Inset,
BS::Outset => BorderStyleValue::Outset,
BS::Solid => BorderStyleValue::Solid,
}
};
style.border_styles = [
convert_border_style(styles.clone_border_top_style()),
convert_border_style(styles.clone_border_right_style()),
convert_border_style(styles.clone_border_bottom_style()),
convert_border_style(styles.clone_border_left_style()),
];
}
style
}
fn is_non_visual_element(node: &Node) -> bool {
if let Some(elem) = node.element_data() {
let tag = elem.name.local.as_ref();
matches!(
tag,
"head" | "script" | "style" | "link" | "meta" | "title" | "noscript"
)
} else {
false
}
}
fn extract_marker_lines(doc: &blitz_dom::BaseDocument, node: &Node) -> (Vec<ShapedLine>, f32) {
let elem_data = match node.element_data() {
Some(d) => d,
None => return (Vec::new(), 0.0),
};
let list_item_data = match &elem_data.list_item_data {
Some(d) => d,
None => return (Vec::new(), 0.0),
};
let parley_layout = match &list_item_data.position {
blitz_dom::node::ListItemLayoutPosition::Outside(layout) => layout,
blitz_dom::node::ListItemLayoutPosition::Inside => return (Vec::new(), 0.0),
};
let marker_text = match &list_item_data.marker {
blitz_dom::node::Marker::Char(c) => {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf).to_string()
}
blitz_dom::node::Marker::String(s) => s.clone(),
};
let mut shaped_lines = Vec::new();
let mut max_width: f32 = 0.0;
for line in parley_layout.lines() {
let metrics = line.metrics();
let mut glyph_runs = Vec::new();
let mut line_width: f32 = 0.0;
for item in line.items() {
if let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item {
let run = glyph_run.run();
let font_data = run.font();
let font_bytes: Vec<u8> = font_data.data.data().to_vec();
let font_index = font_data.index;
let font_size = run.font_size();
let brush = &glyph_run.style().brush;
let color = get_text_color(doc, brush.id);
let text_len = marker_text.len();
let mut glyphs = Vec::new();
for g in glyph_run.glyphs() {
line_width += g.advance;
glyphs.push(ShapedGlyph {
id: g.id,
x_advance: g.advance / font_size,
x_offset: g.x / font_size,
y_offset: g.y / font_size,
text_range: 0..text_len,
});
}
if !glyphs.is_empty() {
glyph_runs.push(ShapedGlyphRun {
font_data: Arc::new(font_bytes),
font_index,
font_size,
color,
decoration: Default::default(),
glyphs,
text: marker_text.clone(),
x_offset: glyph_run.offset(),
});
}
}
}
max_width = max_width.max(line_width);
shaped_lines.push(ShapedLine {
height: metrics.line_height,
baseline: metrics.baseline,
glyph_runs,
});
}
(shaped_lines, max_width)
}
fn get_text_color(doc: &blitz_dom::BaseDocument, node_id: usize) -> [u8; 4] {
if let Some(node) = doc.get_node(node_id)
&& let Some(styles) = node.primary_styles()
{
let color = styles.clone_color();
let r = (color.components.0.clamp(0.0, 1.0) * 255.0) as u8;
let g = (color.components.1.clamp(0.0, 1.0) * 255.0) as u8;
let b = (color.components.2.clamp(0.0, 1.0) * 255.0) as u8;
let a = (color.alpha.clamp(0.0, 1.0) * 255.0) as u8;
return [r, g, b, a];
}
[0, 0, 0, 255] }
fn get_text_decoration(doc: &blitz_dom::BaseDocument, node_id: usize) -> TextDecoration {
if let Some(node) = doc.get_node(node_id)
&& let Some(styles) = node.primary_styles()
{
let current_color = styles.clone_color();
let stylo_line = styles.clone_text_decoration_line();
let mut line = TextDecorationLine::NONE;
if stylo_line.contains(style::values::specified::TextDecorationLine::UNDERLINE) {
line = line | TextDecorationLine::UNDERLINE;
}
if stylo_line.contains(style::values::specified::TextDecorationLine::OVERLINE) {
line = line | TextDecorationLine::OVERLINE;
}
if stylo_line.contains(style::values::specified::TextDecorationLine::LINE_THROUGH) {
line = line | TextDecorationLine::LINE_THROUGH;
}
use style::properties::longhands::text_decoration_style::computed_value::T as StyloTDS;
let style = match styles.clone_text_decoration_style() {
StyloTDS::Solid => TextDecorationStyle::Solid,
StyloTDS::Dashed => TextDecorationStyle::Dashed,
StyloTDS::Dotted => TextDecorationStyle::Dotted,
StyloTDS::Double => TextDecorationStyle::Double,
StyloTDS::Wavy => TextDecorationStyle::Wavy,
_ => TextDecorationStyle::Solid,
};
let deco_color = styles.clone_text_decoration_color();
let resolved = deco_color.resolve_to_absolute(¤t_color);
let color = [
(resolved.components.0.clamp(0.0, 1.0) * 255.0) as u8,
(resolved.components.1.clamp(0.0, 1.0) * 255.0) as u8,
(resolved.components.2.clamp(0.0, 1.0) * 255.0) as u8,
(resolved.alpha.clamp(0.0, 1.0) * 255.0) as u8,
];
return TextDecoration { line, style, color };
}
TextDecoration::default()
}