use hwpforge_blueprint::registry::StyleRegistry;
use hwpforge_core::caption::Caption;
use hwpforge_core::control::Control;
use hwpforge_core::document::{Document, Draft, Validated};
use hwpforge_core::image::Image;
use hwpforge_core::paragraph::Paragraph;
use hwpforge_core::run::{Run, RunContent};
use hwpforge_core::section::Section;
use hwpforge_core::table::Table;
use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
use crate::default_styles::HancomStyleSet;
use crate::error::{HwpxError, HwpxResult};
use crate::style_store::{HwpxStyleStore, RegistryStoreBuild};
#[derive(Debug, Clone)]
pub struct HwpxRegistryBridge {
style_store: HwpxStyleStore,
char_shape_map: Vec<CharShapeIndex>,
para_shape_map: Vec<ParaShapeIndex>,
}
impl HwpxRegistryBridge {
pub fn from_registry(registry: &StyleRegistry) -> HwpxResult<Self> {
Self::from_registry_with(registry, HancomStyleSet::default())
}
pub fn from_registry_with(
registry: &StyleRegistry,
style_set: HancomStyleSet,
) -> HwpxResult<Self> {
let RegistryStoreBuild { store, char_shape_map, para_shape_map } =
HwpxStyleStore::from_registry_with_mappings(registry, style_set)?;
Ok(Self { style_store: store, char_shape_map, para_shape_map })
}
pub fn style_store(&self) -> &HwpxStyleStore {
&self.style_store
}
pub fn into_style_store(self) -> HwpxStyleStore {
self.style_store
}
pub fn rebind_draft_document(
&self,
mut document: Document<Draft>,
) -> HwpxResult<Document<Draft>> {
self.rebind_sections(document.sections_mut())?;
Ok(document)
}
pub fn rebind_validated_document(
&self,
document: &Document<Validated>,
) -> HwpxResult<Document<Validated>> {
let mut draft = Document::with_metadata(document.metadata().clone());
for section in document.sections().iter().cloned() {
draft.add_section(section);
}
self.rebind_draft_document(draft)?.validate().map_err(Into::into)
}
fn rebind_sections(&self, sections: &mut [Section]) -> HwpxResult<()> {
for section in sections {
self.rebind_section(section)?;
}
Ok(())
}
fn rebind_section(&self, section: &mut Section) -> HwpxResult<()> {
self.rebind_paragraphs(&mut section.paragraphs)?;
if let Some(header) = section.header.as_mut() {
self.rebind_paragraphs(&mut header.paragraphs)?;
}
if let Some(footer) = section.footer.as_mut() {
self.rebind_paragraphs(&mut footer.paragraphs)?;
}
if let Some(master_pages) = section.master_pages.as_mut() {
for master_page in master_pages {
self.rebind_paragraphs(&mut master_page.paragraphs)?;
}
}
Ok(())
}
fn rebind_paragraphs(&self, paragraphs: &mut [Paragraph]) -> HwpxResult<()> {
for paragraph in paragraphs {
self.rebind_paragraph(paragraph)?;
}
Ok(())
}
fn rebind_paragraph(&self, paragraph: &mut Paragraph) -> HwpxResult<()> {
paragraph.para_shape_id = self.map_para_shape(paragraph.para_shape_id)?;
for run in &mut paragraph.runs {
self.rebind_run(run)?;
}
Ok(())
}
fn rebind_run(&self, run: &mut Run) -> HwpxResult<()> {
run.char_shape_id = self.map_char_shape(run.char_shape_id)?;
match &mut run.content {
RunContent::Table(table) => self.rebind_table(table)?,
RunContent::Image(image) => self.rebind_image(image)?,
RunContent::Control(control) => self.rebind_control(control)?,
RunContent::Text(_) => {}
_ => {}
}
Ok(())
}
fn rebind_table(&self, table: &mut Table) -> HwpxResult<()> {
for row in &mut table.rows {
for cell in &mut row.cells {
self.rebind_paragraphs(&mut cell.paragraphs)?;
}
}
self.rebind_caption(table.caption.as_mut())
}
fn rebind_image(&self, image: &mut Image) -> HwpxResult<()> {
self.rebind_caption(image.caption.as_mut())
}
fn rebind_control(&self, control: &mut Control) -> HwpxResult<()> {
match control {
Control::TextBox { paragraphs, caption, .. } => {
self.rebind_paragraphs(paragraphs)?;
self.rebind_caption(caption.as_mut())?;
}
Control::Footnote { paragraphs, .. } | Control::Endnote { paragraphs, .. } => {
self.rebind_paragraphs(paragraphs)?;
}
Control::Ellipse { paragraphs, caption, .. }
| Control::Polygon { paragraphs, caption, .. } => {
self.rebind_paragraphs(paragraphs)?;
self.rebind_caption(caption.as_mut())?;
}
Control::Line { caption, .. }
| Control::Arc { caption, .. }
| Control::Curve { caption, .. }
| Control::ConnectLine { caption, .. } => {
self.rebind_caption(caption.as_mut())?;
}
Control::Memo { content, .. } => self.rebind_paragraphs(content)?,
Control::Hyperlink { .. }
| Control::Equation { .. }
| Control::Chart { .. }
| Control::Dutmal { .. }
| Control::Compose { .. }
| Control::Bookmark { .. }
| Control::CrossRef { .. }
| Control::Field { .. }
| Control::IndexMark { .. }
| Control::Unknown { .. } => {}
_ => {}
}
Ok(())
}
fn rebind_caption(&self, caption: Option<&mut Caption>) -> HwpxResult<()> {
if let Some(caption) = caption {
self.rebind_paragraphs(&mut caption.paragraphs)?;
}
Ok(())
}
fn map_char_shape(&self, index: CharShapeIndex) -> HwpxResult<CharShapeIndex> {
self.char_shape_map.get(index.get()).copied().ok_or(HwpxError::IndexOutOfBounds {
kind: "registry char_shape",
index: index.get() as u32,
max: self.char_shape_map.len() as u32,
})
}
fn map_para_shape(&self, index: ParaShapeIndex) -> HwpxResult<ParaShapeIndex> {
self.para_shape_map.get(index.get()).copied().ok_or(HwpxError::IndexOutOfBounds {
kind: "registry para_shape",
index: index.get() as u32,
max: self.para_shape_map.len() as u32,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use hwpforge_blueprint::builtins::builtin_default;
use hwpforge_blueprint::registry::StyleRegistry;
use hwpforge_core::caption::{Caption, CaptionSide};
use hwpforge_core::control::Control;
use hwpforge_core::document::Document;
use hwpforge_core::image::{Image, ImageFormat};
use hwpforge_core::paragraph::Paragraph;
use hwpforge_core::run::Run;
use hwpforge_core::section::{HeaderFooter, MasterPage, Section};
use hwpforge_core::table::{Table, TableCell, TableRow};
use hwpforge_core::PageSettings;
use hwpforge_core::StyleLookup;
use hwpforge_foundation::{ApplyPageType, HwpUnit, StyleIndex};
fn body_registry() -> (StyleRegistry, CharShapeIndex, ParaShapeIndex) {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let body = *registry.get_style("body").unwrap();
(registry, body.char_shape_id, body.para_shape_id)
}
fn simple_paragraph(char_shape_id: CharShapeIndex, para_shape_id: ParaShapeIndex) -> Paragraph {
Paragraph::with_runs(vec![Run::text("body", char_shape_id)], para_shape_id)
}
#[test]
fn rebind_draft_document_remaps_nested_registry_indices() {
let (registry, body_cs, body_ps) = body_registry();
let bridge = HwpxRegistryBridge::from_registry(®istry).unwrap();
let expected_char = bridge.char_shape_map[body_cs.get()];
let expected_para = bridge.para_shape_map[body_ps.get()];
let mut root = simple_paragraph(body_cs, body_ps).with_style(StyleIndex::new(2));
let table = Table::new(vec![TableRow::new(vec![TableCell::new(
vec![simple_paragraph(body_cs, body_ps)],
HwpUnit::from_mm(30.0).unwrap(),
)])])
.with_caption(Caption::new(vec![simple_paragraph(body_cs, body_ps)], CaptionSide::Bottom));
root.add_run(Run::table(table, body_cs));
root.add_run(Run::image(
Image::new(
"BinData/image1.png",
HwpUnit::from_mm(10.0).unwrap(),
HwpUnit::from_mm(10.0).unwrap(),
ImageFormat::Png,
)
.with_caption(Caption::new(
vec![simple_paragraph(body_cs, body_ps)],
CaptionSide::Bottom,
)),
body_cs,
));
root.add_run(Run::control(
Control::TextBox {
paragraphs: vec![simple_paragraph(body_cs, body_ps)],
width: HwpUnit::from_mm(20.0).unwrap(),
height: HwpUnit::from_mm(10.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: Some(Caption::new(
vec![simple_paragraph(body_cs, body_ps)],
CaptionSide::Bottom,
)),
style: None,
},
body_cs,
));
root.add_run(Run::control(
Control::memo(vec![simple_paragraph(body_cs, body_ps)], "tester", "2026-03-20"),
body_cs,
));
let mut section = Section::with_paragraphs(vec![root], PageSettings::a4());
section.header = Some(HeaderFooter::all_pages(vec![simple_paragraph(body_cs, body_ps)]));
section.footer = Some(HeaderFooter::all_pages(vec![simple_paragraph(body_cs, body_ps)]));
section.master_pages = Some(vec![MasterPage::new(
ApplyPageType::Both,
vec![simple_paragraph(body_cs, body_ps)],
)]);
let mut document = Document::new();
document.add_section(section);
let rebound = bridge.rebind_draft_document(document).unwrap();
let section = &rebound.sections()[0];
let root = §ion.paragraphs[0];
assert_eq!(root.para_shape_id, expected_para);
assert_eq!(root.style_id, Some(StyleIndex::new(2)));
assert_eq!(root.runs[0].char_shape_id, expected_char);
let table_run = root.runs[1].content.as_table().unwrap();
assert_eq!(table_run.rows[0].cells[0].paragraphs[0].para_shape_id, expected_para);
assert_eq!(table_run.rows[0].cells[0].paragraphs[0].runs[0].char_shape_id, expected_char);
assert_eq!(table_run.caption.as_ref().unwrap().paragraphs[0].para_shape_id, expected_para);
let image_run = root.runs[2].content.as_image().unwrap();
assert_eq!(image_run.caption.as_ref().unwrap().paragraphs[0].para_shape_id, expected_para);
match root.runs[3].content.as_control().unwrap() {
Control::TextBox { paragraphs, caption, .. } => {
assert_eq!(paragraphs[0].para_shape_id, expected_para);
assert_eq!(caption.as_ref().unwrap().paragraphs[0].para_shape_id, expected_para);
}
other => panic!("expected textbox, got {other:?}"),
}
match root.runs[4].content.as_control().unwrap() {
Control::Memo { content, .. } => {
assert_eq!(content[0].para_shape_id, expected_para);
}
other => panic!("expected memo, got {other:?}"),
}
assert_eq!(section.header.as_ref().unwrap().paragraphs[0].para_shape_id, expected_para);
assert_eq!(section.footer.as_ref().unwrap().paragraphs[0].para_shape_id, expected_para);
assert_eq!(
section.master_pages.as_ref().unwrap()[0].paragraphs[0].para_shape_id,
expected_para
);
}
#[test]
fn rebind_validated_document_preserves_structure() {
let (registry, body_cs, body_ps) = body_registry();
let bridge = HwpxRegistryBridge::from_registry(®istry).unwrap();
let mut draft = Document::new();
draft.add_section(Section::with_paragraphs(
vec![simple_paragraph(body_cs, body_ps).with_style(StyleIndex::new(2))],
PageSettings::a4(),
));
let validated = draft.validate().unwrap();
let rebound = bridge.rebind_validated_document(&validated).unwrap();
assert_eq!(rebound.section_count(), 1);
assert_eq!(rebound.sections()[0].paragraphs[0].style_id, Some(StyleIndex::new(2)));
assert_eq!(
rebound.sections()[0].paragraphs[0].para_shape_id,
bridge.para_shape_map[body_ps.get()]
);
}
#[test]
fn registry_bridge_preserves_builtin_style_id_space() {
let (mut registry, _, _) = body_registry();
let body = *registry.get_style("body").unwrap();
registry.style_entries.insert("custom-body-copy".to_string(), body);
let bridge = HwpxRegistryBridge::from_registry(®istry).unwrap();
let default_style_count = HancomStyleSet::default().default_styles().len();
let custom_style_id = default_style_count + registry.style_entries.len() - 1;
assert_eq!(bridge.style_store().style_name(StyleIndex::new(2)), Some("개요 1"));
assert_eq!(
bridge.style_store().style_name(StyleIndex::new(default_style_count)),
Some("body")
);
assert_eq!(
bridge.style_store().style_name(StyleIndex::new(custom_style_id)),
Some("custom-body-copy")
);
}
}