use super::*;
pub(super) fn build_position_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut entries = Vec::new();
if let Some(cover_fid) = ctx.cover_fragment_id {
let mut contains_list = vec![IonValue::Int(cover_fid as i64)];
if let Some(content_id) = ctx.cover_content_id {
contains_list.push(IonValue::Int(content_id as i64));
}
let entry = IonValue::Struct(vec![
(KfxSymbol::Contains as u64, IonValue::List(contains_list)),
(
KfxSymbol::SectionName as u64,
IonValue::Symbol(ctx.section_ids[0]),
),
]);
entries.push(entry);
}
for &(section_sym, chapter_id) in &ctx.spine_section_chapters {
let Some(&fragment_id) = ctx.chapter_fragments.get(&chapter_id) else {
continue;
};
let mut eid_list = Vec::new();
eid_list.push(IonValue::Int(fragment_id as i64));
if let Some(content_ids) = ctx.content_ids_by_chapter.get(&chapter_id) {
for &content_id in content_ids {
eid_list.push(IonValue::Int(content_id as i64));
}
}
let entry = IonValue::Struct(vec![
(KfxSymbol::Contains as u64, IonValue::List(eid_list)),
(KfxSymbol::SectionName as u64, IonValue::Symbol(section_sym)),
]);
entries.push(entry);
}
let ion = IonValue::List(entries);
KfxFragment::singleton(KfxSymbol::PositionMap, ion)
}
struct PositionChunk {
eid: u64,
length: i64,
section: usize,
}
fn position_chunks(ctx: &ExportContext) -> Vec<PositionChunk> {
let content_len = |eid: u64| -> i64 {
ctx.content_id_lengths
.get(&eid)
.copied()
.unwrap_or(1)
.max(1) as i64
};
let mut chunks = Vec::new();
let mut section = 0usize;
if let Some(cover_fid) = ctx.cover_fragment_id {
chunks.push(PositionChunk {
eid: cover_fid,
length: 1,
section,
});
if let Some(content_id) = ctx.cover_content_id {
chunks.push(PositionChunk {
eid: content_id,
length: content_len(content_id),
section,
});
}
section += 1;
}
for &(_, chapter_id) in &ctx.spine_section_chapters {
let Some(&fragment_id) = ctx.chapter_fragments.get(&chapter_id) else {
continue;
};
chunks.push(PositionChunk {
eid: fragment_id,
length: 1,
section,
});
if let Some(content_ids) = ctx.content_ids_by_chapter.get(&chapter_id) {
for &eid in content_ids {
chunks.push(PositionChunk {
eid,
length: content_len(eid),
section,
});
}
}
section += 1;
}
chunks
}
pub(super) fn page_position_chunks(ctx: &ExportContext) -> Vec<(u64, i64, i64)> {
let mut out = Vec::new();
let mut pid = 0i64;
for chunk in position_chunks(ctx) {
out.push((chunk.eid, chunk.length, pid));
pid += chunk.length;
}
out
}
pub(super) fn build_position_id_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut entries = Vec::new();
let mut pid = 0i64;
for chunk in position_chunks(ctx) {
entries.push(IonValue::Struct(vec![
(KfxSymbol::Eid as u64, IonValue::Int(chunk.eid as i64)),
(KfxSymbol::Pid as u64, IonValue::Int(pid)),
]));
pid += chunk.length;
}
entries.push(IonValue::Struct(vec![
(KfxSymbol::Eid as u64, IonValue::Int(0)),
(KfxSymbol::Pid as u64, IonValue::Int(pid)),
]));
let ion = IonValue::List(entries);
KfxFragment::singleton(KfxSymbol::PositionIdMap, ion)
}
const KFX_POSITIONS_PER_LOCATION: i64 = 150;
pub(super) fn build_location_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut location_entries = Vec::new();
for chunk in position_chunks(ctx) {
let mut offset = 0i64;
while offset < chunk.length.max(1) {
location_entries.push(IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Int(chunk.eid as i64)),
(KfxSymbol::Offset as u64, IonValue::Int(offset)),
]));
offset += KFX_POSITIONS_PER_LOCATION;
}
}
let ion = IonValue::List(vec![IonValue::Struct(vec![(
KfxSymbol::Locations as u64,
IonValue::List(location_entries),
)])]);
KfxFragment::singleton(KfxSymbol::LocationMap, ion)
}
pub(super) fn build_container_entity_map_fragment(
container_id: &str,
fragments: &[KfxFragment],
ctx: &ExportContext,
) -> KfxFragment {
let mut entity_names: Vec<IonValue> = Vec::new();
for frag in fragments {
if frag.fid.starts_with('$') {
continue;
}
if let Some(symbol_id) = ctx.symbols.get(&frag.fid) {
entity_names.push(IonValue::Symbol(symbol_id));
}
}
let container_entry = IonValue::Struct(vec![
(
KfxSymbol::Id as u64,
IonValue::String(container_id.to_string()),
),
(KfxSymbol::Contains as u64, IonValue::List(entity_names)),
]);
let mut dependencies: Vec<IonValue> = Vec::new();
for (section_name, short_names) in &ctx.section_resource_deps {
if short_names.is_empty() {
continue;
}
let Some(section_sym) = ctx.symbols.get(section_name) else {
continue;
};
let deps: Vec<IonValue> = short_names
.iter()
.filter_map(|n| ctx.symbols.get(n).map(IonValue::Symbol))
.collect();
if deps.is_empty() {
continue;
}
dependencies.push(IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Symbol(section_sym)),
(
KfxSymbol::MandatoryDependencies as u64,
IonValue::List(deps),
),
]));
}
let mut all_short_names: BTreeSet<&String> = BTreeSet::new();
for short_names in ctx.section_resource_deps.values() {
for n in short_names {
all_short_names.insert(n);
}
}
for short_name in all_short_names {
let Some(resource_sym) = ctx.symbols.get(short_name) else {
continue;
};
let raw_name = format!("resource/{short_name}");
let Some(raw_sym) = ctx.symbols.get(&raw_name) else {
continue;
};
dependencies.push(IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Symbol(resource_sym)),
(
KfxSymbol::MandatoryDependencies as u64,
IonValue::List(vec![IonValue::Symbol(raw_sym)]),
),
]));
}
let mut ion_fields = vec![(
KfxSymbol::ContainerList as u64,
IonValue::List(vec![container_entry]),
)];
if !dependencies.is_empty() {
ion_fields.push((
KfxSymbol::EntityDependencies as u64,
IonValue::List(dependencies),
));
}
let ion = IonValue::Struct(ion_fields);
KfxFragment::singleton(KfxSymbol::ContainerEntityMap, ion)
}
pub(super) fn max_section_position_count(ctx: &ExportContext) -> i64 {
let mut per_section: Vec<i64> = Vec::new();
for chunk in position_chunks(ctx) {
if per_section.len() <= chunk.section {
per_section.resize(chunk.section + 1, 0);
}
per_section[chunk.section] += chunk.length;
}
per_section.into_iter().max().unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ChapterId;
use crate::kfx::fragment::FragmentData;
fn extract_entries(frag: &KfxFragment) -> Vec<(u64, Vec<i64>)> {
let FragmentData::Ion(IonValue::List(entries)) = &frag.data else {
panic!("expected Ion list fragment data");
};
entries
.iter()
.map(|entry| {
let IonValue::Struct(fields) = entry else {
panic!("expected struct entry");
};
let mut section = None;
let mut eids = Vec::new();
for (id, value) in fields {
if *id == KfxSymbol::SectionName as u64 {
if let IonValue::Symbol(s) = value {
section = Some(*s);
}
} else if *id == KfxSymbol::Contains as u64
&& let IonValue::List(list) = value
{
for item in list {
if let IonValue::Int(i) = item {
eids.push(*i);
}
}
}
}
(section.expect("entry must have section_name"), eids)
})
.collect()
}
fn extract_locations(frag: &KfxFragment) -> Vec<(i64, i64)> {
let FragmentData::Ion(IonValue::List(outer)) = &frag.data else {
panic!("expected list");
};
let IonValue::Struct(fields) = &outer[0] else {
panic!("expected struct");
};
let (_, IonValue::List(entries)) = fields
.iter()
.find(|(id, _)| *id == KfxSymbol::Locations as u64)
.expect("locations field")
else {
panic!("expected list");
};
entries
.iter()
.map(|e| {
let IonValue::Struct(f) = e else { panic!() };
let mut id = 0;
let mut off = 0;
for (k, v) in f {
if let IonValue::Int(n) = v {
if *k == KfxSymbol::Id as u64 {
id = *n;
} else if *k == KfxSymbol::Offset as u64 {
off = *n;
}
}
}
(id, off)
})
.collect()
}
#[test]
fn location_map_marks_element_starts_and_150_interior_fills() {
let mut ctx = ExportContext::new();
let ch = ChapterId(1);
ctx.register_spine_section("c0", ch);
ctx.chapter_fragments.insert(ch, 90);
ctx.content_ids_by_chapter.insert(ch, vec![100, 101, 102]);
ctx.content_id_lengths.insert(100, 40); ctx.content_id_lengths.insert(101, 380); ctx.content_id_lengths.insert(102, 1);
let frag = build_location_map_fragment(&ctx);
let locs = extract_locations(&frag);
assert_eq!(
locs,
vec![
(90, 0), (100, 0), (101, 0), (101, 150), (101, 300), (102, 0), ]
);
}
#[test]
fn position_map_pairs_sections_with_chapters_by_key() {
let mut ctx = ExportContext::new();
let (ch1, ch2, ch3) = (ChapterId(1), ChapterId(2), ChapterId(3));
let c0 = ctx.register_spine_section("c0", ch1);
let c1 = ctx.register_spine_section("c1", ch2);
let c2 = ctx.register_spine_section("c2", ch3);
ctx.chapter_fragments.insert(ch1, 90);
ctx.chapter_fragments.insert(ch2, 95);
ctx.chapter_fragments.insert(ch3, 100);
ctx.content_ids_by_chapter.insert(ch1, vec![91]);
ctx.content_ids_by_chapter.insert(ch2, vec![96, 97]);
ctx.content_ids_by_chapter.insert(ch3, vec![101]);
let frag = build_position_map_fragment(&ctx);
let entries = extract_entries(&frag);
assert_eq!(
entries,
vec![
(c0, vec![90, 91]),
(c1, vec![95, 96, 97]),
(c2, vec![100, 101]),
]
);
}
#[test]
fn position_map_skips_failed_chapter_without_shifting_sections() {
let mut ctx = ExportContext::new();
let (ch1, ch2, ch3) = (ChapterId(1), ChapterId(2), ChapterId(3));
let c0 = ctx.register_spine_section("c0", ch1);
let _c1 = ctx.register_spine_section("c1", ch2);
let c2 = ctx.register_spine_section("c2", ch3);
ctx.chapter_fragments.insert(ch1, 90);
ctx.chapter_fragments.insert(ch3, 100);
ctx.content_ids_by_chapter.insert(ch1, vec![91]);
ctx.content_ids_by_chapter.insert(ch3, vec![101]);
let frag = build_position_map_fragment(&ctx);
let entries = extract_entries(&frag);
assert_eq!(entries, vec![(c0, vec![90, 91]), (c2, vec![100, 101])]);
}
}