use super::*;
pub(super) fn build_chapter_entities_grouped(
chapter: &Chapter,
chapter_id: ChapterId,
section_name: &str,
ctx: &mut ExportContext,
) -> (KfxFragment, KfxFragment, Option<KfxFragment>) {
use crate::kfx::storyline::{ir_to_tokens, tokens_to_ion};
let is_cover = ctx.cover_fragment_id.is_none() && is_image_only_chapter(chapter);
let story_name = format!("story_{}", section_name);
let content_name = format!("content_{}", section_name);
let section_name_symbol = ctx.symbols.get_or_intern(section_name);
let story_name_symbol = ctx.symbols.get_or_intern(&story_name);
let content_name_symbol = ctx.symbols.get_or_intern(&content_name);
ctx.begin_chapter(&content_name);
let section_id = ctx
.get_chapter_fragment(chapter_id)
.unwrap_or_else(|| ctx.next_fragment_id());
let (storyline_content_list, content_strings) = if is_cover {
let content_list = build_cover_storyline(chapter, ctx);
let text = ctx.drain_text();
(content_list, text)
} else {
let tokens = ir_to_tokens(chapter, ctx);
let content_list = tokens_to_ion(&tokens, ctx);
let text = ctx.drain_text();
(content_list, text)
};
let content_fragment = if !content_strings.is_empty() {
let content_ion = IonValue::Struct(vec![
(
KfxSymbol::Name as u64,
IonValue::Symbol(content_name_symbol),
),
(
KfxSymbol::ContentList as u64,
IonValue::List(content_strings.into_iter().map(IonValue::String).collect()),
),
]);
Some(KfxFragment::new(
KfxSymbol::Content,
&content_name,
content_ion,
))
} else {
None
};
let storyline_ion = IonValue::Struct(vec![
(
KfxSymbol::StoryName as u64,
IonValue::Symbol(story_name_symbol),
),
(KfxSymbol::ContentList as u64, storyline_content_list),
]);
let storyline_fragment = KfxFragment::new(KfxSymbol::Storyline, &story_name, storyline_ion);
let page_template = if is_cover {
IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Int(section_id as i64)),
(
KfxSymbol::StoryName as u64,
IonValue::Symbol(story_name_symbol),
),
(
KfxSymbol::Type as u64,
IonValue::Symbol(KfxSymbol::Container as u64),
),
(KfxSymbol::FixedWidth as u64, IonValue::Int(1400)),
(KfxSymbol::FixedHeight as u64, IonValue::Int(2100)),
(
KfxSymbol::Layout as u64,
IonValue::Symbol(KfxSymbol::ScaleFit as u64),
),
(
KfxSymbol::Float as u64,
IonValue::Symbol(KfxSymbol::Center as u64),
),
])
} else {
IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Int(section_id as i64)),
(
KfxSymbol::StoryName as u64,
IonValue::Symbol(story_name_symbol),
),
(
KfxSymbol::Type as u64,
IonValue::Symbol(KfxSymbol::Text as u64),
),
])
};
let section_ion = IonValue::Struct(vec![
(
KfxSymbol::SectionName as u64,
IonValue::Symbol(section_name_symbol),
),
(
KfxSymbol::PageTemplates as u64,
IonValue::List(vec![page_template]),
),
]);
let section_fragment = KfxFragment::new(KfxSymbol::Section, section_name, section_ion);
(section_fragment, storyline_fragment, content_fragment)
}
pub(super) fn build_cover_storyline(chapter: &Chapter, ctx: &mut ExportContext) -> IonValue {
use crate::model::Role;
for node_id in chapter.iter_dfs() {
let node = match chapter.node(node_id) {
Some(n) => n,
None => continue,
};
if node.role == Role::Image {
if let Some(src) = chapter.semantics.src(node_id) {
let resource_name = ctx.resource_registry.get_or_create_name(src);
let resource_name_symbol = ctx.symbols.get_or_intern(&resource_name);
let style_symbol = ctx.register_style_id(node.style, &chapter.styles);
let container_id = ctx.fragment_ids.next_id();
ctx.record_content_id(container_id);
ctx.record_content_length(container_id, 1);
let image_struct = IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Int(container_id as i64)),
(KfxSymbol::Style as u64, IonValue::Symbol(style_symbol)),
(
KfxSymbol::Type as u64,
IonValue::Symbol(KfxSymbol::Image as u64),
),
(
KfxSymbol::ResourceName as u64,
IonValue::Symbol(resource_name_symbol),
),
]);
return IonValue::List(vec![image_struct]);
}
}
}
IonValue::List(vec![])
}
pub(super) fn build_symbol_table_ion(local_symbols: &[String]) -> Vec<u8> {
use crate::kfx::ion::IonWriter;
use crate::kfx::symbols::KFX_MAX_SYMBOL_ID;
let mut writer = IonWriter::new();
writer.write_bvm();
let import_entry = IonValue::Struct(vec![
(4, IonValue::String("YJ_symbols".to_string())), (5, IonValue::Int(10)), (8, IonValue::Int(KFX_MAX_SYMBOL_ID as i64)), ]);
let symbols_list: Vec<IonValue> = local_symbols
.iter()
.map(|s| IonValue::String(s.clone()))
.collect();
let symbol_table = IonValue::Struct(vec![
(6, IonValue::List(vec![import_entry])), (7, IonValue::List(symbols_list)), ]);
writer.write_annotated(&[3], &symbol_table);
writer.into_bytes()
}
pub(super) fn build_format_capabilities_ion() -> Vec<u8> {
let caps = IonValue::Struct(vec![
(
KfxSymbol::Namespace as u64,
IonValue::String("yj".to_string()),
),
(KfxSymbol::MajorVersion as u64, IonValue::Int(1)),
(KfxSymbol::MinorVersion as u64, IonValue::Int(0)),
(KfxSymbol::Features as u64, IonValue::List(vec![])),
]);
serialize_annotated_ion(KfxSymbol::FormatCapabilities as u64, &caps)
}
#[cfg(test)]
#[allow(clippy::vec_init_then_push, clippy::needless_range_loop)]
mod entity_structure_tests {
use super::*;
use crate::kfx::fragment::FragmentData;
use crate::model::Book;
#[test]
fn test_entity_order_matches_reference() {
let book = Book::open("tests/fixtures/epictetus.epub").unwrap();
let container_id = generate_container_id("test-seed");
let mut ctx = ExportContext::new();
let spine_info: Vec<_> = book
.spine()
.iter()
.enumerate()
.map(|(idx, entry)| {
let section_name = format!("c{}", idx);
(entry.id, section_name)
})
.collect();
for (chapter_id, section_name) in &spine_info {
ctx.register_section(section_name);
let source_path = book.source_id(*chapter_id).unwrap_or("").to_string();
if let Ok(chapter) = book.load_chapter(*chapter_id) {
survey_chapter(&chapter, *chapter_id, &source_path, &mut ctx);
}
}
let mut fragments = Vec::new();
fragments.push(build_content_features_fragment());
fragments.push(build_book_metadata_fragment(&book, &container_id, &ctx));
fragments.push(build_metadata_fragment(&ctx));
fragments.push(build_document_data_fragment(&ctx));
fragments.push(build_book_navigation_fragment_with_positions(&book, &ctx));
let mut section_fragments = Vec::new();
let mut storyline_fragments = Vec::new();
let mut content_fragments = Vec::new();
for (chapter_id, section_name) in &spine_info {
if let Ok(chapter) = book.load_chapter(*chapter_id) {
let (section, storyline, content) =
build_chapter_entities_grouped(&chapter, *chapter_id, section_name, &mut ctx);
section_fragments.push(section);
storyline_fragments.push(storyline);
if let Some(c) = content {
content_fragments.push(c);
}
}
}
fragments.extend(section_fragments);
fragments.extend(storyline_fragments);
fragments.extend(content_fragments);
let types: Vec<u64> = fragments.iter().map(|f| f.ftype).collect();
assert_eq!(types[0], KfxSymbol::ContentFeatures as u64);
assert_eq!(types[1], KfxSymbol::BookMetadata as u64);
assert_eq!(types[2], KfxSymbol::Metadata as u64);
assert_eq!(types[3], KfxSymbol::DocumentData as u64);
assert_eq!(types[4], KfxSymbol::BookNavigation as u64);
let after_header = &types[5..];
let section_count = after_header
.iter()
.take_while(|&&t| t == KfxSymbol::Section as u64)
.count();
assert!(section_count > 0, "should have sections after header");
let after_sections = &after_header[section_count..];
let storyline_count = after_sections
.iter()
.take_while(|&&t| t == KfxSymbol::Storyline as u64)
.count();
assert!(storyline_count > 0, "should have storylines after sections");
let after_storylines = &after_sections[storyline_count..];
let content_count = after_storylines
.iter()
.take_while(|&&t| t == KfxSymbol::Content as u64)
.count();
for t in after_storylines.iter().take(content_count) {
assert_eq!(
*t,
KfxSymbol::Content as u64,
"content should follow storylines"
);
}
for i in 1..section_count {
assert_eq!(
after_header[i],
KfxSymbol::Section as u64,
"sections should be grouped"
);
}
for i in 1..storyline_count {
assert_eq!(
after_sections[i],
KfxSymbol::Storyline as u64,
"storylines should be grouped"
);
}
}
#[test]
fn test_chapter_entities_grouped_returns_correct_types() {
let book = Book::open("tests/fixtures/epictetus.epub").unwrap();
let mut ctx = ExportContext::new();
let spine_entry = book.spine().first().unwrap();
let chapter_id = spine_entry.id;
let section_name = "c0";
ctx.register_section(section_name);
let source_path = book.source_id(chapter_id).unwrap_or("").to_string();
if let Ok(chapter) = book.load_chapter(chapter_id) {
survey_chapter(&chapter, chapter_id, &source_path, &mut ctx);
}
let chapter = book.load_chapter(chapter_id).unwrap();
let (section, storyline, content) =
build_chapter_entities_grouped(&chapter, chapter_id, section_name, &mut ctx);
assert_eq!(section.ftype, KfxSymbol::Section as u64);
assert_eq!(storyline.ftype, KfxSymbol::Storyline as u64);
if let FragmentData::Ion(IonValue::Struct(fields)) = §ion.data {
let has_section_name = fields
.iter()
.any(|(id, _)| *id == KfxSymbol::SectionName as u64);
let has_page_templates = fields
.iter()
.any(|(id, _)| *id == KfxSymbol::PageTemplates as u64);
assert!(has_section_name, "section should have section_name");
assert!(has_page_templates, "section should have page_templates");
}
if let FragmentData::Ion(IonValue::Struct(fields)) = &storyline.data {
let has_story_name = fields
.iter()
.any(|(id, _)| *id == KfxSymbol::StoryName as u64);
let has_content_list = fields
.iter()
.any(|(id, _)| *id == KfxSymbol::ContentList as u64);
assert!(has_story_name, "storyline should have story_name");
assert!(has_content_list, "storyline should have content_list");
}
if let Some(content_frag) = content {
assert_eq!(content_frag.ftype, KfxSymbol::Content as u64);
if let FragmentData::Ion(IonValue::Struct(fields)) = &content_frag.data {
let has_name = fields.iter().any(|(id, _)| *id == KfxSymbol::Name as u64);
let has_content_list = fields
.iter()
.any(|(id, _)| *id == KfxSymbol::ContentList as u64);
assert!(has_name, "content should have name");
assert!(has_content_list, "content should have content_list");
}
}
}
}
#[cfg(test)]
mod section_type_tests {
use super::*;
use crate::kfx::cover::{needs_standalone_cover, normalize_cover_path};
use crate::kfx::fragment::FragmentData;
use crate::model::Book;
#[test]
fn test_titlepage_section_has_text_type_when_standalone_cover_exists() {
let book = Book::open("tests/fixtures/epictetus.epub").unwrap();
let mut ctx = ExportContext::new();
let asset_paths = book.list_assets();
let cover_image = book
.metadata()
.cover_image
.clone()
.expect("should have cover");
let normalized = normalize_cover_path(&cover_image, asset_paths);
let first_chapter_id = book.spine().first().expect("should have spine").id;
let first_chapter = book.load_chapter(first_chapter_id).unwrap();
assert!(
needs_standalone_cover(&normalized, &first_chapter),
"test requires a book with different cover and titlepage images"
);
ctx.register_section("c0");
ctx.register_section("c1");
ctx.cover_fragment_id = Some(ctx.next_fragment_id());
let source_path = book.source_id(first_chapter_id).unwrap_or("").to_string();
let first_chapter = book.load_chapter(first_chapter_id).unwrap();
survey_chapter(&first_chapter, first_chapter_id, &source_path, &mut ctx);
let first_chapter = book.load_chapter(first_chapter_id).unwrap();
let (section, _, _) =
build_chapter_entities_grouped(&first_chapter, first_chapter_id, "c1", &mut ctx);
if let FragmentData::Ion(IonValue::Struct(fields)) = §ion.data {
let page_templates = fields
.iter()
.find(|(id, _)| *id == KfxSymbol::PageTemplates as u64)
.expect("section should have page_templates");
if let (_, IonValue::List(templates)) = page_templates {
let template = &templates[0];
if let IonValue::Struct(template_fields) = template {
let type_field = template_fields
.iter()
.find(|(id, _)| *id == KfxSymbol::Type as u64)
.expect("page_template should have type");
if let (_, IonValue::Symbol(type_sym)) = type_field {
assert_eq!(
*type_sym,
KfxSymbol::Text as u64,
"titlepage (c1) should have type: text when standalone cover exists, \
but got type: container"
);
} else {
panic!("type should be a symbol");
}
}
}
} else {
panic!("section should have Ion struct data");
}
}
}