use super::common::find_children_end;
use super::dispatcher::extract_paragraphs_from_range;
use crate::hwp::model::*;
use crate::hwp::record::*;
pub(crate) fn parse_table_ctrl(
records: &[Record],
ctrl_idx: usize,
) -> (u16, u16, Vec<HwpTableCell>) {
let ctrl_end = find_children_end(records, ctrl_idx);
let mut row_count: u16 = 0;
let mut col_count: u16 = 0;
let mut cells: Vec<HwpTableCell> = Vec::new();
let mut idx = ctrl_idx + 1;
while idx < ctrl_end {
let rec = &records[idx];
match rec.tag_id {
HWPTAG_TABLE => {
if rec.data.len() >= 6 {
row_count = u16::from_le_bytes([rec.data[4], rec.data[5]]);
}
if rec.data.len() >= 8 {
col_count = u16::from_le_bytes([rec.data[6], rec.data[7]]);
}
tracing::trace!("TABLE dims: {row_count}×{col_count}");
idx += 1;
}
HWPTAG_LIST_HEADER => {
let col = if rec.data.len() >= 4 {
u16::from_le_bytes([rec.data[2], rec.data[3]])
} else {
0
};
let row = if rec.data.len() >= 6 {
u16::from_le_bytes([rec.data[4], rec.data[5]])
} else {
0
};
let col_span = if rec.data.len() >= 8 {
let v = u16::from_le_bytes([rec.data[6], rec.data[7]]);
if v == 0 {
1
} else {
v
}
} else {
1
};
let row_span = if rec.data.len() >= 10 {
let v = u16::from_le_bytes([rec.data[8], rec.data[9]]);
if v == 0 {
1
} else {
v
}
} else {
1
};
let vertical_align = if rec.data.len() >= 27 {
rec.data[26]
} else {
0
};
let is_header = row == 0;
let cell_end = find_children_end(records, idx);
let paragraphs = extract_paragraphs_from_range(records, idx + 1, cell_end);
cells.push(HwpTableCell {
row,
col,
row_span,
col_span,
vertical_align,
is_header,
paragraphs,
});
idx = cell_end;
}
_ => {
idx += 1;
}
}
}
(row_count, col_count, cells)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hwp::reader::encode_u16s_test;
use crate::hwp::record::{
HWPTAG_CTRL_HEADER, HWPTAG_LIST_HEADER, HWPTAG_PARA_HEADER, HWPTAG_PARA_TEXT, HWPTAG_TABLE,
};
fn make_record_with_data(tag_id: u16, level: u16, data: Vec<u8>) -> Record {
Record {
tag_id,
level,
data,
}
}
fn make_table_record(level: u16, row_count: u16, col_count: u16) -> Record {
let mut data = vec![0u8; 8];
data[4..6].copy_from_slice(&row_count.to_le_bytes());
data[6..8].copy_from_slice(&col_count.to_le_bytes());
make_record_with_data(HWPTAG_TABLE, level, data)
}
fn make_list_header_record(
level: u16,
col: u16,
row: u16,
col_span: u16,
row_span: u16,
) -> Record {
let mut data = vec![0u8; 10];
data[2..4].copy_from_slice(&col.to_le_bytes());
data[4..6].copy_from_slice(&row.to_le_bytes());
data[6..8].copy_from_slice(&col_span.to_le_bytes());
data[8..10].copy_from_slice(&row_span.to_le_bytes());
make_record_with_data(HWPTAG_LIST_HEADER, level, data)
}
fn make_ctrl_header_table(level: u16) -> Record {
make_record_with_data(HWPTAG_CTRL_HEADER, level, CTRL_TABLE.to_le_bytes().to_vec())
}
fn make_list_header_with_valign(
level: u16,
col: u16,
row: u16,
col_span: u16,
row_span: u16,
vertical_align: u8,
) -> Record {
let mut data = vec![0u8; 27];
data[2..4].copy_from_slice(&col.to_le_bytes());
data[4..6].copy_from_slice(&row.to_le_bytes());
data[6..8].copy_from_slice(&col_span.to_le_bytes());
data[8..10].copy_from_slice(&row_span.to_le_bytes());
data[26] = vertical_align;
make_record_with_data(HWPTAG_LIST_HEADER, level, data)
}
#[test]
fn parse_table_ctrl_dimensions() {
let records = vec![make_ctrl_header_table(0), make_table_record(1, 2, 3)];
let (rows, cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(rows, 2);
assert_eq!(cols, 3);
assert!(cells.is_empty(), "no LIST_HEADERs so no cells expected");
}
#[test]
fn parse_table_ctrl_with_cells() {
let text_a = encode_u16s_test(&[b'A' as u16]);
let text_b = encode_u16s_test(&[b'B' as u16]);
let mut para_header_data = vec![0u8; 6];
para_header_data[4] = 0;
para_header_data[5] = 0;
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 1, 2),
make_list_header_record(1, 0, 0, 1, 1),
make_record_with_data(HWPTAG_PARA_HEADER, 2, para_header_data.clone()),
make_record_with_data(HWPTAG_PARA_TEXT, 3, text_a),
make_list_header_record(1, 1, 0, 1, 1),
make_record_with_data(HWPTAG_PARA_HEADER, 2, para_header_data.clone()),
make_record_with_data(HWPTAG_PARA_TEXT, 3, text_b),
];
let (rows, cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(rows, 1);
assert_eq!(cols, 2);
assert_eq!(cells.len(), 2);
let cell_00 = cells
.iter()
.find(|c| c.row == 0 && c.col == 0)
.expect("cell (0,0)");
assert_eq!(cell_00.paragraphs.len(), 1);
assert_eq!(cell_00.paragraphs[0].text, "A");
let cell_01 = cells
.iter()
.find(|c| c.row == 0 && c.col == 1)
.expect("cell (0,1)");
assert_eq!(cell_01.paragraphs.len(), 1);
assert_eq!(cell_01.paragraphs[0].text, "B");
}
#[test]
fn parse_table_ctrl_cell_spans() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 2, 2),
make_list_header_record(1, 0, 0, 2, 2),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].col_span, 2);
assert_eq!(cells[0].row_span, 2);
}
#[test]
fn parse_table_ctrl_malformed_table_record_short_data() {
let mut short_data = vec![0u8; 4];
short_data[0..4].copy_from_slice(&0u32.to_le_bytes());
let records = vec![
make_ctrl_header_table(0),
make_record_with_data(HWPTAG_TABLE, 1, short_data),
];
let (rows, cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(rows, 0);
assert_eq!(cols, 0);
assert!(cells.is_empty());
}
#[test]
fn parse_table_ctrl_cell_row0_is_header_true() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 1, 1),
make_list_header_record(1, 0, 0, 1, 1),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert!(cells[0].is_header, "row 0 cell must be is_header");
}
#[test]
fn parse_table_ctrl_cell_row1_is_header_false() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 2, 1),
make_list_header_record(1, 0, 1, 1, 1),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert!(!cells[0].is_header, "row 1 cell must not be is_header");
}
#[test]
fn parse_table_ctrl_vertical_align_center_parsed() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 1, 1),
make_list_header_with_valign(1, 0, 0, 1, 1, 1),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].vertical_align, 1);
}
#[test]
fn parse_table_ctrl_vertical_align_bottom_parsed() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 1, 1),
make_list_header_with_valign(1, 0, 0, 1, 1, 2),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].vertical_align, 2);
}
#[test]
fn parse_table_ctrl_vertical_align_defaults_to_top_when_short_data() {
let records = vec![
make_ctrl_header_table(0),
make_table_record(1, 1, 1),
make_list_header_record(1, 0, 0, 1, 1),
];
let (_rows, _cols, cells) = parse_table_ctrl(&records, 0);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].vertical_align, 0);
}
}