chore: refactor docx_tool to reduce function size (#6273)

This commit is contained in:
Bradley Axen
2026-01-05 20:41:07 -08:00
committed by GitHub
parent f6042fa6fe
commit 41dbddade6
@@ -11,7 +11,7 @@ enum UpdateMode {
old_text: String, old_text: String,
}, },
InsertStructured { InsertStructured {
level: Option<String>, // e.g., "Heading1", "Heading2", etc. level: Option<String>,
style: Option<DocxStyle>, style: Option<DocxStyle>,
}, },
AddImage { AddImage {
@@ -46,18 +46,11 @@ impl DocxStyle {
alignment: obj alignment: obj
.get("alignment") .get("alignment")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.and_then(|a| match a { .and_then(parse_alignment),
"left" => Some(AlignmentType::Left),
"center" => Some(AlignmentType::Center),
"right" => Some(AlignmentType::Right),
"justified" => Some(AlignmentType::Both),
_ => None,
}),
}) })
} }
fn apply_to_run(&self, run: Run) -> Run { fn apply_to_run(&self, mut run: Run) -> Run {
let mut run = run;
if self.bold { if self.bold {
run = run.bold(); run = run.bold();
} }
@@ -76,8 +69,7 @@ impl DocxStyle {
run run
} }
fn apply_to_paragraph(&self, para: Paragraph) -> Paragraph { fn apply_to_paragraph(&self, mut para: Paragraph) -> Paragraph {
let mut para = para;
if let Some(alignment) = self.alignment { if let Some(alignment) = self.alignment {
para = para.align(alignment); para = para.align(alignment);
} }
@@ -85,44 +77,56 @@ impl DocxStyle {
} }
} }
pub async fn docx_tool( fn parse_alignment(a: &str) -> Option<AlignmentType> {
path: &str, match a {
operation: &str, "left" => Some(AlignmentType::Left),
content: Option<&str>, "center" => Some(AlignmentType::Center),
params: Option<&serde_json::Value>, "right" => Some(AlignmentType::Right),
) -> Result<Vec<Content>, ErrorData> { "justified" => Some(AlignmentType::Both),
match operation { _ => None,
"extract_text" => {
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
let docx = read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?;
let mut text = String::new();
let mut structure = Vec::new();
let mut current_level = None;
// Extract document structure and text
for element in docx.document.children.iter() {
if let DocumentChild::Paragraph(p) = element {
// Check for heading style
if let Some(style) = p.property.style.as_ref() {
if style.val.starts_with("Heading") {
current_level = Some(style.val.clone());
structure.push(format!("{}: ", style.val));
} }
} }
// Extract text from runs fn docx_error(message: impl Into<String>) -> ErrorData {
let para_text: String = p ErrorData {
.children code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(message.into()),
data: None,
}
}
fn invalid_params(message: impl Into<String>) -> ErrorData {
ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(message.into()),
data: None,
}
}
fn read_docx_file(path: &str) -> Result<Docx, ErrorData> {
let file =
fs::read(path).map_err(|e| docx_error(format!("Failed to read DOCX file: {}", e)))?;
read_docx(&file).map_err(|e| docx_error(format!("Failed to parse DOCX file: {}", e)))
}
fn read_or_create_docx(path: &str) -> Result<Docx, ErrorData> {
if std::path::Path::new(path).exists() {
read_docx_file(path)
} else {
Ok(Docx::new())
}
}
fn write_docx_file(path: &str, doc: Docx) -> Result<(), ErrorData> {
let mut buf = Vec::new();
doc.build()
.pack(&mut Cursor::new(&mut buf))
.map_err(|e| docx_error(format!("Failed to build DOCX: {}", e)))?;
fs::write(path, &buf).map_err(|e| docx_error(format!("Failed to write DOCX file: {}", e)))
}
fn extract_paragraph_text(p: &Paragraph) -> String {
p.children
.iter() .iter()
.filter_map(|child| { .filter_map(|child| {
if let ParagraphChild::Run(run) = child { if let ParagraphChild::Run(run) = child {
@@ -144,20 +148,123 @@ pub async fn docx_tool(
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(""); .join("")
}
fn add_styled_paragraphs(mut doc: Docx, content: &str, style: &Option<DocxStyle>) -> Docx {
for para in content.split('\n').filter(|p| !p.trim().is_empty()) {
let mut run = Run::new().add_text(para);
let mut paragraph = Paragraph::new();
if let Some(s) = style {
run = s.apply_to_run(run);
paragraph = s.apply_to_paragraph(paragraph);
}
doc = doc.add_paragraph(paragraph.add_run(run));
}
doc
}
fn parse_update_mode(
params: Option<&serde_json::Value>,
) -> Result<(UpdateMode, Option<DocxStyle>), ErrorData> {
let Some(params) = params else {
return Ok((UpdateMode::Append, None));
};
let mode_str = params
.get("mode")
.and_then(|v| v.as_str())
.unwrap_or("append");
let style = params.get("style").and_then(DocxStyle::from_json);
let mode = match mode_str {
"append" => UpdateMode::Append,
"replace" => {
let old_text = params
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| invalid_params("old_text parameter required for replace mode"))?;
UpdateMode::Replace {
old_text: old_text.to_string(),
}
}
"structured" => UpdateMode::InsertStructured {
level: params
.get("level")
.and_then(|v| v.as_str())
.map(String::from),
style: style.clone(),
},
"add_image" => {
let image_path = params
.get("image_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
invalid_params("image_path parameter required for add_image mode")
})?;
UpdateMode::AddImage {
image_path: image_path.to_string(),
width: params
.get("width")
.and_then(|v| v.as_u64())
.map(|w| w as u32),
height: params
.get("height")
.and_then(|v| v.as_u64())
.map(|h| h as u32),
}
}
_ => {
return Err(invalid_params(
"Invalid mode. Must be 'append', 'replace', 'structured', or 'add_image'",
))
}
};
Ok((mode, style))
}
fn extract_text_from_docx(docx: &Docx) -> String {
let mut text = String::new();
for element in docx.document.children.iter() {
if let DocumentChild::Paragraph(p) = element {
let para_text = extract_paragraph_text(p);
if !para_text.trim().is_empty() { if !para_text.trim().is_empty() {
if current_level.is_some() {
if let Some(s) = structure.last_mut() {
s.push_str(&para_text);
}
current_level = None;
}
text.push_str(&para_text); text.push_str(&para_text);
text.push('\n'); text.push('\n');
} }
} }
} }
text
}
fn extract_structure_from_docx(docx: &Docx) -> Vec<String> {
let mut structure = Vec::new();
let mut current_level = None;
for element in docx.document.children.iter() {
if let DocumentChild::Paragraph(p) = element {
if let Some(style) = p.property.style.as_ref() {
if style.val.starts_with("Heading") {
current_level = Some(style.val.clone());
structure.push(format!("{}: ", style.val));
}
}
let para_text = extract_paragraph_text(p);
if !para_text.trim().is_empty() && current_level.is_some() {
if let Some(s) = structure.last_mut() {
s.push_str(&para_text);
}
current_level = None;
}
}
}
structure
}
fn do_extract_text(path: &str) -> Result<Vec<Content>, ErrorData> {
let docx = read_docx_file(path)?;
let text = extract_text_from_docx(&docx);
let structure = extract_structure_from_docx(&docx);
let result = if !structure.is_empty() { let result = if !structure.is_empty() {
format!( format!(
@@ -168,209 +275,43 @@ pub async fn docx_tool(
} else { } else {
format!("Extracted Text:\n{}", text) format!("Extracted Text:\n{}", text)
}; };
Ok(vec![Content::text(result)]) Ok(vec![Content::text(result)])
} }
"update_doc" => { fn do_append(
let content = content.ok_or_else(|| ErrorData { path: &str,
code: ErrorCode::INVALID_PARAMS, content: &str,
message: Cow::from("Content parameter required for update_doc"), style: &Option<DocxStyle>,
data: None, ) -> Result<Vec<Content>, ErrorData> {
})?; let doc = read_or_create_docx(path)?;
let doc = add_styled_paragraphs(doc, content, style);
// Parse update mode and style from params write_docx_file(path, doc)?;
let (mode, style) = if let Some(params) = params {
let mode = params
.get("mode")
.and_then(|v| v.as_str())
.unwrap_or("append");
let style = params.get("style").and_then(DocxStyle::from_json);
let mode = match mode {
"append" => UpdateMode::Append,
"replace" => {
let old_text = params
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("old_text parameter required for replace mode"),
data: None,
})?;
UpdateMode::Replace {
old_text: old_text.to_string(),
}
}
"structured" => {
let level = params
.get("level")
.and_then(|v| v.as_str())
.map(String::from);
UpdateMode::InsertStructured {
level,
style: style.clone(),
}
}
"add_image" => {
let image_path = params
.get("image_path")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("image_path parameter required for add_image mode"),
data: None,
})?
.to_string();
let width = params
.get("width")
.and_then(|v| v.as_u64())
.map(|w| w as u32);
let height = params
.get("height")
.and_then(|v| v.as_u64())
.map(|h| h as u32);
UpdateMode::AddImage {
image_path,
width,
height,
}
}
_ => return Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Invalid mode. Must be 'append', 'replace', 'structured', or 'add_image'"),
data: None,
}),
};
(mode, style)
} else {
(UpdateMode::Append, None)
};
match mode {
UpdateMode::Append => {
// Read existing document if it exists, or create new one
let mut doc = if std::path::Path::new(path).exists() {
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
};
// Split content into paragraphs and add them
for para in content.split('\n') {
if !para.trim().is_empty() {
let mut run = Run::new().add_text(para);
let mut paragraph = Paragraph::new();
if let Some(style) = &style {
run = style.apply_to_run(run);
paragraph = style.apply_to_paragraph(paragraph);
}
doc = doc.add_paragraph(paragraph.add_run(run));
}
}
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"Successfully wrote content to {}", "Successfully wrote content to {}",
path path
))]) ))])
} }
UpdateMode::Replace { old_text } => { fn do_replace(
// Read existing document path: &str,
let file = fs::read(path).map_err(|e| ErrorData { content: &str,
code: ErrorCode::INTERNAL_ERROR, old_text: &str,
message: Cow::from(format!("Failed to read DOCX file: {}", e)), style: &Option<DocxStyle>,
data: None, ) -> Result<Vec<Content>, ErrorData> {
})?; let docx = read_docx_file(path)?;
let docx = read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?;
let mut new_doc = Docx::new(); let mut new_doc = Docx::new();
let mut found_text = false; let mut found_text = false;
// Process each paragraph
for element in docx.document.children.iter() { for element in docx.document.children.iter() {
if let DocumentChild::Paragraph(p) = element { if let DocumentChild::Paragraph(p) = element {
let para_text: String = p let para_text = extract_paragraph_text(p);
.children if para_text.contains(old_text) {
.iter()
.filter_map(|child| {
if let ParagraphChild::Run(run) = child {
Some(
run.children
.iter()
.filter_map(|rc| {
if let RunChild::Text(t) = rc {
Some(t.text.clone())
} else {
None
}
})
.collect::<Vec<_>>()
.join(""),
)
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
if para_text.contains(&old_text) {
// Replace this paragraph with new content
found_text = true; found_text = true;
for para in content.split('\n') { new_doc = add_styled_paragraphs(new_doc, content, style);
if !para.trim().is_empty() {
let mut run = Run::new().add_text(para);
let mut paragraph = Paragraph::new();
if let Some(style) = &style {
run = style.apply_to_run(run);
paragraph = style.apply_to_paragraph(paragraph);
}
new_doc = new_doc.add_paragraph(paragraph.add_run(run));
}
}
} else { } else {
// Create a new paragraph with the same content and style
let mut para = Paragraph::new(); let mut para = Paragraph::new();
if let Some(style) = &p.property.style { if let Some(s) = &p.property.style {
para = para.style(&style.val); para = para.style(&s.val);
} }
for child in p.children.iter() { for child in p.children.iter() {
if let ParagraphChild::Run(run) = child { if let ParagraphChild::Run(run) = child {
@@ -387,178 +328,95 @@ pub async fn docx_tool(
} }
if !found_text { if !found_text {
return Err(ErrorData { return Err(docx_error(format!(
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!(
"Could not find text to replace: {}", "Could not find text to replace: {}",
old_text old_text
)), )));
data: None,
});
} }
write_docx_file(path, new_doc)?;
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
new_doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"Successfully replaced content in {}", "Successfully replaced content in {}",
path path
))]) ))])
} }
UpdateMode::InsertStructured { level, style } => { fn do_insert_structured(
let mut doc = if std::path::Path::new(path).exists() { path: &str,
let file = fs::read(path).map_err(|e| ErrorData { content: &str,
code: ErrorCode::INTERNAL_ERROR, level: &Option<String>,
message: Cow::from(format!("Failed to read DOCX file: {}", e)), style: &Option<DocxStyle>,
data: None, ) -> Result<Vec<Content>, ErrorData> {
})?; let mut doc = read_or_create_docx(path)?;
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
};
// Create the paragraph with heading style if specified for para in content.split('\n').filter(|p| !p.trim().is_empty()) {
for para in content.split('\n') {
if !para.trim().is_empty() {
let mut run = Run::new().add_text(para); let mut run = Run::new().add_text(para);
let mut paragraph = Paragraph::new(); let mut paragraph = Paragraph::new();
if let Some(lvl) = level {
// Apply heading style if specified paragraph = paragraph.style(lvl);
if let Some(level) = &level {
paragraph = paragraph.style(level);
} }
if let Some(s) = style {
// Apply custom style if specified run = s.apply_to_run(run);
if let Some(style) = &style { paragraph = s.apply_to_paragraph(paragraph);
run = style.apply_to_run(run);
paragraph = style.apply_to_paragraph(paragraph);
} }
doc = doc.add_paragraph(paragraph.add_run(run)); doc = doc.add_paragraph(paragraph.add_run(run));
} }
}
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
write_docx_file(path, doc)?;
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"Successfully added structured content to {}", "Successfully added structured content to {}",
path path
))]) ))])
} }
UpdateMode::AddImage { fn load_image_as_png(image_path: &str) -> Result<Vec<u8>, ErrorData> {
image_path, let image_data = fs::read(image_path)
width, .map_err(|e| docx_error(format!("Failed to read image file: {}", e)))?;
height,
} => {
let mut doc = if std::path::Path::new(path).exists() {
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
};
// Read the image file let extension = std::path::Path::new(image_path)
let image_data = fs::read(&image_path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read image file: {}", e)),
data: None,
})?;
// Get image format and extension
let extension = std::path::Path::new(&image_path)
.extension() .extension()
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.ok_or_else(|| ErrorData { .ok_or_else(|| docx_error("Invalid image file extension"))?
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Invalid image file extension".to_string()),
data: None,
})?
.to_lowercase(); .to_lowercase();
// Convert to PNG if not already PNG if extension == "png" {
let image_data = if extension != "png" { return Ok(image_data);
// Try to convert to PNG using the image crate }
let img = image::load_from_memory(&image_data).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR, let img = image::load_from_memory(&image_data)
message: Cow::from(format!("Failed to load image: {}", e)), .map_err(|e| docx_error(format!("Failed to load image: {}", e)))?;
data: None,
})?;
let mut png_data = Vec::new(); let mut png_data = Vec::new();
img.write_to(&mut Cursor::new(&mut png_data), ImageFormat::Png) img.write_to(&mut Cursor::new(&mut png_data), ImageFormat::Png)
.map_err(|e| ErrorData { .map_err(|e| docx_error(format!("Failed to convert image to PNG: {}", e)))?;
code: ErrorCode::INTERNAL_ERROR, Ok(png_data)
message: Cow::from(format!( }
"Failed to convert image to PNG: {}",
e fn do_add_image(
)), path: &str,
data: None, content: &str,
})?; image_path: &str,
png_data width: Option<u32>,
} else { height: Option<u32>,
image_data style: &Option<DocxStyle>,
}; ) -> Result<Vec<Content>, ErrorData> {
let mut doc = read_or_create_docx(path)?;
let image_data = load_image_as_png(image_path)?;
// Add optional caption if provided
if !content.trim().is_empty() { if !content.trim().is_empty() {
let mut caption = Paragraph::new(); let mut caption = Paragraph::new();
if let Some(style) = &style { if let Some(s) = style {
caption = style.apply_to_paragraph(caption); caption = s.apply_to_paragraph(caption);
caption = caption = caption.add_run(s.apply_to_run(Run::new().add_text(content)));
caption.add_run(style.apply_to_run(Run::new().add_text(content)));
} else { } else {
caption = caption.add_run(Run::new().add_text(content)); caption = caption.add_run(Run::new().add_text(content));
} }
doc = doc.add_paragraph(caption); doc = doc.add_paragraph(caption);
} }
// Create a paragraph with the image
let mut paragraph = Paragraph::new(); let mut paragraph = Paragraph::new();
if let Some(style) = &style { if let Some(s) = style {
paragraph = style.apply_to_paragraph(paragraph); paragraph = s.apply_to_paragraph(paragraph);
} }
// Create and add the image
let mut pic = Pic::new(&image_data); let mut pic = Pic::new(&image_data);
if let (Some(w), Some(h)) = (width, height) { if let (Some(w), Some(h)) = (width, height) {
pic = pic.size(w, h); pic = pic.size(w, h);
@@ -567,38 +425,44 @@ pub async fn docx_tool(
paragraph = paragraph.add_run(Run::new().add_image(pic)); paragraph = paragraph.add_run(Run::new().add_image(pic));
doc = doc.add_paragraph(paragraph); doc = doc.add_paragraph(paragraph);
let mut buf = Vec::new(); write_docx_file(path, doc)?;
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"Successfully added image to {}", "Successfully added image to {}",
path path
))]) ))])
} }
}
}
_ => Err(ErrorData { pub async fn docx_tool(
code: ErrorCode::INVALID_PARAMS, path: &str,
message: Cow::from(format!( operation: &str,
content: Option<&str>,
params: Option<&serde_json::Value>,
) -> Result<Vec<Content>, ErrorData> {
match operation {
"extract_text" => do_extract_text(path),
"update_doc" => {
let content = content
.ok_or_else(|| invalid_params("Content parameter required for update_doc"))?;
let (mode, style) = parse_update_mode(params)?;
match mode {
UpdateMode::Append => do_append(path, content, &style),
UpdateMode::Replace { old_text } => do_replace(path, content, &old_text, &style),
UpdateMode::InsertStructured {
level,
style: mode_style,
} => do_insert_structured(path, content, &level, &mode_style.or(style)),
UpdateMode::AddImage {
image_path,
width,
height,
} => do_add_image(path, content, &image_path, width, height, &style),
}
}
_ => Err(invalid_params(format!(
"Invalid operation: {}. Valid operations are: 'extract_text', 'update_doc'", "Invalid operation: {}. Valid operations are: 'extract_text', 'update_doc'",
operation operation
)), ))),
data: None,
}),
} }
} }