use std::collections::{HashMap, HashSet};
use crate::book::{LandmarkType, TocEntry};
use crate::import::ChapterId;
use crate::ir::{NodeId, StyleId};
use super::style_registry::StyleRegistry;
use super::symbols::KFX_SYMBOL_TABLE_SIZE;
use super::transforms::encode_base32;
pub struct SymbolTable {
local_symbols: Vec<String>,
symbol_map: HashMap<String, u64>,
next_id: u64,
}
impl SymbolTable {
pub const LOCAL_MIN_ID: u64 = KFX_SYMBOL_TABLE_SIZE as u64;
pub fn new() -> Self {
Self {
local_symbols: Vec::new(),
symbol_map: HashMap::new(),
next_id: Self::LOCAL_MIN_ID,
}
}
pub fn get_or_intern(&mut self, name: &str) -> u64 {
if let Some(id_str) = name.strip_prefix('$')
&& let Ok(id) = id_str.parse::<u64>()
{
return id;
}
if let Some(&id) = self.symbol_map.get(name) {
return id;
}
let id = self.next_id;
self.next_id += 1;
self.local_symbols.push(name.to_string());
self.symbol_map.insert(name.to_string(), id);
id
}
pub fn get(&self, name: &str) -> Option<u64> {
if let Some(id_str) = name.strip_prefix('$')
&& let Ok(id) = id_str.parse::<u64>()
{
return Some(id);
}
self.symbol_map.get(name).copied()
}
pub fn local_symbols(&self) -> &[String] {
&self.local_symbols
}
pub fn len(&self) -> usize {
self.local_symbols.len()
}
pub fn is_empty(&self) -> bool {
self.local_symbols.is_empty()
}
}
impl Default for SymbolTable {
fn default() -> Self {
Self::new()
}
}
pub struct IdGenerator {
next_id: u64,
}
impl IdGenerator {
pub const FRAGMENT_MIN_ID: u64 = 866;
pub fn new() -> Self {
Self {
next_id: Self::FRAGMENT_MIN_ID,
}
}
pub fn next_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id += 1;
id
}
pub fn peek(&self) -> u64 {
self.next_id
}
}
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ResourceRegistry {
resources: HashMap<String, u64>,
resource_names: HashMap<String, String>,
next_resource_id: usize,
}
impl ResourceRegistry {
pub fn new() -> Self {
Self {
resources: HashMap::new(),
resource_names: HashMap::new(),
next_resource_id: 0,
}
}
pub fn register(&mut self, href: &str, symbols: &mut SymbolTable) -> u64 {
if let Some(&id) = self.resources.get(href) {
return id;
}
let symbol_name = format!("resource:{}", href);
let id = symbols.get_or_intern(&symbol_name);
self.resources.insert(href.to_string(), id);
id
}
pub fn get_or_create_name(&mut self, href: &str) -> String {
if let Some(name) = self.resource_names.get(href) {
return name.clone();
}
let name = format!("e{:X}", self.next_resource_id);
self.next_resource_id += 1;
self.resource_names.insert(href.to_string(), name.clone());
name
}
pub fn get(&self, href: &str) -> Option<u64> {
self.resources.get(href).copied()
}
pub fn get_name(&self, href: &str) -> Option<&str> {
self.resource_names.get(href).map(|s| s.as_str())
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &u64)> {
self.resources.iter()
}
pub fn len(&self) -> usize {
self.resource_names.len()
}
pub fn is_empty(&self) -> bool {
self.resource_names.is_empty()
}
}
impl Default for ResourceRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Default)]
pub struct TextAccumulator {
segments: Vec<String>,
total_len: usize,
}
impl TextAccumulator {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, text: &str) -> usize {
let index = self.segments.len();
self.total_len += text.len();
self.segments.push(text.to_string());
index
}
pub fn len(&self) -> usize {
self.total_len
}
pub fn is_empty(&self) -> bool {
self.total_len == 0
}
pub fn segments(&self) -> &[String] {
&self.segments
}
pub fn drain(&mut self) -> Vec<String> {
self.total_len = 0;
std::mem::take(&mut self.segments)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Position {
pub fragment_id: u64,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct AnchorPosition {
pub symbol: String,
pub anchor_name: String,
pub fragment_id: u64,
pub section_id: u64,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct ExternalAnchor {
pub symbol: String,
pub uri: String,
}
#[derive(Debug, Default)]
pub struct AnchorRegistry {
link_to_symbol: HashMap<String, String>,
anchor_to_symbol: HashMap<String, String>,
resolved_symbols: HashSet<String>,
resolved: Vec<AnchorPosition>,
external_anchors: Vec<ExternalAnchor>,
next_anchor_id: usize,
anchor_positions: HashMap<String, (u64, usize)>,
}
impl AnchorRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_link_target(&mut self, href: &str) -> String {
if let Some(symbol) = self.link_to_symbol.get(href) {
return symbol.clone();
}
let symbol = format!("a{:X}", self.next_anchor_id);
self.next_anchor_id += 1;
self.link_to_symbol.insert(href.to_string(), symbol.clone());
if href.starts_with("http://") || href.starts_with("https://") {
self.external_anchors.push(ExternalAnchor {
symbol: symbol.clone(),
uri: href.to_string(),
});
} else {
self.anchor_to_symbol
.insert(href.to_string(), symbol.clone());
}
symbol
}
pub fn get_symbol(&self, href: &str) -> Option<&str> {
self.link_to_symbol.get(href).map(|s| s.as_str())
}
pub fn get_symbol_for_anchor(&self, anchor_name: &str) -> Option<&str> {
self.anchor_to_symbol.get(anchor_name).map(|s| s.as_str())
}
pub fn resolve_anchor(
&mut self,
anchor_or_href: &str,
fragment_id: u64,
section_id: u64,
offset: usize,
) {
let anchor_name = extract_fragment(anchor_or_href).unwrap_or(anchor_or_href);
if let Some(symbol) = self.anchor_to_symbol.get(anchor_name).cloned() {
if self.resolved_symbols.contains(&symbol) {
return;
}
self.resolved_symbols.insert(symbol.clone());
self.resolved.push(AnchorPosition {
symbol,
anchor_name: anchor_name.to_string(),
fragment_id,
section_id,
offset,
});
}
}
pub fn is_anchor_needed(&self, anchor_name: &str) -> bool {
self.anchor_to_symbol.contains_key(anchor_name)
}
pub fn create_content_anchor(
&mut self,
anchor_name: &str,
content_fragment_id: u64,
section_id: u64,
offset: usize,
) -> Option<String> {
let symbol = if let Some(existing) = self.anchor_to_symbol.get(anchor_name) {
existing.clone()
} else {
let symbol = format!("a{:X}", self.next_anchor_id);
self.next_anchor_id += 1;
self.anchor_to_symbol
.insert(anchor_name.to_string(), symbol.clone());
symbol
};
if self.resolved_symbols.contains(&symbol) {
return None;
}
self.resolved_symbols.insert(symbol.clone());
self.resolved.push(AnchorPosition {
symbol: symbol.clone(),
anchor_name: anchor_name.to_string(),
fragment_id: content_fragment_id,
section_id,
offset,
});
self.anchor_positions
.insert(anchor_name.to_string(), (content_fragment_id, offset));
Some(symbol)
}
pub fn record_position(&mut self, anchor_name: &str, fragment_id: u64, offset: usize) {
self.anchor_positions
.entry(anchor_name.to_string())
.or_insert((fragment_id, offset));
}
pub fn get_anchor_position(&self, anchor_name: &str) -> Option<(u64, usize)> {
self.anchor_positions.get(anchor_name).copied()
}
pub fn drain_anchors(&mut self) -> Vec<AnchorPosition> {
std::mem::take(&mut self.resolved)
}
pub fn drain_external_anchors(&mut self) -> Vec<ExternalAnchor> {
std::mem::take(&mut self.external_anchors)
}
pub fn len(&self) -> usize {
self.link_to_symbol.len()
}
pub fn is_empty(&self) -> bool {
self.link_to_symbol.is_empty()
}
}
fn extract_fragment(href: &str) -> Option<&str> {
href.find('#').map(|i| &href[i + 1..])
}
pub struct ExportContext {
pub symbols: SymbolTable,
pub fragment_ids: IdGenerator,
pub resource_registry: ResourceRegistry,
pub section_ids: Vec<u64>,
text_accumulator: TextAccumulator,
pub current_content_name: u64,
pub position_map: HashMap<(ChapterId, NodeId), Position>,
pub chapter_fragments: HashMap<ChapterId, u64>,
current_chapter: Option<ChapterId>,
current_chapter_path: Option<String>,
current_fragment_id: u64,
current_text_offset: usize,
pub anchor_map: HashMap<String, (ChapterId, NodeId)>,
pub path_to_fragment: HashMap<String, u64>,
needed_anchors: HashSet<String>,
pub default_style_symbol: u64,
pub style_registry: StyleRegistry,
pub anchor_registry: AnchorRegistry,
pub landmark_fragments: HashMap<LandmarkType, LandmarkTarget>,
pub nav_container_symbols: NavContainerSymbols,
pub heading_positions: Vec<HeadingPosition>,
pub cover_fragment_id: Option<u64>,
pub cover_content_id: Option<u64>,
paths_needing_chapter_start_anchor: HashSet<String>,
pending_chapter_start_anchor: Option<String>,
pub first_content_ids: HashMap<String, u64>,
pub content_ids_by_path: HashMap<String, Vec<u64>>,
pub content_ids_by_chapter: HashMap<ChapterId, Vec<u64>>,
pub content_id_lengths: HashMap<u64, usize>,
current_export_path: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HeadingPosition {
pub level: u8,
pub fragment_id: u64,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct LandmarkTarget {
pub fragment_id: u64,
pub offset: u64,
pub label: String,
}
#[derive(Debug, Clone, Default)]
pub struct NavContainerSymbols {
pub toc: u64,
pub headings: u64,
pub landmarks: u64,
}
impl ExportContext {
pub fn new() -> Self {
let mut symbols = SymbolTable::new();
let default_style_symbol = symbols.get_or_intern("s0");
Self {
symbols,
fragment_ids: IdGenerator::new(),
resource_registry: ResourceRegistry::new(),
section_ids: Vec::new(),
text_accumulator: TextAccumulator::new(),
current_content_name: 0,
position_map: HashMap::new(),
chapter_fragments: HashMap::new(),
current_chapter: None,
current_chapter_path: None,
current_fragment_id: 0,
current_text_offset: 0,
anchor_map: HashMap::new(),
path_to_fragment: HashMap::new(),
needed_anchors: HashSet::new(),
default_style_symbol,
style_registry: StyleRegistry::new(default_style_symbol),
anchor_registry: AnchorRegistry::new(),
landmark_fragments: HashMap::new(),
nav_container_symbols: NavContainerSymbols::default(),
heading_positions: Vec::new(),
cover_fragment_id: None,
cover_content_id: None,
paths_needing_chapter_start_anchor: HashSet::new(),
pending_chapter_start_anchor: None,
first_content_ids: HashMap::new(),
content_ids_by_path: HashMap::new(),
content_ids_by_chapter: HashMap::new(),
content_id_lengths: HashMap::new(),
current_export_path: None,
}
}
pub fn begin_chapter(&mut self, content_name: &str) -> u64 {
self.text_accumulator = TextAccumulator::new();
self.current_content_name = self.symbols.get_or_intern(content_name);
self.current_content_name
}
pub fn begin_chapter_export(&mut self, chapter_id: ChapterId, source_path: &str) {
self.current_chapter = Some(chapter_id);
self.current_chapter_path = Some(source_path.to_string());
self.current_export_path = Some(source_path.to_string());
if self
.paths_needing_chapter_start_anchor
.contains(source_path)
{
self.pending_chapter_start_anchor = Some(source_path.to_string());
} else {
self.pending_chapter_start_anchor = None;
}
}
pub fn intern(&mut self, s: &str) -> u64 {
self.symbols.get_or_intern(s)
}
pub fn append_text(&mut self, text: &str) -> (usize, usize) {
let offset = self.text_accumulator.len();
let index = self.text_accumulator.push(text);
(index, offset)
}
pub fn text_accumulator(&self) -> &TextAccumulator {
&self.text_accumulator
}
pub fn drain_text(&mut self) -> Vec<String> {
self.text_accumulator.drain()
}
pub fn next_fragment_id(&mut self) -> u64 {
self.fragment_ids.next_id()
}
pub fn register_section(&mut self, name: &str) -> u64 {
let id = self.intern(name);
self.section_ids.push(id);
id
}
pub fn register_ir_style(&mut self, ir_style: &crate::ir::ComputedStyle) -> u64 {
let schema = crate::kfx::style_schema::StyleSchema::standard();
let mut builder = crate::kfx::style_registry::StyleBuilder::new(&schema);
builder.ingest_ir_style(ir_style);
let kfx_style = builder.build();
self.style_registry.register(kfx_style, &mut self.symbols)
}
pub fn register_style_id(
&mut self,
style_id: StyleId,
style_pool: &crate::ir::StylePool,
) -> u64 {
if style_id == StyleId::DEFAULT {
return self.default_style_symbol;
}
if let Some(ir_style) = style_pool.get(style_id) {
self.register_ir_style(ir_style)
} else {
self.default_style_symbol
}
}
pub fn begin_chapter_survey(&mut self, chapter_id: ChapterId, path: &str) -> u64 {
let fragment_id = self.fragment_ids.next_id();
self.chapter_fragments.insert(chapter_id, fragment_id);
self.path_to_fragment.insert(path.to_string(), fragment_id);
self.current_chapter = Some(chapter_id);
self.current_fragment_id = fragment_id;
self.current_text_offset = 0;
if self.needed_anchors.contains(path) {
self.paths_needing_chapter_start_anchor
.insert(path.to_string());
}
self.current_chapter_path = Some(path.to_string());
fragment_id
}
pub fn end_chapter_survey(&mut self) {
self.current_chapter = None;
self.current_chapter_path = None;
}
pub fn get_fragment_for_path(&self, path: &str) -> Option<u64> {
self.path_to_fragment.get(path).copied()
}
pub fn record_position(&mut self, node_id: NodeId) {
if let Some(chapter_id) = self.current_chapter {
self.position_map.insert(
(chapter_id, node_id),
Position {
fragment_id: self.current_fragment_id,
offset: self.current_text_offset,
},
);
}
}
pub fn record_heading(&mut self, level: u8) {
self.heading_positions.push(HeadingPosition {
level,
fragment_id: self.current_fragment_id,
offset: self.current_text_offset,
});
}
pub fn record_heading_with_id(&mut self, level: u8, fragment_id: u64) {
self.heading_positions.push(HeadingPosition {
level,
fragment_id,
offset: 0, });
}
pub fn resolve_pending_chapter_start_anchor(&mut self, first_content_id: u64) {
if let Some(ref path) = self.current_export_path
&& !self.first_content_ids.contains_key(path)
{
self.first_content_ids
.insert(path.clone(), first_content_id);
}
let section_id = self
.current_chapter
.and_then(|ch| self.chapter_fragments.get(&ch).copied())
.unwrap_or(first_content_id);
if let Some(path) = self.pending_chapter_start_anchor.take()
&& let Some(symbol) =
self.anchor_registry
.create_content_anchor(&path, first_content_id, section_id, 0)
{
self.symbols.get_or_intern(&symbol);
}
}
pub fn create_anchor_if_needed(&mut self, anchor_id: &str, content_id: u64, offset: usize) {
let section_id = self
.current_chapter
.and_then(|ch| self.chapter_fragments.get(&ch).copied())
.unwrap_or(content_id);
let full_key = self.build_anchor_key(anchor_id);
self.anchor_registry
.record_position(&full_key, content_id, offset);
if self.needed_anchors.contains(&full_key)
&& let Some(symbol) = self
.anchor_registry
.create_content_anchor(&full_key, content_id, section_id, offset)
{
self.symbols.get_or_intern(&symbol);
}
}
pub fn record_content_id(&mut self, content_id: u64) {
if let Some(chapter_id) = self.current_chapter {
self.content_ids_by_chapter
.entry(chapter_id)
.or_default()
.push(content_id);
}
}
pub fn record_content_length(&mut self, content_id: u64, text_len: usize) {
self.content_id_lengths.insert(content_id, text_len);
}
pub fn register_needed_anchor(&mut self, anchor_id: &str) {
self.needed_anchors.insert(anchor_id.to_string());
}
pub fn build_anchor_key(&self, anchor_id: &str) -> String {
if let Some(ref path) = self.current_chapter_path {
format!("{}#{}", path, anchor_id)
} else {
anchor_id.to_string()
}
}
#[cfg(test)]
pub fn get_current_chapter_path(&self) -> Option<&str> {
self.current_chapter_path.as_deref()
}
#[cfg(test)]
pub fn needed_anchor_count(&self) -> usize {
self.needed_anchors.len()
}
#[cfg(test)]
pub fn has_needed_anchor(&self, key: &str) -> bool {
self.needed_anchors.contains(key)
}
#[cfg(test)]
pub fn find_needed_anchors(&self, pattern: &str) -> Vec<&str> {
self.needed_anchors
.iter()
.filter(|a| a.contains(pattern))
.map(|s| s.as_str())
.collect()
}
pub fn is_anchor_needed(&self, anchor_id: &str) -> bool {
let full_key = self.build_anchor_key(anchor_id);
self.needed_anchors.contains(&full_key)
}
pub fn record_anchor(&mut self, anchor_id: &str, node_id: NodeId) {
let full_key = self.build_anchor_key(anchor_id);
if !self.needed_anchors.contains(&full_key) {
return;
}
self.intern(&full_key);
self.record_position(node_id);
if let Some(chapter_id) = self.current_chapter {
self.anchor_map.insert(full_key, (chapter_id, node_id));
}
}
pub fn advance_text_offset(&mut self, text_len: usize) {
self.current_text_offset += text_len;
}
pub fn current_fragment_id(&self) -> u64 {
self.current_fragment_id
}
pub fn current_text_offset(&self) -> usize {
self.current_text_offset
}
pub fn get_position(&self, chapter_id: ChapterId, node_id: NodeId) -> Option<Position> {
self.position_map.get(&(chapter_id, node_id)).copied()
}
pub fn get_chapter_fragment(&self, chapter_id: ChapterId) -> Option<u64> {
self.chapter_fragments.get(&chapter_id).copied()
}
pub fn max_eid(&self) -> u64 {
if self.fragment_ids.peek() > IdGenerator::FRAGMENT_MIN_ID {
self.fragment_ids.peek() - 1
} else {
0
}
}
pub fn format_kindle_pos(fragment_id: u64, offset: usize) -> String {
let fid_encoded = encode_base32(fragment_id as u32, 4);
let off_encoded = encode_base32(offset as u32, 10);
format!("kindle:pos:fid:{}:off:{}", fid_encoded, off_encoded)
}
pub fn register_toc_anchors(&mut self, entries: &[TocEntry]) {
for entry in entries {
self.register_needed_anchor(&entry.href);
if !entry.children.is_empty() {
self.register_toc_anchors(&entry.children);
}
}
}
pub fn fix_landmark_content_ids(&mut self, source_to_chapter: &HashMap<String, ChapterId>) {
let chapter_to_source: HashMap<ChapterId, &String> = source_to_chapter
.iter()
.map(|(path, cid)| (*cid, path))
.collect();
for target in self.landmark_fragments.values_mut() {
let mut found_source = None;
for (cid, &fid) in &self.chapter_fragments {
if fid == target.fragment_id
&& let Some(path) = chapter_to_source.get(cid)
{
found_source = Some((*path).clone());
break;
}
}
if let Some(source_path) = found_source
&& let Some(&content_id) = self.first_content_ids.get(&source_path)
{
target.fragment_id = content_id;
}
}
}
}
impl Default for ExportContext {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_table_shared_symbols() {
let mut symtab = SymbolTable::new();
assert_eq!(symtab.get_or_intern("$260"), 260);
assert_eq!(symtab.get_or_intern("$145"), 145);
}
#[test]
fn test_symbol_table_local_symbols() {
let mut symtab = SymbolTable::new();
let id1 = symtab.get_or_intern("section-1");
let id2 = symtab.get_or_intern("section-2");
assert!(id1 >= SymbolTable::LOCAL_MIN_ID);
assert_eq!(id2, id1 + 1);
assert_eq!(symtab.get_or_intern("section-1"), id1);
}
#[test]
fn test_id_generator() {
let mut id_gen = IdGenerator::new();
assert_eq!(id_gen.next_id(), 866);
assert_eq!(id_gen.next_id(), 867);
assert_eq!(id_gen.next_id(), 868);
}
#[test]
fn test_resource_registry() {
let mut symbols = SymbolTable::new();
let mut registry = ResourceRegistry::new();
let id1 = registry.register("images/cover.jpg", &mut symbols);
let id2 = registry.register("images/cover.jpg", &mut symbols);
let id3 = registry.register("images/other.jpg", &mut symbols);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_resource_registry_unique_names() {
let mut registry = ResourceRegistry::new();
let name1 = registry.get_or_create_name("images/cover.jpg");
let name2 = registry.get_or_create_name("images/photo.png");
let name3 = registry.get_or_create_name("images/logo.gif");
assert_eq!(name1, "e0");
assert_eq!(name2, "e1");
assert_eq!(name3, "e2");
assert_eq!(registry.get_or_create_name("images/cover.jpg"), "e0");
assert_eq!(registry.get_or_create_name("images/photo.png"), "e1");
assert_eq!(registry.get_name("images/cover.jpg"), Some("e0"));
assert_eq!(registry.get_name("images/unknown.jpg"), None);
}
#[test]
fn test_text_accumulator() {
let mut acc = TextAccumulator::new();
let idx1 = acc.push("Hello");
let idx2 = acc.push(" World");
assert_eq!(idx1, 0);
assert_eq!(idx2, 1);
assert_eq!(acc.len(), 11);
assert_eq!(acc.segments().len(), 2);
}
#[test]
fn test_export_context() {
let mut ctx = ExportContext::new();
let id1 = ctx.intern("section-1");
let id2 = ctx.intern("section-1");
assert_eq!(id1, id2);
let fid1 = ctx.next_fragment_id();
let fid2 = ctx.next_fragment_id();
assert_eq!(fid1, 866);
assert_eq!(fid2, 867);
let (idx, offset) = ctx.append_text("Hello");
assert_eq!(idx, 0);
assert_eq!(offset, 0);
let (idx, offset) = ctx.append_text("World");
assert_eq!(idx, 1);
assert_eq!(offset, 5);
}
#[test]
fn test_cross_file_anchor_resolution() {
let mut ctx = ExportContext::new();
let full_href = "OEBPS/text/endnotes.xhtml#note-1";
ctx.anchor_registry.register_link_target(full_href);
ctx.register_needed_anchor(full_href);
assert!(ctx.anchor_registry.get_symbol(full_href).is_some());
let symbol = ctx.anchor_registry.get_symbol(full_href).unwrap();
assert_eq!(symbol, "a0");
ctx.begin_chapter_export(ChapterId(1), "OEBPS/text/endnotes.xhtml");
assert_eq!(ctx.build_anchor_key("note-1"), full_href);
let content_id = 123;
ctx.create_anchor_if_needed("note-1", content_id, 0);
let anchors = ctx.anchor_registry.drain_anchors();
assert_eq!(anchors.len(), 1, "Expected one anchor to be created");
let anchor = &anchors[0];
assert_eq!(anchor.symbol, "a0");
assert_eq!(anchor.fragment_id, content_id);
assert_eq!(anchor.offset, 0);
}
#[test]
fn test_anchor_not_created_if_not_needed() {
let mut ctx = ExportContext::new();
ctx.begin_chapter_export(ChapterId(1), "OEBPS/text/chapter1.xhtml");
ctx.create_anchor_if_needed("unused-id", 123, 0);
let anchors = ctx.anchor_registry.drain_anchors();
assert!(
anchors.is_empty(),
"No anchor should be created for unneeded ID"
);
}
}