use hwpforge_core::{BulletDef, NumberingDef, ParaHead, ParagraphListRef};
use hwpforge_foundation::{HeadingType, NumberFormatType};
use crate::error::{HwpxError, HwpxResult};
use crate::schema::header::{HxBullet, HxBulletParaHead, HxHeading};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WireListParts {
pub heading_type: HeadingType,
pub id_ref: u32,
pub level: u32,
pub checked: bool,
}
pub(crate) fn list_ref_to_wire_parts(
list_ref: Option<ParagraphListRef>,
numberings: &[NumberingDef],
bullets: &[BulletDef],
) -> HwpxResult<WireListParts> {
match list_ref {
Some(ParagraphListRef::Outline { level }) => Ok(WireListParts {
heading_type: HeadingType::Outline,
id_ref: 0,
level: u32::from(level),
checked: false,
}),
Some(ParagraphListRef::Number { numbering_id, level }) => Ok(WireListParts {
heading_type: HeadingType::Number,
id_ref: numberings
.get(numbering_id.get())
.ok_or_else(|| HwpxError::IndexOutOfBounds {
kind: "numbering definition",
index: numbering_id.get() as u32,
max: numberings.len() as u32,
})?
.id,
level: u32::from(level),
checked: false,
}),
Some(ParagraphListRef::Bullet { bullet_id, level }) => {
let bullet = resolve_bullet(bullets, bullet_id)?;
if bullet.is_checkable() {
return Err(HwpxError::InvalidStructure {
detail: format!(
"plain bullet paragraph references checkable bullet definition {}; use CheckBullet semantics",
bullet.id
),
});
}
Ok(WireListParts {
heading_type: HeadingType::Bullet,
id_ref: bullet.id,
level: u32::from(level),
checked: false,
})
}
Some(ParagraphListRef::CheckBullet { bullet_id, level, checked }) => {
let bullet = resolve_bullet(bullets, bullet_id)?;
if !bullet.is_checkable() {
return Err(HwpxError::InvalidStructure {
detail: format!(
"checkable bullet paragraph references non-checkable bullet definition {}",
bullet.id
),
});
}
Ok(WireListParts {
heading_type: HeadingType::Bullet,
id_ref: bullet.id,
level: u32::from(level),
checked,
})
}
None => Ok(WireListParts {
heading_type: HeadingType::None,
id_ref: 0,
level: 0,
checked: false,
}),
}
}
pub(crate) fn wire_parts_to_heading(
heading_type: HeadingType,
id_ref: u32,
level: u32,
) -> HxHeading {
HxHeading { heading_type: heading_type.to_hwpx_str().into(), id_ref, level }
}
pub(crate) fn heading_type_to_para_list_type(heading_type: HeadingType) -> Option<&'static str> {
match heading_type {
HeadingType::Bullet => Some("BULLET"),
HeadingType::Number => Some("NUMBER"),
_ => None,
}
}
pub(crate) fn bullet_def_to_hwpx(bullet: &BulletDef) -> HxBullet {
HxBullet {
id: bullet.id,
bullet_char: bullet.bullet_char.clone(),
checked_char: bullet.checked_char.clone(),
use_image: u32::from(bullet.use_image),
para_heads: vec![para_head_to_hwpx(&bullet.para_head, bullet.is_checkable())],
}
}
pub(crate) fn bullet_def_from_hwpx(bullet: &HxBullet) -> BulletDef {
let para_head =
bullet.para_heads.first().map(para_head_from_hwpx).unwrap_or_else(default_bullet_para_head);
BulletDef {
id: bullet.id,
bullet_char: bullet.bullet_char.clone(),
checked_char: bullet.checked_char.clone(),
use_image: bullet.use_image != 0,
para_head,
}
}
fn para_head_to_hwpx(para_head: &ParaHead, is_checkable: bool) -> HxBulletParaHead {
HxBulletParaHead {
level: para_head.level.saturating_sub(1),
align: "LEFT".into(),
use_inst_width: 0,
auto_indent: 1,
width_adjust: 0,
text_offset_type: "PERCENT".into(),
text_offset: 50,
num_format: number_format_to_hwpx(para_head.num_format).into(),
char_pr_id_ref: u32::MAX,
checkable: u32::from(is_checkable),
text: para_head.text.clone(),
}
}
fn para_head_from_hwpx(para_head: &HxBulletParaHead) -> ParaHead {
ParaHead {
start: 0,
level: para_head.level + 1,
num_format: number_format_from_hwpx(¶_head.num_format),
text: para_head.text.clone(),
checkable: para_head.checkable != 0,
}
}
fn default_bullet_para_head() -> ParaHead {
ParaHead {
start: 0,
level: 1,
num_format: NumberFormatType::Digit,
text: String::new(),
checkable: false,
}
}
fn resolve_bullet(
bullets: &[BulletDef],
bullet_id: hwpforge_foundation::BulletIndex,
) -> HwpxResult<&BulletDef> {
bullets.get(bullet_id.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
kind: "bullet definition",
index: bullet_id.get() as u32,
max: bullets.len() as u32,
})
}
fn number_format_to_hwpx(format: NumberFormatType) -> &'static str {
match format {
NumberFormatType::Digit => "DIGIT",
NumberFormatType::CircledDigit => "CIRCLED_DIGIT",
NumberFormatType::RomanCapital => "ROMAN_CAPITAL",
NumberFormatType::RomanSmall => "ROMAN_SMALL",
NumberFormatType::LatinCapital => "LATIN_CAPITAL",
NumberFormatType::LatinSmall => "LATIN_SMALL",
NumberFormatType::CircledLatinSmall => "CIRCLED_LATIN_SMALL",
NumberFormatType::HangulSyllable => "HANGUL_SYLLABLE",
NumberFormatType::HangulJamo => "HANGUL_JAMO",
NumberFormatType::HanjaDigit => "HANJA_DIGIT",
NumberFormatType::CircledHangulSyllable => "CIRCLED_HANGUL_SYLLABLE",
_ => "DIGIT",
}
}
fn number_format_from_hwpx(format: &str) -> NumberFormatType {
match format {
"CIRCLED_DIGIT" => NumberFormatType::CircledDigit,
"ROMAN_CAPITAL" => NumberFormatType::RomanCapital,
"ROMAN_SMALL" => NumberFormatType::RomanSmall,
"LATIN_CAPITAL" => NumberFormatType::LatinCapital,
"LATIN_SMALL" => NumberFormatType::LatinSmall,
"CIRCLED_LATIN_SMALL" => NumberFormatType::CircledLatinSmall,
"HANGUL_SYLLABLE" => NumberFormatType::HangulSyllable,
"HANGUL_JAMO" => NumberFormatType::HangulJamo,
"HANJA_DIGIT" => NumberFormatType::HanjaDigit,
"CIRCLED_HANGUL_SYLLABLE" => NumberFormatType::CircledHangulSyllable,
_ => NumberFormatType::Digit,
}
}
#[cfg(test)]
mod tests {
use super::*;
use hwpforge_foundation::{BulletIndex, NumberingIndex};
#[test]
fn list_ref_to_wire_parts_uses_definition_ids_not_indices() {
let numberings = vec![NumberingDef {
id: 42,
start: 0,
levels: vec![ParaHead {
start: 1,
level: 1,
num_format: NumberFormatType::Digit,
text: "^1.".into(),
checkable: false,
}],
}];
let bullets = vec![
BulletDef {
id: 7,
bullet_char: "•".into(),
checked_char: None,
use_image: false,
para_head: default_bullet_para_head(),
},
BulletDef {
id: 8,
bullet_char: "☐".into(),
checked_char: Some("☑".into()),
use_image: false,
para_head: ParaHead { checkable: true, ..default_bullet_para_head() },
},
];
assert_eq!(
list_ref_to_wire_parts(
Some(ParagraphListRef::Outline { level: 0 }),
&numberings,
&bullets,
)
.unwrap(),
WireListParts {
heading_type: HeadingType::Outline,
id_ref: 0,
level: 0,
checked: false
},
);
assert_eq!(
list_ref_to_wire_parts(
Some(ParagraphListRef::Number { numbering_id: NumberingIndex::new(0), level: 2 }),
&numberings,
&bullets,
)
.unwrap(),
WireListParts {
heading_type: HeadingType::Number,
id_ref: 42,
level: 2,
checked: false
},
);
assert_eq!(
list_ref_to_wire_parts(
Some(ParagraphListRef::Bullet { bullet_id: BulletIndex::new(0), level: 0 }),
&numberings,
&bullets,
)
.unwrap(),
WireListParts {
heading_type: HeadingType::Bullet,
id_ref: 7,
level: 0,
checked: false
},
);
assert_eq!(
list_ref_to_wire_parts(
Some(ParagraphListRef::CheckBullet {
bullet_id: BulletIndex::new(1),
level: 1,
checked: true,
}),
&numberings,
&bullets,
)
.unwrap(),
WireListParts { heading_type: HeadingType::Bullet, id_ref: 8, level: 1, checked: true },
);
}
#[test]
fn list_ref_to_wire_parts_rejects_invalid_definition_indices() {
let err = list_ref_to_wire_parts(
Some(ParagraphListRef::Number { numbering_id: NumberingIndex::new(99), level: 0 }),
&[],
&[],
)
.unwrap_err();
assert!(matches!(err, HwpxError::IndexOutOfBounds { kind: "numbering definition", .. }));
let err = list_ref_to_wire_parts(
Some(ParagraphListRef::Bullet { bullet_id: BulletIndex::new(99), level: 0 }),
&[],
&[],
)
.unwrap_err();
assert!(matches!(err, HwpxError::IndexOutOfBounds { kind: "bullet definition", .. }));
}
#[test]
fn list_ref_to_wire_parts_rejects_plain_bullet_against_checkable_definition() {
let bullets = vec![BulletDef {
id: 7,
bullet_char: "☐".into(),
checked_char: Some("☑".into()),
use_image: false,
para_head: ParaHead { checkable: true, ..default_bullet_para_head() },
}];
let err = list_ref_to_wire_parts(
Some(ParagraphListRef::Bullet { bullet_id: BulletIndex::new(0), level: 0 }),
&[],
&bullets,
)
.unwrap_err();
assert!(matches!(err, HwpxError::InvalidStructure { .. }));
}
#[test]
fn bullet_roundtrip_preserves_use_image_and_text() {
let bullet = BulletDef {
id: 1,
bullet_char: "".into(),
checked_char: Some("☑".into()),
use_image: true,
para_head: ParaHead {
start: 0,
level: 1,
num_format: NumberFormatType::Digit,
text: String::new(),
checkable: false,
},
};
let hwpx = bullet_def_to_hwpx(&bullet);
let roundtrip = bullet_def_from_hwpx(&hwpx);
assert_eq!(roundtrip.id, 1);
assert_eq!(roundtrip.bullet_char, "");
assert_eq!(roundtrip.checked_char.as_deref(), Some("☑"));
assert!(roundtrip.use_image);
assert_eq!(roundtrip.para_head.level, 1);
assert!(roundtrip.para_head.checkable);
}
}