docxdriver 0.4.3__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {docxdriver-0.4.3 → docxdriver-0.4.4}/Cargo.lock +6 -6
  2. {docxdriver-0.4.3 → docxdriver-0.4.4}/Cargo.toml +1 -1
  3. {docxdriver-0.4.3 → docxdriver-0.4.4}/PKG-INFO +1 -1
  4. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/api.rs +74 -2
  5. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/mod.rs +3 -2
  6. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/table_structure.rs +47 -2
  7. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/typed.rs +58 -0
  8. {docxdriver-0.4.3 → docxdriver-0.4.4/crates/docxdriver-python}/python/docxdriver/plan.py +3 -0
  9. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/tests/test_document.py +23 -1
  10. {docxdriver-0.4.3/crates/docxdriver-python → docxdriver-0.4.4}/python/docxdriver/plan.py +3 -0
  11. {docxdriver-0.4.3 → docxdriver-0.4.4}/README.md +0 -0
  12. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/Cargo.toml +0 -0
  13. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/LICENSE +0 -0
  14. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/README.md +0 -0
  15. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/assets/blank.docx +0 -0
  16. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/bookmarks.rs +0 -0
  17. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/comments.rs +0 -0
  18. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/equations.rs +0 -0
  19. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/headers.rs +0 -0
  20. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/layout.rs +0 -0
  21. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/lists.rs +0 -0
  22. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/reading.rs +0 -0
  23. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/readonly.rs +0 -0
  24. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
  25. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/sections.rs +0 -0
  26. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/tables.rs +0 -0
  27. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/document.rs +0 -0
  28. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/facade.rs +0 -0
  29. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/assets.rs +0 -0
  30. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/build.rs +0 -0
  31. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/chrome.rs +0 -0
  32. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/compact.rs +0 -0
  33. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/ctx.rs +0 -0
  34. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/input.rs +0 -0
  35. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
  36. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/mod.rs +0 -0
  37. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
  38. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
  39. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/parse.rs +0 -0
  40. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/render.rs +0 -0
  41. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/styles.rs +0 -0
  42. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/surface.rs +0 -0
  43. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/mod.rs +0 -0
  44. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/schema.rs +0 -0
  45. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/tree.rs +0 -0
  46. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/wml-model.json +0 -0
  47. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/lib.rs +0 -0
  48. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/media.rs +0 -0
  49. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/outcome.rs +0 -0
  50. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/package.rs +0 -0
  51. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-core/src/segments.rs +0 -0
  52. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/.gitignore +0 -0
  53. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/Cargo.toml +0 -0
  54. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/README.md +0 -0
  55. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/python/docxdriver/__init__.py +0 -0
  56. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/python/docxdriver/document.py +0 -0
  57. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/src/lib.rs +0 -0
  58. {docxdriver-0.4.3 → docxdriver-0.4.4}/crates/docxdriver-python/tests/test_plan.py +0 -0
  59. {docxdriver-0.4.3 → docxdriver-0.4.4}/pyproject.toml +0 -0
  60. {docxdriver-0.4.3 → docxdriver-0.4.4}/python/docxdriver/__init__.py +0 -0
  61. {docxdriver-0.4.3 → docxdriver-0.4.4}/python/docxdriver/document.py +0 -0
@@ -416,7 +416,7 @@ dependencies = [
416
416
 
417
417
  [[package]]
418
418
  name = "docxdriver-cli"
419
- version = "0.4.3"
419
+ version = "0.4.4"
420
420
  dependencies = [
421
421
  "clap",
422
422
  "docxdriver-core",
@@ -427,7 +427,7 @@ dependencies = [
427
427
 
428
428
  [[package]]
429
429
  name = "docxdriver-core"
430
- version = "0.4.3"
430
+ version = "0.4.4"
431
431
  dependencies = [
432
432
  "hex",
433
433
  "latex2mathml",
@@ -445,7 +445,7 @@ dependencies = [
445
445
 
446
446
  [[package]]
447
447
  name = "docxdriver-mcp"
448
- version = "0.4.3"
448
+ version = "0.4.4"
449
449
  dependencies = [
450
450
  "axum",
451
451
  "cap-std",
@@ -463,14 +463,14 @@ dependencies = [
463
463
 
464
464
  [[package]]
465
465
  name = "docxdriver-monty-repl"
466
- version = "0.4.3"
466
+ version = "0.4.4"
467
467
  dependencies = [
468
468
  "docxdriver-core",
469
469
  ]
470
470
 
471
471
  [[package]]
472
472
  name = "docxdriver-python"
473
- version = "0.4.3"
473
+ version = "0.4.4"
474
474
  dependencies = [
475
475
  "docxdriver-core",
476
476
  "pyo3",
@@ -480,7 +480,7 @@ dependencies = [
480
480
 
481
481
  [[package]]
482
482
  name = "docxdriver-wasm"
483
- version = "0.4.3"
483
+ version = "0.4.4"
484
484
  dependencies = [
485
485
  "docxdriver-core",
486
486
  "js-sys",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/docxdriver-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.4.3"
6
+ version = "0.4.4"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/jhnoel/docxdriver"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docxdriver
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Python SDK for the native Rust DOCX engine
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -564,7 +564,20 @@ pub enum EditOp {
564
564
  with: String,
565
565
  },
566
566
  EditTable { at: String, action: TableEdit },
567
- InsertTable { at: ParagraphAddress, position: InsertPosition, rows: u32, columns: u32, #[serde(rename = "as", default)] alias: Option<String> },
567
+ /// A `rows` × `columns` grid of new cells. `cells` fills them row by
568
+ /// row with the inline content `insert_paragraph`'s `with` accepts;
569
+ /// missing trailing rows and cells stay empty. With `as`, cell row N
570
+ /// column M is the paragraph alias `$<as>_rNcM` for later operations.
571
+ InsertTable {
572
+ at: ParagraphAddress,
573
+ position: InsertPosition,
574
+ rows: u32,
575
+ columns: u32,
576
+ #[serde(default)]
577
+ cells: Option<Vec<Vec<String>>>,
578
+ #[serde(rename = "as", default)]
579
+ alias: Option<String>,
580
+ },
568
581
  FormatTable { at: String, format: TableFormat },
569
582
  FormatRow { at: String, format: RowFormat },
570
583
  FormatCell { at: String, format: CellFormat },
@@ -1394,9 +1407,31 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1394
1407
  }
1395
1408
  }
1396
1409
  EditOp::EditTable { at, action } => crate::commands::table_structure::validate(at, action)?,
1397
- EditOp::InsertTable { rows, columns, alias, .. } => {
1410
+ EditOp::InsertTable { rows, columns, cells, alias, .. } => {
1398
1411
  if *rows == 0 || *columns == 0 || *rows > 100 || *columns > 100 { return Err("insert_table dimensions must be 1..100".into()); }
1399
1412
  if alias.as_ref().is_some_and(|a| !valid_alias(a)) { return Err("invalid alias".into()); }
1413
+ let cells = cells.as_deref().unwrap_or_default();
1414
+ if cells.len() > *rows as usize {
1415
+ let extra = cells.len() - *rows as usize;
1416
+ return Err(format!(
1417
+ "cells has {} rows but the table has {rows}: remove cells[{rows}..] (row {}{})",
1418
+ cells.len(),
1419
+ *rows + 1,
1420
+ if extra > 1 { format!(" to row {}", cells.len()) } else { String::new() },
1421
+ ));
1422
+ }
1423
+ for (index, row) in cells.iter().enumerate() {
1424
+ if row.len() > *columns as usize {
1425
+ let extra = row.len() - *columns as usize;
1426
+ return Err(format!(
1427
+ "cells[{index}] (row {}) has {} cells but the table has {columns} columns: remove cells[{index}][{columns}..] (column {}{})",
1428
+ index + 1,
1429
+ row.len(),
1430
+ *columns + 1,
1431
+ if extra > 1 { format!(" to column {}", row.len()) } else { String::new() },
1432
+ ));
1433
+ }
1434
+ }
1400
1435
  }
1401
1436
  EditOp::FormatTable { at, format } => crate::commands::tables::validate_table(at, format)?,
1402
1437
  EditOp::FormatRow { at, format } => crate::commands::tables::validate_row(at, format)?,
@@ -2002,6 +2037,37 @@ fn fold_typed(
2002
2037
  if matches!(op, EditOp::InsertTable { .. } | EditOp::EditTable { .. }) {
2003
2038
  aliases.insert(alias.clone(), Value::String(format!("${alias}")));
2004
2039
  }
2040
+ if let Some(rows) = outcome.result.as_ref().and_then(|r| r.get("cells")).and_then(Value::as_array) {
2041
+ let cells = rows.iter().enumerate().flat_map(|(r, row)| {
2042
+ row.as_array().into_iter().flatten().enumerate().map(move |(c, id)| (format!("{alias}_r{}c{}", r + 1, c + 1), id.clone()))
2043
+ });
2044
+ for (name, id) in cells {
2045
+ if aliases.contains_key(&name) {
2046
+ attach_affected(&mut state, &mut reports, &affected_ids);
2047
+ let report = PlanReport {
2048
+ completed: index,
2049
+ stopped_at: Some(index),
2050
+ ops: reports,
2051
+ repaired_ids: repaired_ids.clone(),
2052
+ allocated_ids: allocated_ids.clone(),
2053
+ aliases: aliases.clone().into_iter().collect(),
2054
+ };
2055
+ return (
2056
+ report.clone(),
2057
+ None,
2058
+ Some((
2059
+ diag(
2060
+ "duplicate_alias",
2061
+ format!("cell alias {name:?} of table alias {alias:?} is already defined"),
2062
+ Some(format!("ops[{index}].as")),
2063
+ ),
2064
+ report,
2065
+ )),
2066
+ );
2067
+ }
2068
+ aliases.insert(name, id);
2069
+ }
2070
+ }
2005
2071
  if let Some(id) = outcome
2006
2072
  .result
2007
2073
  .as_ref()
@@ -2012,6 +2078,12 @@ fn fold_typed(
2012
2078
  aliases.insert(alias.clone(), id);
2013
2079
  }
2014
2080
  }
2081
+ if let EditOp::InsertTable { .. } = op {
2082
+ let rows = outcome.result.as_ref().and_then(|r| r.get("cells")).and_then(Value::as_array);
2083
+ for id in rows.into_iter().flatten().filter_map(Value::as_array).flatten() {
2084
+ allocated_ids.push(id.as_str().unwrap_or_default().into());
2085
+ }
2086
+ }
2015
2087
  }
2016
2088
  attach_affected(&mut state, &mut reports, &affected_ids);
2017
2089
  let report = PlanReport {
@@ -328,9 +328,10 @@ fn run_typed_edit_inner(
328
328
  EditOp::EditTable { at, action } => {
329
329
  mutate(state, |pkg, xml, _source| table_structure::edit(pkg, xml, at, action, tracked))
330
330
  }
331
- EditOp::InsertTable { at, position, rows, columns, .. } => {
331
+ EditOp::InsertTable { at, position, rows, columns, cells, .. } => {
332
332
  let at = para_id!(at);
333
- mutate(state, |pkg, xml, source| table_structure::insert(pkg, xml, source, &at, *position, *rows, *columns, tracked))
333
+ let cells = cells.as_deref().unwrap_or_default();
334
+ mutate(state, |pkg, xml, source| table_structure::insert(pkg, xml, source, &at, *position, *rows, *columns, cells, tracked, &plan_seed, index))
334
335
  }
335
336
  EditOp::FormatTable { at, format } => mutate(state, |pkg, xml, _| tables::format_table(Some(pkg), xml, at, format, tracked, author, ctx)),
336
337
  EditOp::FormatRow { at, format } => mutate(state, |_, xml, _| tables::format_row(xml, at, format, tracked, author, ctx)),
@@ -497,7 +497,10 @@ pub fn insert(
497
497
  position: InsertPosition,
498
498
  row_count: u32,
499
499
  column_count: u32,
500
+ cell_content: &[Vec<String>],
500
501
  tracked: bool,
502
+ plan_seed: &[u8; 32],
503
+ op_index: usize,
501
504
  ) -> Result<(String, Value), Outcome> {
502
505
  if tracked {
503
506
  return Err(error(
@@ -535,10 +538,12 @@ pub fn insert(
535
538
  x.doc.append_child(g, c);
536
539
  }
537
540
  x.doc.append_child(t, g);
541
+ let mut cell_paragraphs = Vec::new();
538
542
  for _ in 0..row_count {
539
543
  let r = x.doc.create_element("w:tr");
540
544
  for _ in 0..column_count {
541
545
  let c = empty_cell(x);
546
+ cell_paragraphs.push(x.doc.children(c).next().unwrap());
542
547
  x.doc.append_child(r, c);
543
548
  }
544
549
  x.doc.append_child(t, r);
@@ -547,6 +552,38 @@ pub fn insert(
547
552
  InsertPosition::Before => x.doc.insert_before(p, t),
548
553
  InsertPosition::After => x.doc.insert_after(p, t),
549
554
  };
555
+ // Every cell paragraph gets a durable id now, so later operations in the
556
+ // same plan can address the cells (as `$alias_rNcM` or by id).
557
+ let ids = super::typed::allocate_table_cell_para_ids(
558
+ x,
559
+ source,
560
+ plan_seed,
561
+ op_index,
562
+ cell_paragraphs.len(),
563
+ )
564
+ .map_err(|message| Outcome::error(message))?;
565
+ for (paragraph, id) in cell_paragraphs.iter().zip(&ids) {
566
+ x.doc.set_attribute(*paragraph, "w14:paraId", id);
567
+ }
568
+ x.ensure_w14_declared();
569
+ let mut filled = 0;
570
+ for (row, contents) in cell_content.iter().enumerate() {
571
+ for (column, with) in contents.iter().enumerate() {
572
+ if with.is_empty() {
573
+ continue;
574
+ }
575
+ let paragraph = cell_paragraphs[row * column_count as usize + column];
576
+ super::typed::fill_paragraph_direct(package, x, paragraph, with).map_err(|mut outcome| {
577
+ outcome.summary = format!("cells[{row}][{column}]: {}", outcome.summary);
578
+ outcome
579
+ })?;
580
+ filled += 1;
581
+ }
582
+ }
583
+ let cell_ids = ids
584
+ .chunks(column_count as usize)
585
+ .map(|row| json!(row))
586
+ .collect::<Vec<_>>();
550
587
  if x.is_w(parent, "tc") && x.doc.children(parent).last() == Some(t) {
551
588
  let end = x.doc.create_element("w:p");
552
589
  x.doc.append_child(parent, end);
@@ -556,8 +593,16 @@ pub fn insert(
556
593
  .position(|n| *n == t)
557
594
  .unwrap()
558
595
  + 1;
596
+ let summary = if cell_content.is_empty() {
597
+ format!("inserted table table:{ordinal}")
598
+ } else {
599
+ format!(
600
+ "inserted table table:{ordinal} with {filled} filled cell{}",
601
+ if filled == 1 { "" } else { "s" }
602
+ )
603
+ };
559
604
  Ok((
560
- "inserted table".into(),
561
- json!({"table":format!("table:{ordinal}"),"tracked":false}),
605
+ summary,
606
+ json!({"table":format!("table:{ordinal}"),"tracked":false,"cells":cell_ids,"filled":filled}),
562
607
  ))
563
608
  }
@@ -1541,6 +1541,19 @@ fn insert_plain_pieces(
1541
1541
  }
1542
1542
  cursor = xml.doc.next_sibling(node);
1543
1543
  }
1544
+ // A part without hyperlinks may not declare the relationships prefix.
1545
+ if let Some(root) = xml.doc.root_element() {
1546
+ let declared = xml.doc.attributes(root).iter().any(|a| {
1547
+ a.name == "xmlns:r" || (a.name == "r" && a.value.contains("relationships"))
1548
+ });
1549
+ if !declared {
1550
+ xml.doc.set_attribute(
1551
+ root,
1552
+ "xmlns:r",
1553
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
1554
+ );
1555
+ }
1556
+ }
1544
1557
  let link = xml.doc.create_element("w:hyperlink");
1545
1558
  xml.doc.set_attribute(link, "r:id", &rid);
1546
1559
  attach_created(xml, paragraph, before, link);
@@ -2546,6 +2559,51 @@ fn allocate_insert_para_id_typed(
2546
2559
  allocator.allocate(&seed).map(format_para_id)
2547
2560
  }
2548
2561
 
2562
+ /// Unused `w14:paraId`s for the cell paragraphs of a table inserted by
2563
+ /// operation `op_index`, one per cell in row-major order. Cell `n` (from 1)
2564
+ /// seeds probe slot `n`; slot 0 is the one `insert_paragraph` uses.
2565
+ pub(crate) fn allocate_table_cell_para_ids(
2566
+ xml: &DocxXml,
2567
+ source_hash: Option<[u8; 32]>,
2568
+ plan_seed: &[u8; 32],
2569
+ op_index: usize,
2570
+ count: usize,
2571
+ ) -> Result<Vec<String>, String> {
2572
+ let mut allocator = ParaIdAllocator::new();
2573
+ for node in xml.all_paragraphs() {
2574
+ if let Some(value) = xml.para_id(node).filter(|v| is_valid_para_id(v)) {
2575
+ allocator.reserve(u32::from_str_radix(value, 16).unwrap_or(0));
2576
+ }
2577
+ }
2578
+ let mut prefix = Vec::with_capacity(96);
2579
+ prefix.extend_from_slice(&source_hash.unwrap_or([0; 32]));
2580
+ prefix.push(b'|');
2581
+ prefix.extend_from_slice(plan_seed);
2582
+ prefix.push(b'|');
2583
+ prefix.extend_from_slice(&(op_index as u64).to_le_bytes());
2584
+ prefix.push(b'|');
2585
+ (1..=count as u64)
2586
+ .map(|slot| {
2587
+ let mut seed = prefix.clone();
2588
+ seed.extend_from_slice(&slot.to_le_bytes());
2589
+ allocator.allocate(&seed).map(format_para_id)
2590
+ })
2591
+ .collect()
2592
+ }
2593
+
2594
+ /// Fill an empty paragraph with `with`, the inline content `insert_paragraph`
2595
+ /// accepts, as direct (untracked) runs.
2596
+ pub(crate) fn fill_paragraph_direct(
2597
+ package: &mut Package,
2598
+ xml: &mut DocxXml,
2599
+ paragraph: NodeId,
2600
+ with: &str,
2601
+ ) -> Result<(), Outcome> {
2602
+ let (pieces, link_href) = parse_with(with)?;
2603
+ insert_plain_pieces(xml, package, paragraph, None, &pieces, link_href.as_deref())
2604
+ .map_err(Outcome::error)
2605
+ }
2606
+
2549
2607
  /// `insert_paragraph`: insert one paragraph before or after an existing
2550
2608
  /// paragraph, assigning an unused `w14:paraId` in the same part. With
2551
2609
  /// tracking enabled the engine records both the new content and the new
@@ -319,6 +319,7 @@ class InsertTable:
319
319
  position: str
320
320
  rows: int
321
321
  as_: str | None = None
322
+ cells: list | None = None
322
323
  def to_dict(self):
323
324
  value = {"op": "insert_table"}
324
325
  if self.at is not None:
@@ -331,6 +332,8 @@ class InsertTable:
331
332
  value["rows"] = self.rows
332
333
  if self.as_ is not None:
333
334
  value["as"] = self.as_
335
+ if self.cells is not None:
336
+ value["cells"] = self.cells
334
337
  return value
335
338
 
336
339
  @dataclass
@@ -4,7 +4,10 @@ import re
4
4
  import unittest
5
5
 
6
6
  from docxdriver import Document, create
7
- from docxdriver.plan import BookmarkAdd, BookmarkDelete, BookmarkMove, BookmarkRename, CommentAdd, Plan, ReplaceText
7
+ from docxdriver.plan import (
8
+ BookmarkAdd, BookmarkDelete, BookmarkMove, BookmarkRename, CommentAdd, InsertTable, Plan, ReplaceParagraph,
9
+ ReplaceText,
10
+ )
8
11
 
9
12
 
10
13
  def first_paragraph(markup):
@@ -75,6 +78,25 @@ class DocumentTests(unittest.TestCase):
75
78
  with self.assertRaisesRegex(ValueError, 'unknown read kind'):
76
79
  doc.read(kind='marks')
77
80
 
81
+ def test_insert_table_fills_cells_in_one_plan(self):
82
+ doc = create(paragraphs=['Team'])
83
+ at = first_paragraph(doc.markup())
84
+ review = doc.review(Plan(author='T', change_mode='direct', ops=[
85
+ InsertTable(at=at, position='after', rows=2, columns=2, as_='t',
86
+ cells=[['<b>Name</b>', '<b>Role</b>'], ['Ada']]),
87
+ ReplaceParagraph(at='$t_r2c2', with_='Analyst'),
88
+ ]))
89
+ self.assertEqual(review.report['ops'][0]['summary'], 'inserted table table:1 with 3 filled cells')
90
+ self.assertEqual(len(review.report['allocated_ids']), 4)
91
+ self.assertEqual(review.report['aliases']['t_r1c1'], review.report['allocated_ids'][0])
92
+ markup = doc.commit(review.key).markup()
93
+ for text in ('<b>Name</b>', '<b>Role</b>', '>Ada<', '>Analyst<'):
94
+ self.assertIn(text, markup)
95
+ with self.assertRaisesRegex(ValueError, r'remove cells\[0\]\[2\.\.\]'):
96
+ doc.review(Plan(author='T', change_mode='direct', ops=[
97
+ InsertTable(at=at, position='after', rows=1, columns=2, cells=[['a', 'b', 'c']]),
98
+ ]))
99
+
78
100
  def test_commit_needs_a_reviewed_key(self):
79
101
  doc = create(paragraphs=['Alpha'])
80
102
  with self.assertRaises(ValueError):
@@ -319,6 +319,7 @@ class InsertTable:
319
319
  position: str
320
320
  rows: int
321
321
  as_: str | None = None
322
+ cells: list | None = None
322
323
  def to_dict(self):
323
324
  value = {"op": "insert_table"}
324
325
  if self.at is not None:
@@ -331,6 +332,8 @@ class InsertTable:
331
332
  value["rows"] = self.rows
332
333
  if self.as_ is not None:
333
334
  value["as"] = self.as_
335
+ if self.cells is not None:
336
+ value["cells"] = self.cells
334
337
  return value
335
338
 
336
339
  @dataclass
File without changes
File without changes