use super::*;
pub(super) fn needs_container_wrapper(style: &ComputedStyle) -> bool {
let has_top = style.border_style_top != BorderStyle::None
&& !matches!(style.border_width_top, Length::Auto | Length::Px(0.0));
let has_bottom = style.border_style_bottom != BorderStyle::None
&& !matches!(style.border_width_bottom, Length::Auto | Length::Px(0.0));
let has_left = style.border_style_left != BorderStyle::None
&& !matches!(style.border_width_left, Length::Auto | Length::Px(0.0));
let has_right = style.border_style_right != BorderStyle::None
&& !matches!(style.border_width_right, Length::Auto | Length::Px(0.0));
has_top || has_bottom || has_left || has_right
}
pub fn ir_to_tokens(chapter: &Chapter, ctx: &mut ExportContext) -> TokenStream {
let sch = schema();
let mut stream = TokenStream::new();
ctx.reset_style_memo();
walk_node_for_export(chapter, chapter.root(), sch, ctx, &mut stream);
stream
}
pub(super) fn walk_node_for_export(
chapter: &Chapter,
node_id: NodeId,
sch: &crate::kfx::schema::KfxSchema,
ctx: &mut ExportContext,
stream: &mut TokenStream,
) {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return,
};
if node.role == Role::Root {
for child in chapter.children(node_id) {
walk_node_for_export(chapter, child, sch, ctx, stream);
}
return;
}
if node.role == Role::Text {
if !node.text.is_empty() {
let text = chapter.text(node.text);
if !text.is_empty() {
stream.push(KfxToken::Text(text.to_string()));
}
}
return;
}
if node.role == Role::Break {
stream.push(KfxToken::Text("\n".to_string()));
return;
}
if node.role == Role::DefinitionList {
emit_definition_list(chapter, node_id, sch, ctx, stream);
return;
}
if node.role == Role::Link || node.role == Role::Inline {
emit_inline_content_flat(chapter, node_id, sch, ctx, stream);
return;
}
let mut elem = ElementStart::new(node.role);
elem.node_id = Some(node_id);
if node.role == Role::Table {
ctx.has_tables = true;
}
let style_symbol = ctx.register_style_id(node.style, &chapter.styles);
elem.style_symbol = Some(style_symbol);
let is_table_structural = matches!(
node.role,
Role::Table
| Role::TableRow
| Role::TableCell
| Role::TableHead
| Role::TableBody
| Role::Image
);
elem.needs_container_wrapper = !is_table_structural
&& chapter
.styles
.get(node.style)
.map(needs_container_wrapper)
.unwrap_or(false);
let export_ctx = crate::kfx::transforms::ExportContext {
spine_map: None,
resource_registry: Some(&ctx.resource_registry),
};
let mut kfx_attrs = sch.export_attributes(
node.role,
|target| match target {
SemanticTarget::Href => chapter.semantics.href(node_id).map(|s| s.to_string()),
SemanticTarget::Src => chapter.semantics.src(node_id).map(|s| s.to_string()),
SemanticTarget::Alt => chapter.semantics.alt(node_id).map(|s| s.to_string()),
SemanticTarget::Id => chapter.semantics.id(node_id).map(|s| s.to_string()),
SemanticTarget::EpubType => chapter.semantics.epub_type(node_id).map(|s| s.to_string()),
},
&export_ctx,
);
for (field_id, value) in &mut kfx_attrs {
if *field_id == sym!(LinkTo) {
let anchor_symbol = ctx.anchor_registry.get_or_create_href_symbol(value);
*value = anchor_symbol;
}
}
if node.role == Role::TableCell {
if let Some(cols) = chapter.semantics.col_span(node_id)
&& cols > 1
{
kfx_attrs.push((sym!(TableColumnSpan), cols.to_string()));
}
if let Some(rows) = chapter.semantics.row_span(node_id)
&& rows > 1
{
kfx_attrs.push((sym!(TableRowSpan), rows.to_string()));
}
elem.is_header_cell = chapter.semantics.is_header_cell(node_id);
}
if node.role == Role::OrderedList
&& let Some(start) = chapter.semantics.list_start(node_id)
&& start != 1
{
kfx_attrs.push((sym!(ListStartOffset), start.to_string()));
}
elem.kfx_attrs = kfx_attrs;
if let Some(href) = chapter.semantics.href(node_id) {
elem.set_semantic(SemanticTarget::Href, href.to_string());
}
if let Some(src) = chapter.semantics.src(node_id) {
elem.set_semantic(SemanticTarget::Src, src.to_string());
ctx.resource_registry.register(src, &mut ctx.symbols);
}
if let Some(alt) = chapter.semantics.alt(node_id) {
elem.set_semantic(SemanticTarget::Alt, alt.to_string());
}
if let Some(id) = chapter.semantics.id(node_id) {
elem.set_semantic(SemanticTarget::Id, id.to_string());
}
if let Some(epub_type) = chapter.semantics.epub_type(node_id) {
elem.set_semantic(SemanticTarget::EpubType, epub_type.to_string());
}
let run_style_symbol = elem.style_symbol;
stream.push(KfxToken::StartElement(elem));
let is_inline_flow =
|role: Role| matches!(role, Role::Text | Role::Break | Role::Link | Role::Inline);
let children: Vec<NodeId> = chapter.children(node_id).collect();
let has_own_text = !node.text.is_empty() && !chapter.text(node.text).is_empty();
let has_flow = has_own_text
|| children
.iter()
.any(|&c| chapter.node(c).is_some_and(|n| is_inline_flow(n.role)));
let has_elements = children
.iter()
.any(|&c| chapter.node(c).is_some_and(|n| !is_inline_flow(n.role)));
if !(has_flow && has_elements) {
if !node.text.is_empty() {
let text = chapter.text(node.text);
if !text.is_empty() {
stream.push(KfxToken::Text(text.to_string()));
}
}
for child in children {
walk_node_for_export(chapter, child, sch, ctx, stream);
}
} else {
let mut run_open = false;
let open_run = |stream: &mut TokenStream, run_open: &mut bool| {
if !*run_open {
let mut run = ElementStart::new(Role::Text);
run.style_symbol = run_style_symbol;
stream.push(KfxToken::StartElement(run));
*run_open = true;
}
};
let close_run = |stream: &mut TokenStream, run_open: &mut bool| {
if *run_open {
stream.push(KfxToken::EndElement);
*run_open = false;
}
};
if has_own_text {
open_run(stream, &mut run_open);
stream.push(KfxToken::Text(chapter.text(node.text).to_string()));
}
for child in children {
let child_is_flow = chapter.node(child).is_some_and(|n| is_inline_flow(n.role));
if child_is_flow {
open_run(stream, &mut run_open);
} else {
close_run(stream, &mut run_open);
}
walk_node_for_export(chapter, child, sch, ctx, stream);
}
close_run(stream, &mut run_open);
}
stream.push(KfxToken::EndElement);
}
#[derive(Clone, Default)]
pub(super) struct InlineState {
pub(super) link_to: Option<String>,
pub(super) style: Option<crate::style::StyleId>,
pub(super) epub_type: Option<String>,
pub(super) element_id: Option<String>,
pub(super) node_id: Option<NodeId>,
}
pub(super) struct FlatSegment {
pub(super) text: String,
pub(super) state: InlineState,
}
pub(super) fn flatten_inline_content(
chapter: &Chapter,
node_id: NodeId,
state: InlineState,
segments: &mut Vec<FlatSegment>,
) {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return,
};
let has_id = chapter.semantics.id(node_id).is_some();
let effective_state = InlineState {
link_to: chapter
.semantics
.href(node_id)
.map(|s| s.to_string())
.or(state.link_to),
style: if node.role == Role::Inline || node.role == Role::Link {
Some(node.style)
} else {
state.style
},
epub_type: chapter
.semantics
.epub_type(node_id)
.map(|s| s.to_string())
.or(state.epub_type),
element_id: chapter
.semantics
.id(node_id)
.map(|s| s.to_string())
.or(state.element_id),
node_id: if has_id { Some(node_id) } else { state.node_id },
};
match node.role {
Role::Text => {
if !node.text.is_empty() {
let text = chapter.text(node.text);
if !text.is_empty() {
segments.push(FlatSegment {
text: text.to_string(),
state: effective_state,
});
}
}
}
Role::Break => {
segments.push(FlatSegment {
text: "\n".to_string(),
state: effective_state,
});
}
_ => {
let children: Vec<_> = chapter.children(node_id).collect();
if children.is_empty() && effective_state.element_id.is_some() {
segments.push(FlatSegment {
text: "\u{200B}".to_string(), state: effective_state,
});
} else {
for child_id in children {
flatten_inline_content(chapter, child_id, effective_state.clone(), segments);
}
}
}
}
}
pub(super) fn emit_flattened_segments(
segments: Vec<FlatSegment>,
chapter: &Chapter,
_sch: &crate::kfx::schema::KfxSchema,
ctx: &mut ExportContext,
stream: &mut TokenStream,
) {
for segment in segments {
let needs_style_event = segment.state.link_to.is_some() || segment.state.style.is_some();
if needs_style_event {
let mut span = SpanStart::new(
if segment.state.link_to.is_some() {
Role::Link
} else {
Role::Inline
},
0,
0,
);
if let Some(style_id) = segment.state.style {
let style_symbol = ctx.register_inline_style_id(style_id, &chapter.styles);
span.style_symbol = Some(style_symbol);
}
let mut kfx_attrs = crate::kfx::tokens::KfxAttrs::new();
if let Some(ref href) = segment.state.link_to {
let anchor_symbol = ctx.anchor_registry.get_or_create_href_symbol(href);
kfx_attrs.push((sym!(LinkTo), anchor_symbol));
}
if let Some(ref epub_type) = segment.state.epub_type
&& epub_type.split_whitespace().any(|t| t == "noteref")
{
kfx_attrs.push((sym!(YjDisplay), "617".to_string()));
}
span.kfx_attrs = kfx_attrs;
if let Some(ref id) = segment.state.element_id {
span.set_semantic(SemanticTarget::Id, id.clone());
}
span.node_id = segment.state.node_id;
stream.push(KfxToken::StartSpan(span));
stream.push(KfxToken::Text(segment.text));
stream.push(KfxToken::EndSpan);
} else {
stream.push(KfxToken::Text(segment.text));
}
}
}
pub(super) fn emit_definition_list(
chapter: &Chapter,
node_id: NodeId,
sch: &crate::kfx::schema::KfxSchema,
ctx: &mut ExportContext,
stream: &mut TokenStream,
) {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return,
};
let mut dl_elem = ElementStart::new(Role::Paragraph);
let dl_style = ctx.register_style_id(node.style, &chapter.styles);
dl_elem.style_symbol = Some(dl_style);
stream.push(KfxToken::StartElement(dl_elem));
let children: Vec<NodeId> = chapter.children(node_id).collect();
let mut i = 0;
while i < children.len() {
let child_id = children[i];
let child = match chapter.node(child_id) {
Some(n) => n,
None => {
i += 1;
continue;
}
};
if child.role == Role::DefinitionTerm {
let dd_info = if i + 1 < children.len() {
let next_id = children[i + 1];
chapter.node(next_id).and_then(|next| {
if next.role == Role::DefinitionDescription {
Some((next_id, next.style))
} else {
None
}
})
} else {
None
};
let mut wrapper_elem = ElementStart::new(Role::Paragraph);
let wrapper_style = if let Some((_, dd_style_id)) = dd_info {
ctx.register_style_id(dd_style_id, &chapter.styles)
} else {
ctx.default_style_symbol
};
if wrapper_style == ctx.default_style_symbol {
ctx.default_style_used = true;
}
wrapper_elem.style_symbol = Some(wrapper_style);
stream.push(KfxToken::StartElement(wrapper_elem));
let dt_style = ctx.register_style_id(child.style, &chapter.styles);
let mut dt_elem = ElementStart::new(Role::DefinitionTerm);
dt_elem.style_symbol = Some(dt_style);
stream.push(KfxToken::StartElement(dt_elem));
let mut dt_inner = ElementStart::new(Role::Paragraph);
dt_inner.style_symbol = Some(dt_style);
stream.push(KfxToken::StartElement(dt_inner));
for dt_child in chapter.children(child_id) {
walk_node_for_export(chapter, dt_child, sch, ctx, stream);
}
stream.push(KfxToken::EndElement); stream.push(KfxToken::EndElement);
if let Some((dd_id, _)) = dd_info {
let is_inline_flow = |role: Role| {
matches!(role, Role::Text | Role::Break | Role::Link | Role::Inline)
};
let mut run_open = false;
for dd_child in chapter.children(dd_id) {
let child_is_flow = chapter
.node(dd_child)
.is_some_and(|n| is_inline_flow(n.role));
if child_is_flow && !run_open {
let mut run = ElementStart::new(Role::Text);
run.style_symbol = Some(wrapper_style);
stream.push(KfxToken::StartElement(run));
run_open = true;
} else if !child_is_flow && run_open {
stream.push(KfxToken::EndElement);
run_open = false;
}
walk_node_for_export(chapter, dd_child, sch, ctx, stream);
}
if run_open {
stream.push(KfxToken::EndElement);
}
i += 1; }
stream.push(KfxToken::EndElement);
} else {
walk_node_for_export(chapter, child_id, sch, ctx, stream);
}
i += 1;
}
stream.push(KfxToken::EndElement);
}
pub(super) fn emit_inline_content_flat(
chapter: &Chapter,
node_id: NodeId,
sch: &crate::kfx::schema::KfxSchema,
ctx: &mut ExportContext,
stream: &mut TokenStream,
) {
let mut segments = Vec::new();
flatten_inline_content(chapter, node_id, InlineState::default(), &mut segments);
emit_flattened_segments(segments, chapter, sch, ctx, stream);
}