feat: spreadsheet part 3 (#13350)

=
This commit is contained in:
jif-oai
2026-03-03 13:09:37 +00:00
committed by GitHub
parent a7d90b867d
commit 821024f9c9
6 changed files with 1673 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ mod error;
mod formula;
mod manager;
mod model;
mod style;
mod xlsx;
#[cfg(test)]
@@ -12,3 +13,4 @@ pub use address::*;
pub use error::*;
pub use manager::*;
pub use model::*;
pub use style::*;

View File

@@ -109,6 +109,21 @@ impl SpreadsheetArtifactManager {
"set_row_heights_bulk" => self.set_row_heights_bulk(request),
"get_row_height" => self.get_row_height(request),
"cleanup_and_validate_sheet" => self.cleanup_and_validate_sheet(request),
"create_text_style" => self.create_text_style(request),
"get_text_style" => self.get_text_style(request),
"create_fill" => self.create_fill(request),
"get_fill" => self.get_fill(request),
"create_border" => self.create_border(request),
"get_border" => self.get_border(request),
"create_number_format" => self.create_number_format(request),
"get_number_format" => self.get_number_format(request),
"create_cell_format" => self.create_cell_format(request),
"get_cell_format" => self.get_cell_format(request),
"create_differential_format" => self.create_differential_format(request),
"get_differential_format" => self.get_differential_format(request),
"get_cell_format_summary" => self.get_cell_format_summary(request),
"get_range_format_summary" => self.get_range_format_summary(request),
"get_reference" => self.get_reference(request),
"get_cell" => self.get_cell(request),
"get_cell_by_indices" => self.get_cell_by_indices(request),
"get_cell_field" => self.get_cell_field(request),
@@ -312,6 +327,13 @@ impl SpreadsheetArtifactManager {
.map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry));
response.rendered_text = Some(sheet.to_rendered_text(range.as_ref()));
response.range = range.as_ref().map(|entry| sheet.get_range_view(entry));
response.top_left_style_index = range
.as_ref()
.map(|entry| sheet.top_left_style_index(entry));
response.range_format = range.as_ref().map(|entry| sheet.range_format(entry));
response.cell_format_summary = response
.top_left_style_index
.and_then(|style_index| artifact.cell_format_summary(style_index));
response.serialized_dict = Some(sheet.to_dict()?);
Ok(response)
}
@@ -665,6 +687,417 @@ impl SpreadsheetArtifactManager {
Ok(response)
}
fn create_text_style(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateTextStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_text_style(
args.style,
args.source_style_id,
args.merge_with_existing_components.unwrap_or(false),
)?;
let style = artifact.get_text_style(style_id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::Serialization {
message: format!("created text style `{style_id}` was not available"),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created text style `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(style)?);
Ok(response)
}
fn get_text_style(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let style = artifact.get_text_style(args.id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("text style `{}` was not found", args.id),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved text style `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(style)?);
Ok(response)
}
fn create_fill(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateFillArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_fill(
args.fill,
args.source_fill_id,
args.merge_with_existing_components.unwrap_or(false),
)?;
let fill = artifact.get_fill(style_id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::Serialization {
message: format!("created fill `{style_id}` was not available"),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created fill `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(fill)?);
Ok(response)
}
fn get_fill(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let fill = artifact.get_fill(args.id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("fill `{}` was not found", args.id),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved fill `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(fill)?);
Ok(response)
}
fn create_border(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateBorderArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_border(
args.border,
args.source_border_id,
args.merge_with_existing_components.unwrap_or(false),
)?;
let border = artifact.get_border(style_id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::Serialization {
message: format!("created border `{style_id}` was not available"),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created border `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(border)?);
Ok(response)
}
fn get_border(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let border = artifact.get_border(args.id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("border `{}` was not found", args.id),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved border `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(border)?);
Ok(response)
}
fn create_number_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateNumberFormatArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_number_format(
args.number_format,
args.source_number_format_id,
args.merge_with_existing_components.unwrap_or(false),
)?;
let number_format = artifact
.get_number_format(style_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::Serialization {
message: format!("created number format `{style_id}` was not available"),
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created number format `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(number_format)?);
Ok(response)
}
fn get_number_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let number_format = artifact
.get_number_format(args.id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("number format `{}` was not found", args.id),
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved number format `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(number_format)?);
Ok(response)
}
fn create_cell_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateCellFormatArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_cell_format(
args.format,
args.source_format_id,
args.merge_with_existing_components.unwrap_or(false),
)?;
let format = artifact.get_cell_format(style_id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::Serialization {
message: format!("created cell format `{style_id}` was not available"),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created cell format `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(format)?);
response.cell_format_summary = artifact.cell_format_summary(style_id);
Ok(response)
}
fn get_cell_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let format = artifact.get_cell_format(args.id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("cell format `{}` was not found", args.id),
}
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved cell format `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(format)?);
response.cell_format_summary = artifact.cell_format_summary(args.id);
Ok(response)
}
fn create_differential_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CreateDifferentialFormatArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact_mut(&artifact_id, &request.action)?;
let style_id = artifact.create_differential_format(args.format);
let format = artifact
.get_differential_format(style_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::Serialization {
message: format!("created differential format `{style_id}` was not available"),
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Created differential format `{style_id}`"),
snapshot_for_artifact(artifact),
);
response.style_id = Some(style_id);
response.serialized_dict = Some(to_serialized_value(format)?);
Ok(response)
}
fn get_differential_format(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: GetStyleArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let format = artifact
.get_differential_format(args.id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: request.action.clone(),
message: format!("differential format `{}` was not found", args.id),
})?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved differential format `{}`", args.id),
snapshot_for_artifact(artifact),
);
response.style_id = Some(args.id);
response.serialized_dict = Some(to_serialized_value(format)?);
Ok(response)
}
fn get_cell_format_summary(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: CellAddressArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let sheet = artifact.sheet_lookup(
&request.action,
args.sheet_name.as_deref(),
args.sheet_index.map(|value| value as usize),
)?;
let cell = sheet.get_cell_view(CellAddress::parse(&args.address)?);
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved cell format summary for `{}`", args.address),
snapshot_for_artifact(artifact),
);
response.cell_ref = Some(sheet.cell_ref(&args.address)?);
response.cell = Some(cell.clone());
response.cell_format_summary = artifact.cell_format_summary(cell.style_index);
Ok(response)
}
fn get_range_format_summary(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: RangeArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let sheet = artifact.sheet_lookup(
&request.action,
args.sheet_name.as_deref(),
args.sheet_index.map(|value| value as usize),
)?;
let range = CellRange::parse(&args.range)?;
let top_left_style_index = sheet.top_left_style_index(&range);
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!("Retrieved range format summary for `{}`", args.range),
snapshot_for_artifact(artifact),
);
response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range));
response.range_format = Some(sheet.range_format(&range));
response.range = Some(sheet.get_range_view(&range));
response.top_left_style_index = Some(top_left_style_index);
response.cell_format_summary = artifact.cell_format_summary(top_left_style_index);
Ok(response)
}
fn get_reference(
&mut self,
request: SpreadsheetArtifactRequest,
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
let args: ReferenceArgs = parse_args(&request.action, &request.args)?;
let artifact_id = required_artifact_id(&request)?;
let artifact = self.get_artifact(&artifact_id, &request.action)?;
let sheet = artifact.sheet_lookup(
&request.action,
args.sheet_name.as_deref(),
args.sheet_index.map(|value| value as usize),
)?;
let mut response = SpreadsheetArtifactResponse::new(
artifact_id,
request.action,
format!(
"Resolved reference `{}` on `{}`",
args.reference, sheet.name
),
snapshot_for_artifact(artifact),
);
match sheet.reference(&args.reference)? {
crate::SpreadsheetSheetReference::Cell { cell_ref } => {
let address = cell_ref.cell_address()?;
let cell = sheet.get_cell_view(address);
response.cell_format_summary = artifact.cell_format_summary(cell.style_index);
response.cell = Some(cell);
response.raw_cell = sheet.get_raw_cell(address);
response.cell_ref = Some(cell_ref);
}
crate::SpreadsheetSheetReference::Range { range_ref } => {
let range = range_ref.range()?;
let top_left_style_index = sheet.top_left_style_index(&range);
response.range = Some(sheet.get_range_view(&range));
response.range_ref = Some(range_ref);
response.range_format = Some(sheet.range_format(&range));
response.top_left_style_index = Some(top_left_style_index);
response.cell_format_summary = artifact.cell_format_summary(top_left_style_index);
response.rendered_text = Some(sheet.to_rendered_text(Some(&range)));
}
}
Ok(response)
}
fn get_cell(
&mut self,
request: SpreadsheetArtifactRequest,
@@ -684,7 +1117,9 @@ impl SpreadsheetArtifactManager {
format!("Retrieved cell `{}` from `{}`", args.address, sheet.name),
snapshot_for_artifact(artifact),
);
response.cell_format_summary = artifact.cell_format_summary(cell.style_index);
response.cell = Some(cell);
response.raw_cell = sheet.get_raw_cell(CellAddress::parse(&args.address)?);
response.cell_ref = Some(sheet.cell_ref(&args.address)?);
Ok(response)
}
@@ -714,7 +1149,10 @@ impl SpreadsheetArtifactManager {
),
snapshot_for_artifact(artifact),
);
response.cell = Some(sheet.get_cell_view_by_indices(args.column_index, args.row_index));
let cell = sheet.get_cell_view_by_indices(args.column_index, args.row_index);
response.cell_format_summary = artifact.cell_format_summary(cell.style_index);
response.cell = Some(cell);
response.raw_cell = sheet.get_raw_cell(address);
response.cell_ref = Some(sheet.cell_ref(address.to_a1())?);
Ok(response)
}
@@ -798,6 +1236,11 @@ impl SpreadsheetArtifactManager {
snapshot_for_artifact(artifact),
);
response.range = Some(sheet.get_range_view(&range));
response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range));
response.range_format = Some(sheet.range_format(&range));
response.top_left_style_index = Some(sheet.top_left_style_index(&range));
response.cell_format_summary =
artifact.cell_format_summary(sheet.top_left_style_index(&range));
response.rendered_text = Some(sheet.to_rendered_text(Some(&range)));
Ok(response)
}
@@ -1470,12 +1913,22 @@ pub struct SpreadsheetArtifactResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub range_ref: Option<SpreadsheetCellRangeRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub range_format: Option<crate::SpreadsheetRangeFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cell: Option<crate::SpreadsheetCellView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_cell: Option<crate::SpreadsheetCell>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_field: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub range: Option<SpreadsheetRangeView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_left_style_index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_format_summary: Option<crate::SpreadsheetCellFormatSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rendered_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub row_height: Option<f64>,
@@ -1505,9 +1958,14 @@ impl SpreadsheetArtifactResponse {
sheet_ref: None,
cell_ref: None,
range_ref: None,
range_format: None,
cell: None,
raw_cell: None,
style_id: None,
cell_field: None,
range: None,
top_left_style_index: None,
cell_format_summary: None,
rendered_text: None,
row_height: None,
serialized_dict: None,
@@ -1636,6 +2094,51 @@ struct GetRowHeightArgs {
row_index: u32,
}
#[derive(Debug, Deserialize)]
struct CreateTextStyleArgs {
style: crate::SpreadsheetTextStyle,
source_style_id: Option<u32>,
merge_with_existing_components: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct CreateFillArgs {
fill: crate::SpreadsheetFill,
source_fill_id: Option<u32>,
merge_with_existing_components: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct CreateBorderArgs {
border: crate::SpreadsheetBorder,
source_border_id: Option<u32>,
merge_with_existing_components: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct CreateNumberFormatArgs {
number_format: crate::SpreadsheetNumberFormat,
source_number_format_id: Option<u32>,
merge_with_existing_components: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct CreateCellFormatArgs {
format: crate::SpreadsheetCellFormat,
source_format_id: Option<u32>,
merge_with_existing_components: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct CreateDifferentialFormatArgs {
format: crate::SpreadsheetDifferentialFormat,
}
#[derive(Debug, Deserialize)]
struct GetStyleArgs {
id: u32,
}
#[derive(Debug, Deserialize)]
struct CellAddressArgs {
sheet_name: Option<String>,
@@ -1643,6 +2146,13 @@ struct CellAddressArgs {
address: String,
}
#[derive(Debug, Deserialize)]
struct ReferenceArgs {
sheet_name: Option<String>,
sheet_index: Option<u32>,
reference: String,
}
#[derive(Debug, Deserialize)]
struct CellIndicesArgs {
sheet_name: Option<String>,
@@ -1903,3 +2413,9 @@ fn resolve_path(cwd: &Path, path: &Path) -> PathBuf {
cwd.join(path)
}
}
fn to_serialized_value<T: Serialize>(value: T) -> Result<Value, SpreadsheetArtifactError> {
serde_json::to_value(value).map_err(|error| SpreadsheetArtifactError::Serialization {
message: error.to_string(),
})
}

View File

@@ -65,6 +65,11 @@ impl TryFrom<Value> for SpreadsheetCellValue {
fn try_from(value: Value) -> Result<Self, SpreadsheetArtifactError> {
match value {
Value::Object(_) => serde_json::from_value(value).map_err(|error| {
SpreadsheetArtifactError::Serialization {
message: error.to_string(),
}
}),
Value::Bool(value) => Ok(Self::Bool(value)),
Value::Number(value) => {
if let Some(integer) = value.as_i64() {
@@ -337,6 +342,14 @@ impl SpreadsheetCellRangeRef {
Ok(self.get(sheet)?.data)
}
pub fn top_left_style_index(
&self,
sheet: &SpreadsheetSheet,
) -> Result<u32, SpreadsheetArtifactError> {
self.ensure_sheet(sheet)?;
Ok(sheet.top_left_style_index(&self.range()?))
}
pub fn set_value(
&self,
sheet: &mut SpreadsheetSheet,
@@ -420,6 +433,13 @@ impl SpreadsheetCellRangeRef {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SpreadsheetSheetReference {
Cell { cell_ref: SpreadsheetCellRef },
Range { range_ref: SpreadsheetCellRangeRef },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpreadsheetSheetSummary {
pub name: String,
@@ -686,6 +706,19 @@ impl SpreadsheetSheet {
Ok(SpreadsheetCellRangeRef::new(self.name.clone(), &range))
}
pub fn reference(
&self,
address: impl AsRef<str>,
) -> Result<SpreadsheetSheetReference, SpreadsheetArtifactError> {
let address = address.as_ref();
if let Ok(cell_ref) = self.cell_ref(address) {
return Ok(SpreadsheetSheetReference::Cell { cell_ref });
}
Ok(SpreadsheetSheetReference::Range {
range_ref: self.range_ref(address)?,
})
}
pub fn set_column_widths(
&mut self,
reference: &str,
@@ -1156,6 +1189,12 @@ impl SpreadsheetSheet {
self.get_cell(address).cloned()
}
pub fn top_left_style_index(&self, range: &CellRange) -> u32 {
self.get_cell(range.start)
.map(|cell| cell.style_index)
.unwrap_or(0)
}
pub fn get_range_view(&self, range: &CellRange) -> SpreadsheetRangeView {
let mut values = Vec::new();
let mut formulas = Vec::new();
@@ -1307,6 +1346,18 @@ pub struct SpreadsheetArtifact {
#[serde(default)]
pub sheets: Vec<SpreadsheetSheet>,
pub auto_recalculate: bool,
#[serde(default)]
pub text_styles: BTreeMap<u32, crate::SpreadsheetTextStyle>,
#[serde(default)]
pub fills: BTreeMap<u32, crate::SpreadsheetFill>,
#[serde(default)]
pub borders: BTreeMap<u32, crate::SpreadsheetBorder>,
#[serde(default)]
pub number_formats: BTreeMap<u32, crate::SpreadsheetNumberFormat>,
#[serde(default)]
pub cell_formats: BTreeMap<u32, crate::SpreadsheetCellFormat>,
#[serde(default)]
pub differential_formats: BTreeMap<u32, crate::SpreadsheetDifferentialFormat>,
}
impl SpreadsheetArtifact {
@@ -1316,6 +1367,12 @@ impl SpreadsheetArtifact {
name,
sheets: Vec::new(),
auto_recalculate: false,
text_styles: BTreeMap::new(),
fills: BTreeMap::new(),
borders: BTreeMap::new(),
number_formats: BTreeMap::new(),
cell_formats: BTreeMap::new(),
differential_formats: BTreeMap::new(),
}
}

View File

@@ -0,0 +1,580 @@
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use crate::CellRange;
use crate::SpreadsheetArtifact;
use crate::SpreadsheetArtifactError;
use crate::SpreadsheetCellRangeRef;
use crate::SpreadsheetSheet;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SpreadsheetFontFace {
pub font_family: Option<String>,
pub font_scheme: Option<String>,
pub typeface: Option<String>,
}
impl SpreadsheetFontFace {
fn merge(&self, patch: &Self) -> Self {
Self {
font_family: patch
.font_family
.clone()
.or_else(|| self.font_family.clone()),
font_scheme: patch
.font_scheme
.clone()
.or_else(|| self.font_scheme.clone()),
typeface: patch.typeface.clone().or_else(|| self.typeface.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SpreadsheetTextStyle {
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underline: Option<bool>,
pub font_size: Option<f64>,
pub font_color: Option<String>,
pub text_alignment: Option<String>,
pub anchor: Option<String>,
pub vertical_text_orientation: Option<String>,
pub text_rotation: Option<i32>,
pub paragraph_spacing: Option<bool>,
pub bottom_inset: Option<f64>,
pub left_inset: Option<f64>,
pub right_inset: Option<f64>,
pub top_inset: Option<f64>,
pub font_family: Option<String>,
pub font_scheme: Option<String>,
pub typeface: Option<String>,
pub font_face: Option<SpreadsheetFontFace>,
}
impl SpreadsheetTextStyle {
fn merge(&self, patch: &Self) -> Self {
Self {
bold: patch.bold.or(self.bold),
italic: patch.italic.or(self.italic),
underline: patch.underline.or(self.underline),
font_size: patch.font_size.or(self.font_size),
font_color: patch.font_color.clone().or_else(|| self.font_color.clone()),
text_alignment: patch
.text_alignment
.clone()
.or_else(|| self.text_alignment.clone()),
anchor: patch.anchor.clone().or_else(|| self.anchor.clone()),
vertical_text_orientation: patch
.vertical_text_orientation
.clone()
.or_else(|| self.vertical_text_orientation.clone()),
text_rotation: patch.text_rotation.or(self.text_rotation),
paragraph_spacing: patch.paragraph_spacing.or(self.paragraph_spacing),
bottom_inset: patch.bottom_inset.or(self.bottom_inset),
left_inset: patch.left_inset.or(self.left_inset),
right_inset: patch.right_inset.or(self.right_inset),
top_inset: patch.top_inset.or(self.top_inset),
font_family: patch
.font_family
.clone()
.or_else(|| self.font_family.clone()),
font_scheme: patch
.font_scheme
.clone()
.or_else(|| self.font_scheme.clone()),
typeface: patch.typeface.clone().or_else(|| self.typeface.clone()),
font_face: match (&self.font_face, &patch.font_face) {
(Some(base), Some(update)) => Some(base.merge(update)),
(None, Some(update)) => Some(update.clone()),
(Some(base), None) => Some(base.clone()),
(None, None) => None,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpreadsheetGradientStop {
pub position: f64,
pub color: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpreadsheetFillRectangle {
pub left: f64,
pub right: f64,
pub top: f64,
pub bottom: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SpreadsheetFill {
pub solid_fill_color: Option<String>,
pub pattern_type: Option<String>,
pub pattern_foreground_color: Option<String>,
pub pattern_background_color: Option<String>,
#[serde(default)]
pub color_transforms: Vec<String>,
pub gradient_fill_type: Option<String>,
#[serde(default)]
pub gradient_stops: Vec<SpreadsheetGradientStop>,
pub gradient_kind: Option<String>,
pub angle: Option<f64>,
pub scaled: Option<bool>,
pub path_type: Option<String>,
pub fill_rectangle: Option<SpreadsheetFillRectangle>,
pub image_reference: Option<String>,
}
impl SpreadsheetFill {
fn merge(&self, patch: &Self) -> Self {
Self {
solid_fill_color: patch
.solid_fill_color
.clone()
.or_else(|| self.solid_fill_color.clone()),
pattern_type: patch
.pattern_type
.clone()
.or_else(|| self.pattern_type.clone()),
pattern_foreground_color: patch
.pattern_foreground_color
.clone()
.or_else(|| self.pattern_foreground_color.clone()),
pattern_background_color: patch
.pattern_background_color
.clone()
.or_else(|| self.pattern_background_color.clone()),
color_transforms: if patch.color_transforms.is_empty() {
self.color_transforms.clone()
} else {
patch.color_transforms.clone()
},
gradient_fill_type: patch
.gradient_fill_type
.clone()
.or_else(|| self.gradient_fill_type.clone()),
gradient_stops: if patch.gradient_stops.is_empty() {
self.gradient_stops.clone()
} else {
patch.gradient_stops.clone()
},
gradient_kind: patch
.gradient_kind
.clone()
.or_else(|| self.gradient_kind.clone()),
angle: patch.angle.or(self.angle),
scaled: patch.scaled.or(self.scaled),
path_type: patch.path_type.clone().or_else(|| self.path_type.clone()),
fill_rectangle: patch
.fill_rectangle
.clone()
.or_else(|| self.fill_rectangle.clone()),
image_reference: patch
.image_reference
.clone()
.or_else(|| self.image_reference.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SpreadsheetBorderLine {
pub style: Option<String>,
pub color: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SpreadsheetBorder {
pub top: Option<SpreadsheetBorderLine>,
pub right: Option<SpreadsheetBorderLine>,
pub bottom: Option<SpreadsheetBorderLine>,
pub left: Option<SpreadsheetBorderLine>,
}
impl SpreadsheetBorder {
fn merge(&self, patch: &Self) -> Self {
Self {
top: patch.top.clone().or_else(|| self.top.clone()),
right: patch.right.clone().or_else(|| self.right.clone()),
bottom: patch.bottom.clone().or_else(|| self.bottom.clone()),
left: patch.left.clone().or_else(|| self.left.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SpreadsheetAlignment {
pub horizontal: Option<String>,
pub vertical: Option<String>,
}
impl SpreadsheetAlignment {
fn merge(&self, patch: &Self) -> Self {
Self {
horizontal: patch.horizontal.clone().or_else(|| self.horizontal.clone()),
vertical: patch.vertical.clone().or_else(|| self.vertical.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SpreadsheetNumberFormat {
pub format_id: Option<u32>,
pub format_code: Option<String>,
}
impl SpreadsheetNumberFormat {
fn merge(&self, patch: &Self) -> Self {
Self {
format_id: patch.format_id.or(self.format_id),
format_code: patch
.format_code
.clone()
.or_else(|| self.format_code.clone()),
}
}
fn normalized(mut self) -> Self {
if self.format_code.is_none() {
self.format_code = self.format_id.and_then(builtin_number_format_code);
}
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SpreadsheetCellFormat {
pub text_style_id: Option<u32>,
pub fill_id: Option<u32>,
pub border_id: Option<u32>,
pub alignment: Option<SpreadsheetAlignment>,
pub number_format_id: Option<u32>,
pub wrap_text: Option<bool>,
pub base_cell_style_format_id: Option<u32>,
}
impl SpreadsheetCellFormat {
pub fn wrap(mut self) -> Self {
self.wrap_text = Some(true);
self
}
pub fn unwrap(mut self) -> Self {
self.wrap_text = Some(false);
self
}
fn merge(&self, patch: &Self) -> Self {
Self {
text_style_id: patch.text_style_id.or(self.text_style_id),
fill_id: patch.fill_id.or(self.fill_id),
border_id: patch.border_id.or(self.border_id),
alignment: match (&self.alignment, &patch.alignment) {
(Some(base), Some(update)) => Some(base.merge(update)),
(None, Some(update)) => Some(update.clone()),
(Some(base), None) => Some(base.clone()),
(None, None) => None,
},
number_format_id: patch.number_format_id.or(self.number_format_id),
wrap_text: patch.wrap_text.or(self.wrap_text),
base_cell_style_format_id: patch
.base_cell_style_format_id
.or(self.base_cell_style_format_id),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct SpreadsheetDifferentialFormat {
pub text_style_id: Option<u32>,
pub fill_id: Option<u32>,
pub border_id: Option<u32>,
pub alignment: Option<SpreadsheetAlignment>,
pub number_format_id: Option<u32>,
pub wrap_text: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpreadsheetCellFormatSummary {
pub style_index: u32,
pub text_style: Option<SpreadsheetTextStyle>,
pub fill: Option<SpreadsheetFill>,
pub border: Option<SpreadsheetBorder>,
pub alignment: Option<SpreadsheetAlignment>,
pub number_format: Option<SpreadsheetNumberFormat>,
pub wrap_text: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpreadsheetRangeFormat {
pub sheet_name: String,
pub range: String,
}
impl SpreadsheetRangeFormat {
pub fn new(sheet_name: String, range: &CellRange) -> Self {
Self {
sheet_name,
range: range.to_a1(),
}
}
pub fn range_ref(&self) -> Result<SpreadsheetCellRangeRef, SpreadsheetArtifactError> {
let range = CellRange::parse(&self.range)?;
Ok(SpreadsheetCellRangeRef::new(
self.sheet_name.clone(),
&range,
))
}
pub fn top_left_style_index(
&self,
sheet: &SpreadsheetSheet,
) -> Result<u32, SpreadsheetArtifactError> {
self.range_ref()?.top_left_style_index(sheet)
}
pub fn top_left_cell_format(
&self,
artifact: &SpreadsheetArtifact,
sheet: &SpreadsheetSheet,
) -> Result<Option<SpreadsheetCellFormatSummary>, SpreadsheetArtifactError> {
let range = self.range_ref()?.range()?;
Ok(artifact.cell_format_summary(sheet.top_left_style_index(&range)))
}
}
impl SpreadsheetArtifact {
pub fn create_text_style(
&mut self,
style: SpreadsheetTextStyle,
source_style_id: Option<u32>,
merge_with_existing_components: bool,
) -> Result<u32, SpreadsheetArtifactError> {
let created = if let Some(source_style_id) = source_style_id {
let source = self
.text_styles
.get(&source_style_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: "create_text_style".to_string(),
message: format!("text style `{source_style_id}` was not found"),
})?;
if merge_with_existing_components {
source.merge(&style)
} else {
style
}
} else {
style
};
Ok(insert_with_next_id(&mut self.text_styles, created))
}
pub fn get_text_style(&self, style_id: u32) -> Option<&SpreadsheetTextStyle> {
self.text_styles.get(&style_id)
}
pub fn create_fill(
&mut self,
fill: SpreadsheetFill,
source_fill_id: Option<u32>,
merge_with_existing_components: bool,
) -> Result<u32, SpreadsheetArtifactError> {
let created = if let Some(source_fill_id) = source_fill_id {
let source = self.fills.get(&source_fill_id).cloned().ok_or_else(|| {
SpreadsheetArtifactError::InvalidArgs {
action: "create_fill".to_string(),
message: format!("fill `{source_fill_id}` was not found"),
}
})?;
if merge_with_existing_components {
source.merge(&fill)
} else {
fill
}
} else {
fill
};
Ok(insert_with_next_id(&mut self.fills, created))
}
pub fn get_fill(&self, fill_id: u32) -> Option<&SpreadsheetFill> {
self.fills.get(&fill_id)
}
pub fn create_border(
&mut self,
border: SpreadsheetBorder,
source_border_id: Option<u32>,
merge_with_existing_components: bool,
) -> Result<u32, SpreadsheetArtifactError> {
let created = if let Some(source_border_id) = source_border_id {
let source = self
.borders
.get(&source_border_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: "create_border".to_string(),
message: format!("border `{source_border_id}` was not found"),
})?;
if merge_with_existing_components {
source.merge(&border)
} else {
border
}
} else {
border
};
Ok(insert_with_next_id(&mut self.borders, created))
}
pub fn get_border(&self, border_id: u32) -> Option<&SpreadsheetBorder> {
self.borders.get(&border_id)
}
pub fn create_number_format(
&mut self,
format: SpreadsheetNumberFormat,
source_number_format_id: Option<u32>,
merge_with_existing_components: bool,
) -> Result<u32, SpreadsheetArtifactError> {
let created = if let Some(source_number_format_id) = source_number_format_id {
let source = self
.number_formats
.get(&source_number_format_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: "create_number_format".to_string(),
message: format!("number format `{source_number_format_id}` was not found"),
})?;
if merge_with_existing_components {
source.merge(&format)
} else {
format
}
} else {
format
};
Ok(insert_with_next_id(
&mut self.number_formats,
created.normalized(),
))
}
pub fn get_number_format(&self, number_format_id: u32) -> Option<&SpreadsheetNumberFormat> {
self.number_formats.get(&number_format_id)
}
pub fn create_cell_format(
&mut self,
format: SpreadsheetCellFormat,
source_format_id: Option<u32>,
merge_with_existing_components: bool,
) -> Result<u32, SpreadsheetArtifactError> {
let created = if let Some(source_format_id) = source_format_id {
let source = self
.cell_formats
.get(&source_format_id)
.cloned()
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
action: "create_cell_format".to_string(),
message: format!("cell format `{source_format_id}` was not found"),
})?;
if merge_with_existing_components {
source.merge(&format)
} else {
format
}
} else {
format
};
Ok(insert_with_next_id(&mut self.cell_formats, created))
}
pub fn get_cell_format(&self, format_id: u32) -> Option<&SpreadsheetCellFormat> {
self.cell_formats.get(&format_id)
}
pub fn create_differential_format(&mut self, format: SpreadsheetDifferentialFormat) -> u32 {
insert_with_next_id(&mut self.differential_formats, format)
}
pub fn get_differential_format(
&self,
format_id: u32,
) -> Option<&SpreadsheetDifferentialFormat> {
self.differential_formats.get(&format_id)
}
pub fn resolve_cell_format(&self, style_index: u32) -> Option<SpreadsheetCellFormat> {
let format = self.cell_formats.get(&style_index)?.clone();
resolve_cell_format_recursive(&self.cell_formats, &format, 0)
}
pub fn cell_format_summary(&self, style_index: u32) -> Option<SpreadsheetCellFormatSummary> {
let resolved = self.resolve_cell_format(style_index)?;
Some(SpreadsheetCellFormatSummary {
style_index,
text_style: resolved
.text_style_id
.and_then(|id| self.text_styles.get(&id).cloned()),
fill: resolved.fill_id.and_then(|id| self.fills.get(&id).cloned()),
border: resolved
.border_id
.and_then(|id| self.borders.get(&id).cloned()),
alignment: resolved.alignment,
number_format: resolved
.number_format_id
.and_then(|id| self.number_formats.get(&id).cloned()),
wrap_text: resolved.wrap_text,
})
}
}
impl SpreadsheetSheet {
pub fn range_format(&self, range: &CellRange) -> SpreadsheetRangeFormat {
SpreadsheetRangeFormat::new(self.name.clone(), range)
}
}
fn insert_with_next_id<T>(map: &mut BTreeMap<u32, T>, value: T) -> u32 {
let next_id = map.last_key_value().map(|(key, _)| key + 1).unwrap_or(1);
map.insert(next_id, value);
next_id
}
fn resolve_cell_format_recursive(
cell_formats: &BTreeMap<u32, SpreadsheetCellFormat>,
format: &SpreadsheetCellFormat,
depth: usize,
) -> Option<SpreadsheetCellFormat> {
if depth > 32 {
return None;
}
let base = format
.base_cell_style_format_id
.and_then(|id| cell_formats.get(&id))
.and_then(|base| resolve_cell_format_recursive(cell_formats, base, depth + 1));
Some(match base {
Some(base) => base.merge(format),
None => format.clone(),
})
}
fn builtin_number_format_code(format_id: u32) -> Option<String> {
match format_id {
0 => Some("General".to_string()),
1 => Some("0".to_string()),
2 => Some("0.00".to_string()),
3 => Some("#,##0".to_string()),
4 => Some("#,##0.00".to_string()),
9 => Some("0%".to_string()),
10 => Some("0.00%".to_string()),
_ => None,
}
}

View File

@@ -8,9 +8,16 @@ use crate::SpreadsheetArtifact;
use crate::SpreadsheetArtifactManager;
use crate::SpreadsheetArtifactRequest;
use crate::SpreadsheetCell;
use crate::SpreadsheetCellFormat;
use crate::SpreadsheetCellFormatSummary;
use crate::SpreadsheetCellValue;
use crate::SpreadsheetFileType;
use crate::SpreadsheetFill;
use crate::SpreadsheetFontFace;
use crate::SpreadsheetNumberFormat;
use crate::SpreadsheetSheet;
use crate::SpreadsheetSheetReference;
use crate::SpreadsheetTextStyle;
#[test]
fn manager_can_create_edit_recalculate_and_export() -> Result<(), Box<dyn std::error::Error>> {
@@ -554,3 +561,491 @@ fn manager_supports_bulk_sizes_and_row_heights() -> Result<(), Box<dyn std::erro
);
Ok(())
}
#[test]
fn manager_style_registry_and_format_summaries_work() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
let mut manager = SpreadsheetArtifactManager::default();
let created = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: None,
action: "create".to_string(),
args: serde_json::json!({ "name": "Styles" }),
},
temp_dir.path(),
)?;
let artifact_id = created.artifact_id;
manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_sheet".to_string(),
args: serde_json::json!({ "name": "Sheet1" }),
},
temp_dir.path(),
)?;
let text_style = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_text_style".to_string(),
args: serde_json::json!({
"style": {
"bold": true,
"italic": true,
"underline": true,
"font_size": 14.0,
"font_color": "#112233",
"text_alignment": "center",
"anchor": "middle",
"vertical_text_orientation": "stacked",
"text_rotation": 90,
"paragraph_spacing": true,
"bottom_inset": 1.0,
"left_inset": 2.0,
"right_inset": 3.0,
"top_inset": 4.0,
"font_family": "IBM Plex Sans",
"font_scheme": "minor",
"typeface": "IBM Plex Sans",
"font_face": {
"font_family": "IBM Plex Sans",
"font_scheme": "minor",
"typeface": "IBM Plex Sans"
}
}
}),
},
temp_dir.path(),
)?;
let text_style_id = text_style.style_id.expect("text style id");
let fill = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_fill".to_string(),
args: serde_json::json!({
"fill": {
"solid_fill_color": "#ffeeaa",
"pattern_type": "solid",
"pattern_foreground_color": "#ffeeaa",
"pattern_background_color": "#221100",
"color_transforms": ["tint:0.2"],
"gradient_fill_type": "linear",
"gradient_stops": [
{ "position": 0.0, "color": "#ffeeaa" },
{ "position": 1.0, "color": "#aa5500" }
],
"gradient_kind": "linear",
"angle": 45.0,
"scaled": true,
"path_type": "rect",
"fill_rectangle": {
"left": 0.0,
"right": 1.0,
"top": 0.0,
"bottom": 1.0
},
"image_reference": "image://fill"
}
}),
},
temp_dir.path(),
)?;
let fill_id = fill.style_id.expect("fill id");
let border = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_border".to_string(),
args: serde_json::json!({
"border": {
"top": { "style": "solid", "color": "#111111" },
"right": { "style": "dashed", "color": "#222222" },
"bottom": { "style": "double", "color": "#333333" },
"left": { "style": "solid", "color": "#444444" }
}
}),
},
temp_dir.path(),
)?;
let border_id = border.style_id.expect("border id");
let number_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_number_format".to_string(),
args: serde_json::json!({
"number_format": {
"format_id": 4
}
}),
},
temp_dir.path(),
)?;
let number_format_id = number_format.style_id.expect("number format id");
let base_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_cell_format".to_string(),
args: serde_json::json!({
"format": {
"text_style_id": text_style_id,
"number_format_id": number_format_id,
"alignment": {
"horizontal": "center",
"vertical": "middle"
}
}
}),
},
temp_dir.path(),
)?;
let base_format_id = base_format.style_id.expect("base format id");
let derived_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_cell_format".to_string(),
args: serde_json::json!({
"format": {
"fill_id": fill_id,
"border_id": border_id,
"wrap_text": true,
"base_cell_style_format_id": base_format_id
}
}),
},
temp_dir.path(),
)?;
let derived_format_id = derived_format.style_id.expect("derived format id");
let merged_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_cell_format".to_string(),
args: serde_json::json!({
"source_format_id": derived_format_id,
"merge_with_existing_components": true,
"format": {
"alignment": {
"vertical": "bottom"
}
}
}),
},
temp_dir.path(),
)?;
let merged_format_id = merged_format.style_id.expect("merged format id");
let differential_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "create_differential_format".to_string(),
args: serde_json::json!({
"format": {
"fill_id": fill_id,
"wrap_text": true
}
}),
},
temp_dir.path(),
)?;
let differential_format_id = differential_format.style_id.expect("dxf id");
manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "set_cell_style".to_string(),
args: serde_json::json!({
"sheet_name": "Sheet1",
"address": "A1",
"style_index": merged_format_id
}),
},
temp_dir.path(),
)?;
let summary = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_cell_format_summary".to_string(),
args: serde_json::json!({
"sheet_name": "Sheet1",
"address": "A1"
}),
},
temp_dir.path(),
)?;
assert_eq!(
summary.cell_format_summary,
Some(SpreadsheetCellFormatSummary {
style_index: merged_format_id,
text_style: Some(SpreadsheetTextStyle {
bold: Some(true),
italic: Some(true),
underline: Some(true),
font_size: Some(14.0),
font_color: Some("#112233".to_string()),
text_alignment: Some("center".to_string()),
anchor: Some("middle".to_string()),
vertical_text_orientation: Some("stacked".to_string()),
text_rotation: Some(90),
paragraph_spacing: Some(true),
bottom_inset: Some(1.0),
left_inset: Some(2.0),
right_inset: Some(3.0),
top_inset: Some(4.0),
font_family: Some("IBM Plex Sans".to_string()),
font_scheme: Some("minor".to_string()),
typeface: Some("IBM Plex Sans".to_string()),
font_face: Some(SpreadsheetFontFace {
font_family: Some("IBM Plex Sans".to_string()),
font_scheme: Some("minor".to_string()),
typeface: Some("IBM Plex Sans".to_string()),
}),
}),
fill: Some(SpreadsheetFill {
solid_fill_color: Some("#ffeeaa".to_string()),
pattern_type: Some("solid".to_string()),
pattern_foreground_color: Some("#ffeeaa".to_string()),
pattern_background_color: Some("#221100".to_string()),
color_transforms: vec!["tint:0.2".to_string()],
gradient_fill_type: Some("linear".to_string()),
gradient_stops: vec![
crate::SpreadsheetGradientStop {
position: 0.0,
color: "#ffeeaa".to_string(),
},
crate::SpreadsheetGradientStop {
position: 1.0,
color: "#aa5500".to_string(),
},
],
gradient_kind: Some("linear".to_string()),
angle: Some(45.0),
scaled: Some(true),
path_type: Some("rect".to_string()),
fill_rectangle: Some(crate::SpreadsheetFillRectangle {
left: 0.0,
right: 1.0,
top: 0.0,
bottom: 1.0,
}),
image_reference: Some("image://fill".to_string()),
}),
border: Some(crate::SpreadsheetBorder {
top: Some(crate::SpreadsheetBorderLine {
style: Some("solid".to_string()),
color: Some("#111111".to_string()),
}),
right: Some(crate::SpreadsheetBorderLine {
style: Some("dashed".to_string()),
color: Some("#222222".to_string()),
}),
bottom: Some(crate::SpreadsheetBorderLine {
style: Some("double".to_string()),
color: Some("#333333".to_string()),
}),
left: Some(crate::SpreadsheetBorderLine {
style: Some("solid".to_string()),
color: Some("#444444".to_string()),
}),
}),
alignment: Some(crate::SpreadsheetAlignment {
horizontal: Some("center".to_string()),
vertical: Some("bottom".to_string()),
}),
number_format: Some(SpreadsheetNumberFormat {
format_id: Some(4),
format_code: Some("#,##0.00".to_string()),
}),
wrap_text: Some(true),
})
);
let retrieved_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_cell_format".to_string(),
args: serde_json::json!({ "id": merged_format_id }),
},
temp_dir.path(),
)?;
let retrieved_format: SpreadsheetCellFormat =
serde_json::from_value(retrieved_format.serialized_dict.expect("cell format"))?;
assert_eq!(
retrieved_format,
SpreadsheetCellFormat {
text_style_id: None,
fill_id: Some(fill_id),
border_id: Some(border_id),
alignment: Some(crate::SpreadsheetAlignment {
horizontal: None,
vertical: Some("bottom".to_string()),
}),
number_format_id: None,
wrap_text: Some(true),
base_cell_style_format_id: Some(base_format_id),
}
);
let retrieved_number_format = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_number_format".to_string(),
args: serde_json::json!({ "id": number_format_id }),
},
temp_dir.path(),
)?;
assert_eq!(
serde_json::from_value::<SpreadsheetNumberFormat>(
retrieved_number_format
.serialized_dict
.expect("number format")
)?,
SpreadsheetNumberFormat {
format_id: Some(4),
format_code: Some("#,##0.00".to_string()),
}
);
let retrieved_text_style = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_text_style".to_string(),
args: serde_json::json!({ "id": text_style_id }),
},
temp_dir.path(),
)?;
assert_eq!(
serde_json::from_value::<SpreadsheetTextStyle>(
retrieved_text_style.serialized_dict.expect("text style")
)?
.bold,
Some(true)
);
let range_summary = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_range_format_summary".to_string(),
args: serde_json::json!({
"sheet_name": "Sheet1",
"range": "A1:B2"
}),
},
temp_dir.path(),
)?;
assert_eq!(range_summary.top_left_style_index, Some(merged_format_id));
assert_eq!(
range_summary
.range_format
.as_ref()
.map(|format| format.range.clone()),
Some("A1:B2".to_string())
);
let retrieved_dxf = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id),
action: "get_differential_format".to_string(),
args: serde_json::json!({ "id": differential_format_id }),
},
temp_dir.path(),
)?;
assert_eq!(
serde_json::from_value::<crate::SpreadsheetDifferentialFormat>(
retrieved_dxf.serialized_dict.expect("differential format")
)?
.wrap_text,
Some(true)
);
Ok(())
}
#[test]
fn sheet_references_resolve_cells_and_ranges() -> Result<(), Box<dyn std::error::Error>> {
let sheet = SpreadsheetSheet::new("Sheet1".to_string());
assert_eq!(
sheet.reference("A1")?,
SpreadsheetSheetReference::Cell {
cell_ref: sheet.cell_ref("A1")?,
}
);
assert_eq!(
sheet.reference("A1:B2")?,
SpreadsheetSheetReference::Range {
range_ref: sheet.range_ref("A1:B2")?,
}
);
Ok(())
}
#[test]
fn manager_get_reference_and_xlsx_import_preserve_workbook_name()
-> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
let path = temp_dir.path().join("named.xlsx");
let mut artifact = SpreadsheetArtifact::new(Some("Named Workbook".to_string()));
artifact.create_sheet("Sheet1".to_string())?.set_value(
CellAddress::parse("A1")?,
Some(SpreadsheetCellValue::Integer(9)),
)?;
artifact.export(&path)?;
let restored = SpreadsheetArtifact::from_source_file(&path, None)?;
assert_eq!(restored.name, Some("Named Workbook".to_string()));
let mut manager = SpreadsheetArtifactManager::default();
let imported = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: None,
action: "read".to_string(),
args: serde_json::json!({ "path": path }),
},
temp_dir.path(),
)?;
let artifact_id = imported.artifact_id;
let cell_reference = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id.clone()),
action: "get_reference".to_string(),
args: serde_json::json!({
"sheet_name": "Sheet1",
"reference": "A1"
}),
},
temp_dir.path(),
)?;
assert_eq!(
cell_reference
.raw_cell
.as_ref()
.and_then(|cell| cell.value.clone()),
Some(SpreadsheetCellValue::Integer(9))
);
let range_reference = manager.execute(
SpreadsheetArtifactRequest {
artifact_id: Some(artifact_id),
action: "get_reference".to_string(),
args: serde_json::json!({
"sheet_name": "Sheet1",
"reference": "A1:B2"
}),
},
temp_dir.path(),
)?;
assert_eq!(
range_reference
.range_ref
.as_ref()
.map(|range_ref| range_ref.address.clone()),
Some("A1:B2".to_string())
);
Ok(())
}

View File

@@ -195,6 +195,13 @@ pub(crate) fn import_xlsx(
let workbook_xml = read_zip_entry(&mut archive, "xl/workbook.xml", path)?;
let workbook_rels = read_zip_entry(&mut archive, "xl/_rels/workbook.xml.rels", path)?;
let workbook_name = if archive.by_name("docProps/core.xml").is_ok() {
let title =
extract_workbook_title(&read_zip_entry(&mut archive, "docProps/core.xml", path)?);
(!title.trim().is_empty()).then_some(title)
} else {
None
};
let shared_strings = if archive.by_name("xl/sharedStrings.xml").is_ok() {
Some(parse_shared_strings(&read_zip_entry(
&mut archive,
@@ -226,11 +233,11 @@ pub(crate) fn import_xlsx(
})
.collect::<Result<Vec<_>, SpreadsheetArtifactError>>()?;
let mut artifact = SpreadsheetArtifact::new(
let mut artifact = SpreadsheetArtifact::new(workbook_name.or_else(|| {
path.file_stem()
.and_then(|value| value.to_str())
.map(str::to_string),
);
.map(str::to_string)
}));
if let Some(artifact_id) = artifact_id {
artifact.artifact_id = artifact_id;
}
@@ -802,6 +809,18 @@ fn first_tag_text(xml: &str, tag: &str) -> Option<String> {
captures.get(1).map(|value| value.as_str().to_string())
}
fn extract_workbook_title(xml: &str) -> String {
let Ok(regex) =
Regex::new(r#"(?s)<(?:[A-Za-z0-9_]+:)?title\b[^>]*>(.*?)</(?:[A-Za-z0-9_]+:)?title>"#)
else {
return String::new();
};
regex
.captures(xml)
.and_then(|captures| captures.get(1).map(|value| xml_unescape(value.as_str())))
.unwrap_or_default()
}
fn all_text_nodes(xml: &str) -> Result<String, SpreadsheetArtifactError> {
let regex = Regex::new(r#"(?s)<t\b[^>]*>(.*?)</t>"#).map_err(|error| {
SpreadsheetArtifactError::Serialization {