use std::io::Write;
use unicode_width::UnicodeWidthStr;
use crate::api::MergeStrategy;
use crate::error::XlsxToMdError;
use crate::types::{CellCoord, MergedRegion, RawCellData, SheetMetadata};
#[derive(Debug, Clone)]
pub(crate) struct Cell {
pub content: String,
pub is_merged: bool,
pub merge_parent: Option<CellCoord>,
}
impl Cell {
pub fn new(content: String) -> Self {
Self {
content,
is_merged: false,
merge_parent: None,
}
}
pub fn new_merged(content: String, parent: CellCoord) -> Self {
Self {
content,
is_merged: true,
merge_parent: Some(parent),
}
}
pub fn empty() -> Self {
Self {
content: String::new(),
is_merged: false,
merge_parent: None,
}
}
}
pub(crate) struct LogicalGrid {
cells: Vec<Vec<Cell>>,
rows: usize,
cols: usize,
}
impl LogicalGrid {
pub fn build(
cells: Vec<RawCellData>,
formatted_cells: Vec<(CellCoord, String)>,
metadata: &SheetMetadata,
merge_strategy: MergeStrategy,
) -> Result<Self, XlsxToMdError> {
let (rows, cols) = Self::determine_grid_size(&cells);
let mut grid_cells = vec![vec![Cell::empty(); cols]; rows];
for (coord, content) in formatted_cells {
if coord.row < rows as u32 && coord.col < cols as u32 {
grid_cells[coord.row as usize][coord.col as usize] = Cell::new(content);
}
}
let mut grid = LogicalGrid {
cells: grid_cells,
rows,
cols,
};
match merge_strategy {
MergeStrategy::DataDuplication => {
grid.apply_data_duplication(&metadata.merged_regions)?;
}
MergeStrategy::HtmlFallback => {
}
}
Ok(grid)
}
fn determine_grid_size(cells: &[RawCellData]) -> (usize, usize) {
let mut max_row = 0;
let mut max_col = 0;
for cell in cells {
max_row = max_row.max(cell.coord.row);
max_col = max_col.max(cell.coord.col);
}
((max_row + 1) as usize, (max_col + 1) as usize)
}
fn apply_data_duplication(
&mut self,
merged_regions: &[MergedRegion],
) -> Result<(), XlsxToMdError> {
for region in merged_regions {
let parent_content = self.cells[region.parent.row as usize][region.parent.col as usize]
.content
.clone();
for row in region.range.start.row..=region.range.end.row {
for col in region.range.start.col..=region.range.end.col {
if row == region.parent.row && col == region.parent.col {
continue;
}
self.cells[row as usize][col as usize] =
Cell::new_merged(parent_content.clone(), region.parent);
}
}
}
Ok(())
}
pub fn render_markdown<W: Write>(&self, writer: &mut W) -> Result<(), XlsxToMdError> {
if self.rows == 0 || self.cols == 0 {
return Ok(());
}
let col_widths = self.calculate_column_widths();
let separator = self.generate_separator(&col_widths);
for (row_idx, row) in self.cells.iter().enumerate() {
write!(writer, "|")?;
for (col_idx, cell) in row.iter().enumerate() {
let width = col_widths[col_idx];
let trimmed_content = cell.content.trim();
let content_width = trimmed_content.width();
write!(writer, " ")?;
write!(writer, "{}", trimmed_content)?;
if content_width < width {
for _ in content_width..width {
write!(writer, " ")?;
}
}
write!(writer, " |")?;
}
writeln!(writer)?;
if row_idx == 0 {
writeln!(writer, "{}", separator)?;
}
}
writer.flush()?;
Ok(())
}
fn calculate_column_widths(&self) -> Vec<usize> {
let mut widths = vec![3; self.cols];
for row in &self.cells {
for (col_idx, cell) in row.iter().enumerate() {
let trimmed_width = cell.content.trim().width();
widths[col_idx] = widths[col_idx].max(trimmed_width);
}
}
widths
}
fn generate_separator(&self, col_widths: &[usize]) -> String {
let mut parts = vec!["|".to_string()];
for &width in col_widths {
parts.push("-".repeat(width + 2));
parts.push("|".to_string());
}
parts.join("")
}
pub fn render_html<W: Write>(
&self,
writer: &mut W,
merged_regions: &[MergedRegion],
) -> Result<(), XlsxToMdError> {
writeln!(writer, "<table>")?;
for (row_idx, row) in self.cells.iter().enumerate() {
writeln!(writer, " <tr>")?;
for (col_idx, cell) in row.iter().enumerate() {
let coord = CellCoord::new(row_idx as u32, col_idx as u32);
if cell.is_merged && cell.merge_parent.is_some() {
continue; }
let (rowspan, colspan) = self.calculate_span(&coord, merged_regions);
if rowspan > 1 || colspan > 1 {
write!(
writer,
" <td rowspan=\"{}\" colspan=\"{}\">",
rowspan, colspan
)?;
} else {
write!(writer, " <td>")?;
}
writeln!(writer, "{}</td>", cell.content)?;
}
writeln!(writer, " </tr>")?;
}
writeln!(writer, "</table>")?;
writer.flush()?;
Ok(())
}
fn calculate_span(&self, coord: &CellCoord, merged_regions: &[MergedRegion]) -> (u32, u32) {
for region in merged_regions {
if region.parent == *coord {
return (region.row_span(), region.col_span());
}
}
(1, 1)
}
pub(crate) fn get_rows(&self) -> usize {
self.rows
}
pub(crate) fn get_cols(&self) -> usize {
self.cols
}
pub(crate) fn get_row(&self, row_idx: usize) -> &[Cell] {
if row_idx < self.rows {
&self.cells[row_idx]
} else {
&[]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CellRange, CellValue};
#[test]
fn test_cell_new() {
let cell = Cell::new("Hello".to_string());
assert_eq!(cell.content, "Hello");
assert!(!cell.is_merged);
assert!(cell.merge_parent.is_none());
}
#[test]
fn test_cell_new_merged() {
let parent = CellCoord::new(0, 0);
let cell = Cell::new_merged("Merged".to_string(), parent);
assert_eq!(cell.content, "Merged");
assert!(cell.is_merged);
assert_eq!(cell.merge_parent, Some(parent));
}
#[test]
fn test_cell_empty() {
let cell = Cell::empty();
assert_eq!(cell.content, "");
assert!(!cell.is_merged);
assert!(cell.merge_parent.is_none());
}
#[test]
fn test_determine_grid_size() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("A1".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(2, 3),
value: CellValue::String("D3".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let (rows, cols) = LogicalGrid::determine_grid_size(&cells);
assert_eq!(rows, 3);
assert_eq!(cols, 4);
}
#[test]
fn test_build_empty_grid() {
let cells = vec![];
let formatted_cells = vec![];
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let result = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
);
assert!(result.is_ok());
}
#[test]
fn test_build_simple_grid() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("A1".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::String("B1".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![
(CellCoord::new(0, 0), "A1".to_string()),
(CellCoord::new(0, 1), "B1".to_string()),
];
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let result = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
);
assert!(result.is_ok());
let grid = result.unwrap();
assert_eq!(grid.rows, 1);
assert_eq!(grid.cols, 2);
}
#[test]
fn test_build_with_merged_cells_data_duplication() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("Header".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::Empty,
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 2),
value: CellValue::Empty,
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![(CellCoord::new(0, 0), "Header".to_string())];
let merged_range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(0, 2));
let merged_region = MergedRegion::new(merged_range);
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![merged_region],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let result = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
);
assert!(result.is_ok());
let _grid = result.unwrap();
}
#[test]
fn test_render_markdown() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("A1".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::String("B1".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![
(CellCoord::new(0, 0), "A1".to_string()),
(CellCoord::new(0, 1), "B1".to_string()),
];
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let grid = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
)
.unwrap();
let mut output = Vec::new();
let result = grid.render_markdown(&mut output);
assert!(result.is_ok());
let markdown = String::from_utf8(output).unwrap();
assert!(markdown.contains("A1"));
assert!(markdown.contains("B1"));
assert!(markdown.contains("|"));
assert!(markdown.contains("-"));
assert!(markdown.contains("| A1") || markdown.contains("|A1"));
assert!(markdown.contains("| B1") || markdown.contains("|B1"));
}
#[test]
fn test_render_markdown_with_trim() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String(" Header1 ".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::String("Header2".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(1, 0),
value: CellValue::String(" Data1 ".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(1, 1),
value: CellValue::String("Data2".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![
(CellCoord::new(0, 0), " Header1 ".to_string()),
(CellCoord::new(0, 1), "Header2".to_string()),
(CellCoord::new(1, 0), " Data1 ".to_string()),
(CellCoord::new(1, 1), "Data2".to_string()),
];
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let grid = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
)
.unwrap();
let mut output = Vec::new();
let result = grid.render_markdown(&mut output);
assert!(result.is_ok());
let markdown = String::from_utf8(output).unwrap();
assert!(markdown.contains("Header1"), "Markdown should contain 'Header1'. Got: {}", markdown);
assert!(markdown.contains("Header2"), "Markdown should contain 'Header2'. Got: {}", markdown);
assert!(markdown.contains("Data1"), "Markdown should contain 'Data1'. Got: {}", markdown);
assert!(markdown.contains("Data2"), "Markdown should contain 'Data2'. Got: {}", markdown);
assert!(!markdown.contains(" Header1 "), "Markdown should not contain spaces around 'Header1'");
assert!(!markdown.contains(" Data1 "), "Markdown should not contain spaces around 'Data1'");
let lines: Vec<&str> = markdown.lines().collect();
assert!(lines.len() >= 2);
let separator_line = lines[1];
let header_line = lines[0];
let header_parts: Vec<&str> = header_line.split('|').collect();
let separator_parts: Vec<&str> = separator_line.split('|').collect();
for i in 1..header_parts.len().min(separator_parts.len()) - 1 {
let header_cell = header_parts[i].trim();
let separator_cell = separator_parts[i].trim();
assert_eq!(
separator_cell.len(),
header_cell.len() + 2,
"Column {} width mismatch: header='{}' (len={}), separator='{}' (len={})",
i,
header_cell,
header_cell.len(),
separator_cell,
separator_cell.len()
);
}
}
#[test]
fn test_render_html() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("Header".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::Empty,
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![(CellCoord::new(0, 0), "Header".to_string())];
let merged_range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(0, 1));
let merged_region = MergedRegion::new(merged_range);
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![merged_region.clone()],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let grid = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::HtmlFallback,
)
.unwrap();
let mut output = Vec::new();
let result = grid.render_html(&mut output, &metadata.merged_regions);
assert!(result.is_ok());
let html = String::from_utf8(output).unwrap();
assert!(html.contains("<table>"));
assert!(html.contains("</table>"));
assert!(html.contains("Header"));
}
#[test]
fn test_calculate_column_widths() {
let grid_cells = vec![
vec![
Cell::new("Short".to_string()),
Cell::new("Very Long Content".to_string()),
],
vec![
Cell::new("Longer".to_string()),
Cell::new("Short".to_string()),
],
];
let grid = LogicalGrid {
cells: grid_cells,
rows: 2,
cols: 2,
};
let widths = grid.calculate_column_widths();
assert_eq!(widths[0], 6); assert_eq!(widths[1], 17); }
#[test]
fn test_generate_separator() {
let grid = LogicalGrid {
cells: vec![],
rows: 0,
cols: 0,
};
let col_widths = vec![3, 5, 2];
let separator = grid.generate_separator(&col_widths);
assert!(separator.contains("|"));
assert!(separator.contains("-"));
}
#[test]
fn test_calculate_span() {
let merged_range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(1, 2));
let merged_region = MergedRegion::new(merged_range);
let grid = LogicalGrid {
cells: vec![],
rows: 0,
cols: 0,
};
let (rowspan, colspan) =
grid.calculate_span(&CellCoord::new(0, 0), std::slice::from_ref(&merged_region));
assert_eq!(rowspan, 2);
assert_eq!(colspan, 3);
let (rowspan, colspan) = grid.calculate_span(&CellCoord::new(5, 5), &[merged_region]);
assert_eq!(rowspan, 1);
assert_eq!(colspan, 1);
}
#[test]
fn test_calculate_column_widths_with_japanese() {
let grid_cells = vec![
vec![
Cell::new("市区町村コード".to_string()), Cell::new("店舗名".to_string()), ],
vec![
Cell::new("01100".to_string()), Cell::new("札幌店".to_string()), ],
];
let grid = LogicalGrid {
cells: grid_cells,
rows: 2,
cols: 2,
};
let widths = grid.calculate_column_widths();
assert_eq!(widths[0], 14); assert_eq!(widths[1], 6); }
#[test]
fn test_render_markdown_with_japanese() {
let cells = vec![
RawCellData {
coord: CellCoord::new(0, 0),
value: CellValue::String("ヘッダー".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(0, 1),
value: CellValue::String("Header".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(1, 0),
value: CellValue::String("データ".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
RawCellData {
coord: CellCoord::new(1, 1),
value: CellValue::String("Data".to_string()),
format_id: None,
format_string: None,
formula: None,
hyperlink: None,
rich_text: None,
},
];
let formatted_cells = vec![
(CellCoord::new(0, 0), "ヘッダー".to_string()),
(CellCoord::new(0, 1), "Header".to_string()),
(CellCoord::new(1, 0), "データ".to_string()),
(CellCoord::new(1, 1), "Data".to_string()),
];
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
let grid = LogicalGrid::build(
cells,
formatted_cells,
&metadata,
MergeStrategy::DataDuplication,
)
.unwrap();
let mut output = Vec::new();
let result = grid.render_markdown(&mut output);
assert!(result.is_ok());
let markdown = String::from_utf8(output).unwrap();
let lines: Vec<&str> = markdown.lines().collect();
assert_eq!(lines.len(), 3);
let separator_line = lines[1];
assert!(separator_line.starts_with("|"));
assert!(separator_line.ends_with("|"));
assert!(separator_line.contains("-"));
assert!(
separator_line.contains("----------"),
"Separator should contain 10 dashes for column with width 8. Got: {}",
separator_line
);
}
#[test]
fn test_display_width_calculation() {
use unicode_width::UnicodeWidthStr;
assert_eq!("日本語".width(), 6); assert_eq!("ABC".width(), 3); assert_eq!("日本ABC".width(), 7);
assert_eq!("市区町村コード".width(), 14); assert_eq!("01100".width(), 5); }
}