use super::*;
pub(super) struct TokenizeContext<'a> {
doc_symbols: &'a [String],
anchors: Option<&'a HashMap<String, String>>,
}
pub fn tokenize_storyline(
storyline: &IonValue,
doc_symbols: &[String],
anchors: Option<&HashMap<String, String>>,
_styles: Option<&HashMap<String, Vec<(u64, IonValue)>>>,
) -> TokenStream {
let mut stream = TokenStream::new();
let fields = match storyline.as_struct() {
Some(f) => f,
None => return stream,
};
let content_list = match get_field(fields, sym!(ContentList)) {
Some(list) => list,
None => return stream,
};
let ctx = TokenizeContext {
doc_symbols,
anchors,
};
tokenize_content_list(content_list, &ctx, &mut stream);
stream
}
pub(super) fn tokenize_content_list(
list: &IonValue,
ctx: &TokenizeContext,
stream: &mut TokenStream,
) {
let items = match list.as_list() {
Some(l) => l,
None => return,
};
for item in items {
tokenize_content_item(item, ctx, stream);
}
}
pub(super) fn tokenize_content_item(
item: &IonValue,
ctx: &TokenizeContext,
stream: &mut TokenStream,
) {
let inner = item.unwrap_annotated();
let fields = match inner.as_struct() {
Some(f) => f,
None => return,
};
let kfx_type_id = get_field(fields, sym!(Type))
.and_then(|v| v.as_symbol())
.and_then(|s| u32::try_from(s).ok())
.unwrap_or(KfxSymbol::Container as u32);
let mut role = schema().resolve_element_role(kfx_type_id, |symbol| {
get_field(fields, symbol as u64).and_then(|v| {
v.as_int()
.or_else(|| v.as_symbol().and_then(|s| i64::try_from(s).ok()))
})
});
let mut is_header_cell = false;
if let Some(semantic_type) = get_semantic_type_annotation(fields, ctx.doc_symbols) {
if semantic_type == "table_header_cell" {
role = Role::TableCell;
is_header_cell = true;
} else if let Some(mapped_role) = schema().role_for_semantic_type(&semantic_type) {
role = mapped_role;
}
}
if let Some(layout_hints) = get_field(fields, sym!(LayoutHints))
&& let Some(hints_list) = layout_hints.as_list()
{
for hint in hints_list {
if let Some(hint_id) = hint.as_symbol().and_then(|s| u32::try_from(s).ok())
&& let Some(mapped_role) = schema().role_for_layout_hint(hint_id)
{
role = mapped_role;
break;
}
}
}
if let Some(override_role) =
schema().check_span_role_override(|sym| get_field(fields, sym as u64).is_some())
{
role = override_role;
}
let id = get_field(fields, sym!(Id)).and_then(|v| v.as_int());
let semantics = extract_all_element_attrs(fields, kfx_type_id, ctx);
let content_ref = get_field(fields, sym!(Content))
.and_then(|v| v.as_struct())
.and_then(|content_fields| {
let name = get_field(content_fields, sym!(Name))
.and_then(|v| resolve_symbol_or_string(v, ctx.doc_symbols))?;
let index = get_field(content_fields, sym!(Index))
.and_then(|v| v.as_int())
.and_then(|n| usize::try_from(n).ok())?;
Some(ContentRef { name, index })
});
let style_events = get_field(fields, sym!(StyleEvents))
.and_then(|v| v.as_list())
.map(|events| parse_style_events(events, ctx))
.unwrap_or_default();
let has_children = get_field(fields, sym!(ContentList)).is_some();
let style_name =
get_field(fields, sym!(Style)).and_then(|v| resolve_symbol_or_string(v, ctx.doc_symbols));
let mut kfx_attrs = crate::kfx::tokens::KfxAttrs::new();
for sym in [
KfxSymbol::TableColumnSpan,
KfxSymbol::TableRowSpan,
KfxSymbol::ListStartOffset,
] {
if let Some(n) = get_field(fields, sym as u64).and_then(|v| v.as_int()) {
kfx_attrs.push((sym as u64, n.to_string()));
}
}
stream.push(KfxToken::StartElement(ElementStart {
role,
node_id: None, id,
semantics,
content_ref,
style_events,
kfx_attrs,
style_symbol: None, style_name, needs_container_wrapper: false, is_header_cell,
}));
if has_children && let Some(children) = get_field(fields, sym!(ContentList)) {
tokenize_content_list(children, ctx, stream);
}
stream.push(KfxToken::EndElement);
}
pub(super) fn get_semantic_type_annotation(
fields: &[(u64, IonValue)],
doc_symbols: &[String],
) -> Option<String> {
let doc_idx = doc_symbols.iter().position(|s| s == "yj.semantics.type")?;
let field_id = crate::kfx::symbols::KFX_SYMBOL_TABLE_SIZE + doc_idx;
get_field(fields, field_id as u64).and_then(|v| resolve_symbol_or_string(v, doc_symbols))
}
pub(super) fn extract_all_element_attrs(
fields: &[(u64, IonValue)],
kfx_type_id: u32,
ctx: &TokenizeContext,
) -> HashMap<SemanticTarget, String> {
let mut result = HashMap::new();
let import_ctx = ImportContext {
doc_symbols: ctx.doc_symbols,
chapter_id: None,
anchors: ctx.anchors,
};
for rule in schema().element_attr_rules(kfx_type_id) {
if let Some(raw_value) = get_field(fields, rule.kfx_field as u64)
.and_then(|v| resolve_symbol_or_string(v, ctx.doc_symbols))
{
let parsed = rule.transform.import(&raw_value, &import_ctx);
let final_value = match parsed {
crate::kfx::transforms::ParsedAttribute::String(s) => s,
crate::kfx::transforms::ParsedAttribute::Link(link) => link.to_href(),
crate::kfx::transforms::ParsedAttribute::Anchor(id) => id,
};
result.insert(rule.target, final_value);
}
}
let has_field = |symbol: KfxSymbol| get_field(fields, symbol as u64).is_some();
for rule in schema().span_attr_rules(has_field) {
if result.contains_key(&rule.target) {
continue;
}
if let Some(raw_value) = get_field(fields, rule.kfx_field as u64)
.and_then(|v| resolve_symbol_or_string(v, ctx.doc_symbols))
{
let parsed = rule.transform.import(&raw_value, &import_ctx);
let final_value = match parsed {
crate::kfx::transforms::ParsedAttribute::String(s) => s,
crate::kfx::transforms::ParsedAttribute::Link(link) => link.to_href(),
crate::kfx::transforms::ParsedAttribute::Anchor(id) => id,
};
result.insert(rule.target, final_value);
}
}
result
}
pub(super) fn parse_style_events(events: &[IonValue], ctx: &TokenizeContext) -> Vec<SpanStart> {
events
.iter()
.filter_map(|event| {
let fields = event.as_struct()?;
let offset = get_field(fields, sym!(Offset))
.and_then(|v| v.as_int())
.and_then(|n| usize::try_from(n).ok())?;
let length = get_field(fields, sym!(Length))
.and_then(|v| v.as_int())
.and_then(|n| usize::try_from(n).ok())?;
let style_symbol = get_field(fields, sym!(Style)).and_then(|v| v.as_symbol());
let has_field = |symbol: KfxSymbol| get_field(fields, symbol as u64).is_some();
let role = schema().resolve_span_role(has_field);
let semantics = extract_all_span_attrs(fields, has_field, ctx);
Some(SpanStart {
role,
node_id: None, semantics,
offset,
length,
style_symbol,
kfx_attrs: crate::kfx::tokens::KfxAttrs::new(),
})
})
.collect()
}
pub(super) fn extract_all_span_attrs<F>(
fields: &[(u64, IonValue)],
has_field: F,
ctx: &TokenizeContext,
) -> HashMap<SemanticTarget, String>
where
F: Fn(KfxSymbol) -> bool,
{
let mut result = HashMap::new();
let import_ctx = ImportContext {
doc_symbols: ctx.doc_symbols,
chapter_id: None,
anchors: ctx.anchors,
};
for rule in schema().span_attr_rules(&has_field) {
if let Some(raw_value) = get_field(fields, rule.kfx_field as u64)
.and_then(|v| resolve_symbol_or_string(v, ctx.doc_symbols))
{
let parsed = rule.transform.import(&raw_value, &import_ctx);
let final_value = match parsed {
crate::kfx::transforms::ParsedAttribute::String(s) => s,
crate::kfx::transforms::ParsedAttribute::Link(link) => link.to_href(),
crate::kfx::transforms::ParsedAttribute::Anchor(id) => id,
};
result.insert(rule.target, final_value);
}
}
result
}
pub fn build_ir_from_tokens<F>(
tokens: &TokenStream,
doc_symbols: &[String],
styles: Option<&HashMap<String, Vec<(u64, IonValue)>>>,
mut content_lookup: F,
) -> Chapter
where
F: FnMut(&str, usize) -> Option<String>,
{
let mut chapter = Chapter::new();
let mut stack: Vec<NodeId> = vec![chapter.root()];
for token in tokens {
match token {
KfxToken::StartElement(elem) => {
let parent = *stack.last().unwrap_or(&chapter.root());
let node = Node::new(elem.role);
let node_id = chapter.alloc_node(node);
chapter.append_child(parent, node_id);
if let Some(style_name) = &elem.style_name
&& let Some(styles_map) = styles
&& let Some(kfx_props) = styles_map.get(style_name)
{
let ir_style = kfx_style_to_ir(kfx_props);
let style_id = chapter.styles.intern(ir_style);
if let Some(node) = chapter.node_mut(node_id) {
node.style = style_id;
}
}
apply_semantics_to_node(&mut chapter, node_id, &elem.semantics);
if elem.is_header_cell {
chapter.semantics.set_header_cell(node_id, true);
}
for (field_id, value) in &elem.kfx_attrs {
let Ok(n) = value.parse::<u32>() else {
continue;
};
if *field_id == sym!(TableColumnSpan) {
chapter.semantics.set_col_span(node_id, n);
} else if *field_id == sym!(TableRowSpan) {
chapter.semantics.set_row_span(node_id, n);
} else if *field_id == sym!(ListStartOffset) {
chapter.semantics.set_list_start(node_id, n);
}
}
if let Some(id) = elem.id {
chapter.semantics.set_id(node_id, &id.to_string());
}
if let Some(ref content_ref) = elem.content_ref
&& let Some(text) = content_lookup(&content_ref.name, content_ref.index)
{
if elem.style_events.is_empty() {
let range = chapter.append_text(&text);
let text_node = chapter.alloc_node(Node::text(range));
chapter.append_child(node_id, text_node);
} else {
build_text_with_spans(
&mut chapter,
node_id,
&text,
&elem.style_events,
doc_symbols,
styles,
);
}
}
stack.push(node_id);
}
KfxToken::EndElement => {
stack.pop();
}
KfxToken::Text(text) => {
let parent = *stack.last().unwrap_or(&chapter.root());
let range = chapter.append_text(text);
let text_node = chapter.alloc_node(Node::text(range));
chapter.append_child(parent, text_node);
}
KfxToken::StartSpan(_) | KfxToken::EndSpan => {
}
}
}
chapter
}
pub(super) fn apply_semantics_to_node(
chapter: &mut Chapter,
node_id: NodeId,
semantics: &HashMap<SemanticTarget, String>,
) {
for (target, value) in semantics {
match target {
SemanticTarget::Src => chapter.semantics.set_src(node_id, value),
SemanticTarget::Href => {
chapter.semantics.set_href(node_id, value);
}
SemanticTarget::Alt => chapter.semantics.set_alt(node_id, value),
SemanticTarget::Id => chapter.semantics.set_id(node_id, value),
SemanticTarget::EpubType => chapter.semantics.set_epub_type(node_id, value),
}
}
}
pub(super) fn kfx_style_to_ir(props: &[(u64, IonValue)]) -> crate::style::ComputedStyle {
use crate::kfx::style_schema::{StyleSchema, import_kfx_style};
let schema = StyleSchema::standard();
import_kfx_style(schema, props)
}
pub(super) fn build_text_with_spans(
chapter: &mut Chapter,
parent: NodeId,
text: &str,
spans: &[SpanStart],
doc_symbols: &[String],
styles: Option<&HashMap<String, Vec<(u64, IonValue)>>>,
) {
let mut sorted_spans: Vec<_> = spans.iter().collect();
sorted_spans.sort_by(|a, b| {
a.offset
.cmp(&b.offset)
.then_with(|| b.length.cmp(&a.length)) });
let create_span_node = |chapter: &mut Chapter, span: &SpanStart| -> NodeId {
let span_node = chapter.alloc_node(Node::new(span.role));
if let Some(style_sym) = span.style_symbol
&& let Some(style_name) = resolve_symbol(style_sym, doc_symbols)
&& let Some(styles_map) = styles
&& let Some(kfx_props) = styles_map.get(style_name)
{
let ir_style = kfx_style_to_ir(kfx_props);
let style_id = chapter.styles.intern(ir_style);
if let Some(node) = chapter.node_mut(span_node) {
node.style = style_id;
}
}
apply_semantics_to_node(chapter, span_node, &span.semantics);
span_node
};
let mut span_stack: Vec<(NodeId, usize)> = vec![(parent, usize::MAX)];
let mut char_pos: usize = 0;
for span in sorted_spans {
let span_start = span.offset;
let span_end = span.offset.saturating_add(span.length);
while span_stack.len() > 1 {
let (_, stack_end) = span_stack.last().unwrap();
if *stack_end <= span_start {
if char_pos < *stack_end {
let byte_start = char_to_byte_offset(text, char_pos);
let byte_end = char_to_byte_offset(text, *stack_end);
if byte_end > byte_start {
let segment = &text[byte_start..byte_end];
let range = chapter.append_text(segment);
let text_node = chapter.alloc_node(Node::text(range));
let (parent_id, _) = span_stack.last().unwrap();
chapter.append_child(*parent_id, text_node);
}
char_pos = *stack_end;
}
span_stack.pop();
} else {
break;
}
}
if char_pos < span_start {
let byte_start = char_to_byte_offset(text, char_pos);
let byte_end = char_to_byte_offset(text, span_start);
if byte_end > byte_start {
let before = &text[byte_start..byte_end];
let range = chapter.append_text(before);
let text_node = chapter.alloc_node(Node::text(range));
let (current_parent, _) = span_stack.last().unwrap();
chapter.append_child(*current_parent, text_node);
}
char_pos = span_start;
}
if span_end > span_start {
let span_node = create_span_node(chapter, span);
let (current_parent, _) = span_stack.last().unwrap();
chapter.append_child(*current_parent, span_node);
span_stack.push((span_node, span_end));
}
}
while let Some((node_id, end_offset)) = span_stack.pop() {
let actual_end = end_offset.min(text.chars().count());
if char_pos < actual_end {
let byte_start = char_to_byte_offset(text, char_pos);
let byte_end = char_to_byte_offset(text, actual_end);
if byte_end > byte_start {
let segment = &text[byte_start..byte_end];
let range = chapter.append_text(segment);
let text_node = chapter.alloc_node(Node::text(range));
chapter.append_child(node_id, text_node);
}
char_pos = actual_end;
}
}
}
pub(super) fn char_to_byte_offset(text: &str, char_offset: usize) -> usize {
text.char_indices()
.nth(char_offset)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(text.len())
}
pub(super) fn resolve_symbol(id: u64, doc_symbols: &[String]) -> Option<&str> {
crate::kfx::container::resolve_symbol(id, doc_symbols)
}
pub(super) fn resolve_symbol_or_string(value: &IonValue, doc_symbols: &[String]) -> Option<String> {
match value {
IonValue::String(s) => Some(s.clone()),
IonValue::Symbol(id) => resolve_symbol(*id, doc_symbols).map(|s| s.to_string()),
_ => None,
}
}
pub fn parse_storyline_to_ir<F>(
storyline: &IonValue,
doc_symbols: &[String],
anchors: Option<&HashMap<String, String>>,
styles: Option<&HashMap<String, Vec<(u64, IonValue)>>>,
content_lookup: F,
) -> Chapter
where
F: FnMut(&str, usize) -> Option<String>,
{
let tokens = tokenize_storyline(storyline, doc_symbols, anchors, styles);
build_ir_from_tokens(&tokens, doc_symbols, styles, content_lookup)
}