use crate::doc_info::DocumentProperties;
use crate::model::{HwpDocument, ParagraphDetail, Section};
use crate::shape::{Align, CharShape, ParaShape, ShapeTables};
use crate::summary::Metadata;
use crate::table::{Cell, Table};
impl HwpDocument {
pub fn new_for_writing(title: Option<String>) -> Self {
let mut shapes = ShapeTables::default();
shapes.char_shapes.insert(
0,
CharShape {
face_ids: [0; 7],
face_names: Default::default(),
size_pt: 10.0,
italic: false,
bold: false,
underline: false,
strikethrough: false,
superscript: false,
subscript: false,
color: 0,
},
);
shapes.para_shapes.insert(
0,
ParaShape {
align: Align::Both,
left_margin: 0,
right_margin: 0,
indent: 0,
space_before: 0,
space_after: 0,
line_spacing: 160,
},
);
let metadata = Metadata {
title,
..Metadata::default()
};
HwpDocument {
version: "hwpx".into(),
metadata,
properties: DocumentProperties::default(),
shapes,
assets: Default::default(),
sections: vec![Section {
index: 0,
paragraphs: vec![],
paragraph_details: vec![],
structure: vec![],
tables: vec![],
}],
warnings: vec![],
}
}
pub fn ensure_char_shape(
&mut self,
bold: bool,
italic: bool,
underline: bool,
size_pt: f32,
color: u32,
) -> u32 {
for (&id, cs) in &self.shapes.char_shapes {
if cs.bold == bold
&& cs.italic == italic
&& cs.underline == underline
&& (cs.size_pt - size_pt).abs() < 0.01
&& cs.color == color
{
return id;
}
}
let id = self.shapes.char_shapes.len() as u32;
self.shapes.char_shapes.insert(
id,
CharShape {
face_ids: [0; 7],
face_names: Default::default(),
size_pt,
italic,
bold,
underline,
strikethrough: false,
superscript: false,
subscript: false,
color,
},
);
id
}
pub fn ensure_para_shape(&mut self, align: Align) -> u32 {
for (&id, ps) in &self.shapes.para_shapes {
if ps.align == align {
return id;
}
}
let id = self.shapes.para_shapes.len() as u32;
self.shapes.para_shapes.insert(
id,
ParaShape {
align,
left_margin: 0,
right_margin: 0,
indent: 0,
space_before: 0,
space_after: 0,
line_spacing: 160,
},
);
id
}
pub fn add_paragraph(
&mut self,
text: &str,
char_shape_id: u32,
para_shape_id: u32,
insert_after: Option<&str>,
) -> String {
let sec = &mut self.sections[0];
let idx = if let Some(after_id) = insert_after {
if let Some(pos) = after_id
.split(':')
.nth(1)
.and_then(|s| s.parse::<usize>().ok())
{
let insert_at = (pos + 1).min(sec.paragraphs.len());
sec.paragraphs.insert(insert_at, text.to_string());
sec.paragraph_details.insert(
insert_at,
ParagraphDetail {
text: text.to_string(),
para_shape_id,
runs: vec![(0, char_shape_id)],
footnotes: vec![],
equation: None,
image_refs: vec![],
},
);
insert_at
} else {
sec.paragraphs.push(text.to_string());
sec.paragraph_details.push(ParagraphDetail {
text: text.to_string(),
para_shape_id,
runs: vec![(0, char_shape_id)],
footnotes: vec![],
equation: None,
image_refs: vec![],
});
sec.paragraphs.len() - 1
}
} else {
sec.paragraphs.push(text.to_string());
sec.paragraph_details.push(ParagraphDetail {
text: text.to_string(),
para_shape_id,
runs: vec![(0, char_shape_id)],
footnotes: vec![],
equation: None,
image_refs: vec![],
});
sec.paragraphs.len() - 1
};
format!("0:{}", idx)
}
pub fn set_text(&mut self, paragraph_id: &str, text: &str) -> Result<(), String> {
let (sec, para) = parse_element_id(paragraph_id)?;
let section = self.sections.get_mut(sec).ok_or("section not found")?;
*section
.paragraphs
.get_mut(para)
.ok_or("paragraph not found")? = text.to_string();
if let Some(d) = section.paragraph_details.get_mut(para) {
d.text = text.to_string();
}
Ok(())
}
pub fn add_table(
&mut self,
rows: u16,
cols: u16,
cells_text: Option<Vec<Vec<String>>>,
_insert_after: Option<&str>,
) -> String {
let sec = &mut self.sections[0];
let cells: Vec<Vec<Option<Cell>>> = (0..rows)
.map(|r| {
(0..cols)
.map(|c| {
let text = cells_text
.as_ref()
.and_then(|ct| ct.get(r as usize))
.and_then(|row| row.get(c as usize))
.cloned()
.unwrap_or_default();
Some(Cell {
col: c,
row: r,
col_span: 1,
row_span: 1,
text: text.clone(),
paragraphs: vec![text],
})
})
.collect()
})
.collect();
let para_idx = sec.paragraphs.len();
sec.paragraphs.push(String::new());
sec.paragraph_details.push(ParagraphDetail {
text: String::new(),
para_shape_id: 0,
runs: vec![],
footnotes: vec![],
equation: None,
image_refs: vec![],
});
let table = Table {
id: format!("0:{}", para_idx),
rows,
cols,
caption: None,
cells,
};
sec.tables.push(table);
format!("0:{}", para_idx)
}
pub fn delete_element(&mut self, element_id: &str) -> Result<(), String> {
let (sec, idx) = parse_element_id(element_id)?;
let section = self.sections.get_mut(sec).ok_or("section not found")?;
if let Some(tbl_pos) = section.tables.iter().position(|t| t.id == element_id) {
section.tables.remove(tbl_pos);
}
if idx < section.paragraphs.len() {
section.paragraphs.remove(idx);
if idx < section.paragraph_details.len() {
section.paragraph_details.remove(idx);
}
}
Ok(())
}
}
fn parse_element_id(id: &str) -> Result<(usize, usize), String> {
let parts: Vec<&str> = id.split(':').collect();
if parts.len() != 2 {
return Err("element_id must be 'section:index'".into());
}
let sec: usize = parts[0].parse().map_err(|_| "invalid section")?;
let idx: usize = parts[1].parse().map_err(|_| "invalid index")?;
Ok((sec, idx))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_and_add_paragraphs() {
let mut doc = HwpDocument::new_for_writing(Some("Test".into()));
let p1 = doc.add_paragraph("Hello", 0, 0, None);
let p2 = doc.add_paragraph("World", 0, 0, None);
assert_eq!(p1, "0:0");
assert_eq!(p2, "0:1");
assert_eq!(doc.sections[0].paragraphs, vec!["Hello", "World"]);
}
#[test]
fn set_text_updates_paragraph() {
let mut doc = HwpDocument::new_for_writing(None);
doc.add_paragraph("old", 0, 0, None);
doc.set_text("0:0", "new").unwrap();
assert_eq!(doc.sections[0].paragraphs[0], "new");
}
#[test]
fn add_table_populates_cells() {
let mut doc = HwpDocument::new_for_writing(None);
let _tid = doc.add_table(
2,
2,
Some(vec![
vec!["A".into(), "B".into()],
vec!["C".into(), "D".into()],
]),
None,
);
assert_eq!(doc.sections[0].tables.len(), 1);
let tbl = &doc.sections[0].tables[0];
assert_eq!(tbl.cells[0][0].as_ref().unwrap().text, "A");
assert_eq!(tbl.cells[1][1].as_ref().unwrap().text, "D");
}
#[test]
fn delete_element_removes_paragraph() {
let mut doc = HwpDocument::new_for_writing(None);
doc.add_paragraph("a", 0, 0, None);
doc.add_paragraph("b", 0, 0, None);
doc.delete_element("0:0").unwrap();
assert_eq!(doc.sections[0].paragraphs, vec!["b"]);
}
#[test]
fn ensure_char_shape_deduplicates() {
let mut doc = HwpDocument::new_for_writing(None);
let id1 = doc.ensure_char_shape(true, false, false, 12.0, 0);
let id2 = doc.ensure_char_shape(true, false, false, 12.0, 0);
let id3 = doc.ensure_char_shape(false, true, false, 12.0, 0);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
}