docxdriver 0.3.2__tar.gz → 0.3.3__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 (78) hide show
  1. {docxdriver-0.3.2 → docxdriver-0.3.3}/Cargo.lock +5 -5
  2. {docxdriver-0.3.2 → docxdriver-0.3.3}/Cargo.toml +1 -1
  3. {docxdriver-0.3.2 → docxdriver-0.3.3}/PKG-INFO +1 -1
  4. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/api.rs +38 -14
  5. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/bin/generate_typed_schema.rs +3 -2
  6. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/layout.rs +11 -4
  7. docxdriver-0.3.3/crates/docxdriver-core/src/commands/lists.rs +531 -0
  8. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/mod.rs +38 -9
  9. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/readonly.rs +1 -3
  10. docxdriver-0.3.3/crates/docxdriver-core/src/commands/sections.rs +183 -0
  11. docxdriver-0.3.3/crates/docxdriver-core/src/commands/table_structure.rs +563 -0
  12. docxdriver-0.3.3/crates/docxdriver-core/src/commands/tables.rs +925 -0
  13. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/document.rs +109 -16
  14. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/build.rs +41 -22
  15. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/ctx.rs +4 -1
  16. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/input.rs +5 -1
  17. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/render.rs +6 -5
  18. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/formatting_fidelity.rs +308 -0
  19. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/html_e2e.rs +86 -0
  20. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/typed_api.rs +54 -0
  21. docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/prelude.py +3 -0
  22. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/tests/test_host.py +50 -1
  23. docxdriver-0.3.3/python/docxdriver/repl/prelude.py +3 -0
  24. docxdriver-0.3.2/crates/docxdriver-core/src/commands/lists.rs +0 -141
  25. docxdriver-0.3.2/crates/docxdriver-core/src/commands/table_structure.rs +0 -100
  26. docxdriver-0.3.2/crates/docxdriver-core/src/commands/tables.rs +0 -128
  27. docxdriver-0.3.2/crates/docxdriver-python/python/docxdriver/repl/prelude.py +0 -3
  28. docxdriver-0.3.2/python/docxdriver/repl/prelude.py +0 -3
  29. {docxdriver-0.3.2 → docxdriver-0.3.3}/README.md +0 -0
  30. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/Cargo.toml +0 -0
  31. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/README.md +0 -0
  32. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/assets/blank.docx +0 -0
  33. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/examples/list_scaling.rs +0 -0
  34. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/examples/plan_scaling.rs +0 -0
  35. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/comments.rs +0 -0
  36. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/equations.rs +0 -0
  37. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/headers.rs +0 -0
  38. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/reading.rs +0 -0
  39. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
  40. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/commands/typed.rs +0 -0
  41. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/README.md +0 -0
  42. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/assets.rs +0 -0
  43. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/chrome.rs +0 -0
  44. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/compact.rs +0 -0
  45. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
  46. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/mod.rs +0 -0
  47. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
  48. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
  49. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/parse.rs +0 -0
  50. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/styles.rs +0 -0
  51. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/html/surface.rs +0 -0
  52. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/lib.rs +0 -0
  53. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/media.rs +0 -0
  54. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/outcome.rs +0 -0
  55. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/package.rs +0 -0
  56. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/src/segments.rs +0 -0
  57. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/README.md +0 -0
  58. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/fixtures/projection-parity/features.json +0 -0
  59. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/fixtures/projection-parity/parity.docx +0 -0
  60. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/fixtures/projection-parity/prior-projection.json +0 -0
  61. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/fixtures/ts-reference.docx +0 -0
  62. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/interop.rs +0 -0
  63. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/package_layout.rs +0 -0
  64. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/projection_parity.rs +0 -0
  65. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-core/tests/spike.rs +0 -0
  66. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/.gitignore +0 -0
  67. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/Cargo.toml +0 -0
  68. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/README.md +0 -0
  69. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/python/docxdriver/__init__.py +0 -0
  70. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/python/docxdriver/repl/__init__.py +0 -0
  71. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/python/docxdriver/repl/host.py +0 -0
  72. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/python/docxdriver/repl/quotes.py +0 -0
  73. {docxdriver-0.3.2 → docxdriver-0.3.3}/crates/docxdriver-python/src/lib.rs +0 -0
  74. {docxdriver-0.3.2 → docxdriver-0.3.3}/pyproject.toml +0 -0
  75. {docxdriver-0.3.2 → docxdriver-0.3.3}/python/docxdriver/__init__.py +0 -0
  76. {docxdriver-0.3.2 → docxdriver-0.3.3}/python/docxdriver/repl/__init__.py +0 -0
  77. {docxdriver-0.3.2 → docxdriver-0.3.3}/python/docxdriver/repl/host.py +0 -0
  78. {docxdriver-0.3.2 → docxdriver-0.3.3}/python/docxdriver/repl/quotes.py +0 -0
@@ -416,7 +416,7 @@ dependencies = [
416
416
 
417
417
  [[package]]
418
418
  name = "docxdriver-cli"
419
- version = "0.3.2"
419
+ version = "0.3.3"
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.3.2"
430
+ version = "0.3.3"
431
431
  dependencies = [
432
432
  "hex",
433
433
  "latex2mathml",
@@ -444,7 +444,7 @@ dependencies = [
444
444
 
445
445
  [[package]]
446
446
  name = "docxdriver-mcp"
447
- version = "0.3.2"
447
+ version = "0.3.3"
448
448
  dependencies = [
449
449
  "axum",
450
450
  "cap-std",
@@ -462,7 +462,7 @@ dependencies = [
462
462
 
463
463
  [[package]]
464
464
  name = "docxdriver-python"
465
- version = "0.3.2"
465
+ version = "0.3.3"
466
466
  dependencies = [
467
467
  "docxdriver-core",
468
468
  "pyo3",
@@ -471,7 +471,7 @@ dependencies = [
471
471
 
472
472
  [[package]]
473
473
  name = "docxdriver-wasm"
474
- version = "0.3.2"
474
+ version = "0.3.3"
475
475
  dependencies = [
476
476
  "docxdriver-core",
477
477
  "js-sys",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/docxdriver-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.2"
6
+ version = "0.3.3"
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.3.2
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for the native Rust DOCX engine
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -159,9 +159,9 @@ pub struct RowFormat {
159
159
  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
160
160
  #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
161
161
  pub enum TableEdit {
162
- InsertRows { row: u32, position: InsertPosition, count: u32 },
162
+ InsertRows { row: u32, position: InsertPosition, count: u32, #[serde(rename = "as", default)] alias: Option<String> },
163
163
  DeleteRows { row: u32, count: u32 },
164
- InsertColumns { column: u32, position: InsertPosition, count: u32 },
164
+ InsertColumns { column: u32, position: InsertPosition, count: u32, #[serde(rename = "as", default)] alias: Option<String> },
165
165
  DeleteColumns { column: u32, count: u32 },
166
166
  MergeCells { row: u32, column: u32, row_span: u32, column_span: u32 },
167
167
  SplitCell { row: u32, column: u32 },
@@ -665,7 +665,9 @@ pub enum EditOp {
665
665
  gutter: Option<Points>,
666
666
  },
667
667
  /// Selective section-property edits. Dimensions are points; omitted fields preserve source.
668
+ SetSectionBoundary { at: ParagraphAddress, #[serde(default)] remove: bool, #[serde(default)] break_type: Option<String> },
668
669
  SetSectionLayout {
670
+ #[serde(default)] section_handle: Option<String>,
669
671
  #[serde(default)]
670
672
  section: Option<u32>,
671
673
  #[serde(default)]
@@ -1480,7 +1482,7 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1480
1482
  return Err("levels requires a new list with 1..9 definitions covering the selected level".into());
1481
1483
  }
1482
1484
  for (i, def) in defs.iter().enumerate() {
1483
- if !["bullet","decimal","lowerLetter","upperLetter","lowerRoman","upperRoman"].contains(&def.format.as_str()) || def.start == Some(0) || def.label.is_empty() || def.label.len() > 255 || def.restart_after.is_some_and(|v| v as usize >= i) {
1485
+ if !["bullet","decimal","lowerLetter","upperLetter","lowerRoman","upperRoman"].contains(&def.format.as_str()) || def.start.is_some_and(|v|v == 0 || v > i32::MAX as u32) || def.label.is_empty() || def.label.len() > 255 || def.restart_after.is_some_and(|v| v as usize >= i) {
1484
1486
  return Err("invalid list level format, label, start or restart ancestor".into());
1485
1487
  }
1486
1488
  if [def.indent_left,def.indent_right,def.first_line_indent].into_iter().flatten().any(|v| v.0.unsigned_abs() > 2_000_000) { return Err("list indentation is out of range".into()); }
@@ -1491,8 +1493,8 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1491
1493
  if def.marker_font.as_ref().is_some_and(|v| v.is_empty()) || def.marker_color.as_ref().is_some_and(|v| v.len() != 6 || !v.bytes().all(|c| c.is_ascii_hexdigit())) { return Err("invalid list marker font or six-digit RGB color".into()); }
1492
1494
  }
1493
1495
  }
1494
- if at.is_empty() || *level > 8 || *start == Some(0) || *num_id == Some(0) {
1495
- return Err("set_list requires paragraphs, level 0..8 and positive IDs/starts".into());
1496
+ if at.is_empty() || *level > 8 || start.is_some_and(|v|v == 0 || v > i32::MAX as u32) || num_id.is_some_and(|v|v == 0 || v > i32::MAX as u32) {
1497
+ return Err("set_list requires paragraphs, level 0..8 and IDs/starts in 1..2147483647".into());
1496
1498
  }
1497
1499
  if *remove && (num_id.is_some() || format.is_some() || start.is_some() || *level != 0) {
1498
1500
  return Err("set_list remove cannot be combined with list settings".into());
@@ -1516,7 +1518,13 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1516
1518
  return Err("at must not be empty".into());
1517
1519
  }
1518
1520
  }
1519
- EditOp::SetSectionLayout { section, page_size, width, height, orientation, columns, column_spacing, column_widths, column_separator, break_type, page_number_format, page_number_start, page_number_continue } => {
1521
+ EditOp::SetSectionBoundary {remove,break_type,..} => {
1522
+ if *remove && break_type.is_some(){return Err("removing a boundary cannot set break_type".into());}
1523
+ if break_type.as_ref().is_some_and(|v|!matches!(v.as_str(),"nextPage"|"continuous"|"oddPage"|"evenPage"|"nextColumn")){return Err("unsupported section break_type".into());}
1524
+ }
1525
+ EditOp::SetSectionLayout { section, section_handle, page_size, width, height, orientation, columns, column_spacing, column_widths, column_separator, break_type, page_number_format, page_number_start, page_number_continue } => {
1526
+ if section.is_some() && section_handle.is_some() {return Err("section and section_handle are mutually exclusive".into());}
1527
+ if section_handle.as_ref().is_some_and(|v|!v.starts_with("section:")){return Err("section_handle must use an inventory handle".into());}
1520
1528
  if section == &Some(0) { return Err("section must be positive".into()); }
1521
1529
  if let Some(size) = page_size { if !matches!(size.as_str(), "letter" | "legal" | "a4" | "a3" | "a5") { return Err("page_size must be letter/legal/a4/a3/a5".into()); } }
1522
1530
  if page_size.is_some() && (width.is_some() || height.is_some()) { return Err("page_size conflicts with width/height".into()); }
@@ -1524,11 +1532,13 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1524
1532
  if [width.as_ref(), height.as_ref()].into_iter().flatten().any(|value| value.0 <= 0 || value.0 > 31680) { return Err("page dimensions must be >0 and <=1584 points".into()); }
1525
1533
  if orientation.as_ref().is_some_and(|v| !matches!(v.as_str(), "portrait" | "landscape")) { return Err("orientation must be portrait/landscape".into()); }
1526
1534
  if columns.is_some_and(|v| v == 0 || v > 45) { return Err("columns must be between 1 and 45".into()); }
1527
- if column_widths.as_ref().is_some_and(|v| v.is_empty() || v.len() > 45 || v.iter().any(|p| p.0 <= 0)) { return Err("column_widths requires 1..45 positive widths".into()); }
1535
+ if column_spacing.is_some_and(|p| !(0..=31680).contains(&p.0)) { return Err("column_spacing must be between 0 and 1584 points".into()); }
1536
+ if column_widths.as_ref().is_some_and(|v| v.is_empty() || v.len() > 45 || v.iter().any(|p| p.0 <= 0 || p.0 > 31680)) { return Err("column_widths requires 1..45 positive widths of at most 1584 points".into()); }
1528
1537
  if let (Some(count), Some(widths)) = (columns.as_ref(), column_widths.as_ref()) { if *count as usize != widths.len() { return Err("columns must match column_widths length".into()); } }
1529
1538
  if break_type.as_ref().is_some_and(|v| !matches!(v.as_str(), "nextPage" | "continuous" | "oddPage" | "evenPage" | "nextColumn")) { return Err("unsupported section break_type".into()); }
1530
1539
  if page_number_format.as_ref().is_some_and(|v| !matches!(v.as_str(), "decimal" | "upperRoman" | "lowerRoman" | "upperLetter" | "lowerLetter")) { return Err("unsupported page_number_format".into()); }
1531
- if page_number_start == &Some(0) { return Err("page_number_start must be positive".into()); }
1540
+ if page_number_continue == &Some(false) { return Err("page_number_continue only accepts true; use page_number_start to restart".into()); }
1541
+ if page_number_start.is_some_and(|value| value == 0 || value > i32::MAX as u32) { return Err("page_number_start must be between 1 and 2147483647".into()); }
1532
1542
  if page_number_start.is_some() && page_number_continue == &Some(true) { return Err("page_number_start conflicts with page_number_continue".into()); }
1533
1543
  if page_size.is_none() && width.is_none() && orientation.is_none() && columns.is_none() && column_spacing.is_none() && column_widths.is_none() && column_separator.is_none() && break_type.is_none() && page_number_format.is_none() && page_number_start.is_none() && page_number_continue.is_none() { return Err("set_section_layout requires a property".into()); }
1534
1544
  }
@@ -1833,6 +1843,7 @@ fn fold_typed(
1833
1843
  let resolved = match resolve_typed_aliases(op, &aliases) {
1834
1844
  Ok(value) => value,
1835
1845
  Err(error) => {
1846
+ attach_affected(&mut state, &mut reports, &affected_ids);
1836
1847
  let report = PlanReport {
1837
1848
  completed: index,
1838
1849
  stopped_at: Some(index),
@@ -1872,7 +1883,7 @@ fn fold_typed(
1872
1883
  report.summary.push_str(&note);
1873
1884
  }
1874
1885
  }
1875
- attach_affected(&state, &mut reports, &affected_ids);
1886
+ attach_affected(&mut state, &mut reports, &affected_ids);
1876
1887
  let report = PlanReport {
1877
1888
  completed: index,
1878
1889
  stopped_at: Some(index),
@@ -1892,10 +1903,11 @@ fn fold_typed(
1892
1903
  );
1893
1904
  }
1894
1905
  if let EditOp::InsertParagraph { alias: Some(alias), .. }
1895
- | EditOp::InsertTable { alias: Some(alias), .. } = op
1906
+ | EditOp::InsertTable { alias: Some(alias), .. }
1907
+ | EditOp::EditTable { action: TableEdit::InsertRows { alias: Some(alias), .. } | TableEdit::InsertColumns { alias: Some(alias), .. }, .. } = op
1896
1908
  {
1897
1909
  if aliases.contains_key(alias) {
1898
- attach_affected(&state, &mut reports, &affected_ids);
1910
+ attach_affected(&mut state, &mut reports, &affected_ids);
1899
1911
  let report = PlanReport {
1900
1912
  completed: index,
1901
1913
  stopped_at: Some(index),
@@ -1917,7 +1929,7 @@ fn fold_typed(
1917
1929
  )),
1918
1930
  );
1919
1931
  }
1920
- if let EditOp::InsertTable { .. } = op {
1932
+ if matches!(op, EditOp::InsertTable { .. } | EditOp::EditTable { .. }) {
1921
1933
  aliases.insert(alias.clone(), Value::String(format!("${alias}")));
1922
1934
  }
1923
1935
  if let Some(id) = outcome
@@ -1931,7 +1943,7 @@ fn fold_typed(
1931
1943
  }
1932
1944
  }
1933
1945
  }
1934
- attach_affected(&state, &mut reports, &affected_ids);
1946
+ attach_affected(&mut state, &mut reports, &affected_ids);
1935
1947
  let report = PlanReport {
1936
1948
  completed: plan.ops.len(),
1937
1949
  stopped_at: None,
@@ -2013,10 +2025,11 @@ fn explain_selector_drift(
2013
2025
  /// Fill each operation report with the rendered markup of the paragraphs it
2014
2026
  /// affected, as they stand in `state`, from one render of the document.
2015
2027
  fn attach_affected(
2016
- state: &crate::commands::WorkState,
2028
+ state: &mut crate::commands::WorkState,
2017
2029
  reports: &mut [OperationReport],
2018
2030
  affected_ids: &[Vec<String>],
2019
2031
  ) {
2032
+ state.flush_lists();
2020
2033
  let wanted = affected_ids.iter().flatten().cloned().collect();
2021
2034
  let markup = crate::commands::affected_markup(state, &wanted);
2022
2035
  for (report, ids) in reports.iter_mut().zip(affected_ids) {
@@ -2058,6 +2071,7 @@ fn operation_name(op: &EditOp) -> &'static str {
2058
2071
  EditOp::InsertParagraph { .. } => "insert_paragraph",
2059
2072
  EditOp::DeleteParagraphs { .. } => "delete_paragraphs",
2060
2073
  EditOp::SetPageMargins { .. } => "set_page_margins",
2074
+ EditOp::SetSectionBoundary { .. } => "set_section_boundary",
2061
2075
  EditOp::SetSectionLayout { .. } => "set_section_layout",
2062
2076
  EditOp::SetFirstPageMode { .. } => "set_first_page_mode",
2063
2077
  EditOp::SetEvenAndOddHeaders { .. } => "set_even_and_odd_headers",
@@ -2097,6 +2111,7 @@ fn resolve_typed_aliases(
2097
2111
  | EditOp::ReplaceParagraph { at, .. }
2098
2112
  | EditOp::FormatText { at, .. }
2099
2113
  | EditOp::InsertTable { at, .. }
2114
+ | EditOp::SetSectionBoundary { at, .. }
2100
2115
  | EditOp::FormatParagraph { at, .. }
2101
2116
  | EditOp::CommentAdd { at, .. } => resolve(at)?,
2102
2117
  EditOp::ReviseText { at: Some(at), .. } => resolve(at)?,
@@ -2110,6 +2125,12 @@ fn resolve_typed_aliases(
2110
2125
  resolve(address)?;
2111
2126
  }
2112
2127
  }
2128
+ EditOp::SetSectionLayout { section_handle:Some(handle),.. } => {
2129
+ if let Some(alias)=handle.strip_prefix("section:$") {
2130
+ let Some(Value::String(id))=aliases.get(alias) else { return Err(diag("unknown_alias",format!("unknown section paragraph alias ${alias}"),None)); };
2131
+ *handle=format!("section:{id}");
2132
+ }
2133
+ }
2113
2134
  _ => {}
2114
2135
  }
2115
2136
  Ok(resolved)
@@ -2305,6 +2326,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
2305
2326
  map.insert("markup".into(), serde_json::json!(markup));
2306
2327
  map.insert("css".into(), serde_json::json!(css));
2307
2328
  map.insert("tables".into(), crate::commands::tables::inventory(&state.xml));
2329
+ map.insert("sections".into(), crate::commands::sections::inventory(&state.package,&state.xml,&paragraph_ids,view==Some("original")));
2308
2330
  map.insert("styles".into(), serde_json::json!(crate::html::styles::Styles::load(&state.package).catalog()));
2309
2331
  if !equations.is_empty() {
2310
2332
  map.insert("equations".into(), serde_json::json!(equations));
@@ -2379,6 +2401,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
2379
2401
  }).collect();
2380
2402
  let mut result = result;
2381
2403
  if let Some(map) = result.result.as_mut().and_then(Value::as_object_mut) {
2404
+ map.insert("sections".into(),crate::commands::sections::inventory(&state.package,&state.xml,&paragraph_ids,view==Some("original")));
2382
2405
  if let Some(Value::Array(paragraphs)) = map.get("paragraphs") {
2383
2406
  let equations: Vec<Value> = paragraphs
2384
2407
  .iter()
@@ -2409,6 +2432,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
2409
2432
  serde_json::json!(crate::html::assets::manifest(&state.package, false)),
2410
2433
  );
2411
2434
  map.insert("tables".into(), crate::commands::tables::inventory(&state.xml));
2435
+ map.insert("sections".into(), crate::commands::sections::inventory(&state.package,&state.xml,&paragraph_ids,view==Some("original")));
2412
2436
  map.insert("html".into(), Value::String(rendered.html));
2413
2437
  }
2414
2438
  if let Some(map) = result.result.as_mut().and_then(Value::as_object_mut) {
@@ -104,7 +104,7 @@ export type CellPadding = { top?: Point; right?: Point; bottom?: Point; left?: P
104
104
  export type TableFormat = { width?: TableWidth; column_widths?: Point[]; layout?: 'autofit'|'fixed'; alignment?: 'left'|'center'|'right'; borders?: TableBorders; cell_spacing?: Point; padding?: CellPadding; style?: string; clear?: string[] };
105
105
  export type RowFormat = { height?: Point; height_rule?: 'auto'|'at_least'|'exact'; repeating_header?: boolean; allow_split?: boolean; clear?: string[] };
106
106
  export type CellFormat = { width?: TableWidth; borders?: TableBorders; fill?: string; padding?: CellPadding; vertical_alignment?: 'top'|'center'|'bottom'; clear?: string[] };
107
- export type TableEdit = { kind: 'insert_rows'; row: number; position: InsertPosition; count: number } | { kind: 'delete_rows'; row: number; count: number } | { kind: 'insert_columns'; column: number; position: InsertPosition; count: number } | { kind: 'delete_columns'; column: number; count: number } | { kind: 'merge_cells'; row: number; column: number; row_span: number; column_span: number } | { kind: 'split_cell'; row: number; column: number } | { kind: 'delete' };
107
+ export type TableEdit = { kind: 'insert_rows'; row: number; position: InsertPosition; count: number; as?: string } | { kind: 'delete_rows'; row: number; count: number } | { kind: 'insert_columns'; column: number; position: InsertPosition; count: number; as?: string } | { kind: 'delete_columns'; column: number; count: number } | { kind: 'merge_cells'; row: number; column: number; row_span: number; column_span: number } | { kind: 'split_cell'; row: number; column: number } | { kind: 'delete' };
108
108
  export type EditOp =
109
109
  | { op: 'replace_equation'; at: ParagraphAddress; mathml: string; equation?: number; display?: boolean }
110
110
  | { op: 'delete_equation'; at: ParagraphAddress; equation?: number }
@@ -122,7 +122,8 @@ export type EditOp =
122
122
  | { op: 'edit_table'; at: string; action: TableEdit }
123
123
  | { op: 'insert_table'; at: ParagraphAddress; position: InsertPosition; rows: number; columns: number; as?: string }
124
124
  | { op: 'set_page_margins'; section?: number; top?: Point; right?: Point; bottom?: Point; left?: Point; header?: Point; footer?: Point; gutter?: Point }
125
- | { op: 'set_section_layout'; section?: number; page_size?: 'letter'|'legal'|'a4'|'a3'|'a5'; width?: Point; height?: Point; orientation?: 'portrait'|'landscape'; columns?: number; column_spacing?: Point; column_widths?: Point[]; column_separator?: boolean; break_type?: 'nextPage'|'continuous'|'oddPage'|'evenPage'|'nextColumn'; page_number_format?: 'decimal'|'upperRoman'|'lowerRoman'|'upperLetter'|'lowerLetter'; page_number_start?: number; page_number_continue?: boolean }
125
+ | { op: 'set_section_boundary'; at: ParagraphAddress; remove?: boolean; break_type?: 'nextPage'|'continuous'|'oddPage'|'evenPage'|'nextColumn' }
126
+ | { op: 'set_section_layout'; section?: number; section_handle?: string; page_size?: 'letter'|'legal'|'a4'|'a3'|'a5'; width?: Point; height?: Point; orientation?: 'portrait'|'landscape'; columns?: number; column_spacing?: Point; column_widths?: Point[]; column_separator?: boolean; break_type?: 'nextPage'|'continuous'|'oddPage'|'evenPage'|'nextColumn'; page_number_format?: 'decimal'|'upperRoman'|'lowerRoman'|'upperLetter'|'lowerLetter'; page_number_start?: number; page_number_continue?: boolean }
126
127
  | { op: 'set_first_page_mode'; section?: number; enabled: boolean }
127
128
  | { op: 'set_even_and_odd_headers'; even_and_odd: boolean }
128
129
  | { op: 'set_header'; section?: number; kind?: ChromeKind; with: string }
@@ -346,19 +346,22 @@ pub fn set_section_layout(
346
346
  if page_size.is_some() || width.is_some() || orientation.is_some() {
347
347
  if (orient == "landscape" && w < h) || (orient == "portrait" && w > h) { std::mem::swap(&mut w, &mut h); }
348
348
  }
349
- let area = w - dimension("pgMar", "left", 1440) - dimension("pgMar", "right", 1440) - dimension("pgMar", "gutter", 0);
349
+ let area = w.saturating_sub(dimension("pgMar", "left", 1440)).saturating_sub(dimension("pgMar", "right", 1440)).saturating_sub(dimension("pgMar", "gutter", 0));
350
350
  let cols = xml.child_w(sect, "cols");
351
351
  let old_widths: Vec<i64> = cols.map(|n| xml.doc.children(n).filter(|&n| xml.is_w(n, "col")).filter_map(|n| xml.attr(n, "w").and_then(|v| v.parse().ok())).collect()).unwrap_or_default();
352
352
  let count = columns.map(|v| v as i64).or_else(|| column_widths.as_ref().map(|v| v.len() as i64)).unwrap_or_else(|| dimension("cols", "num", if old_widths.is_empty() { 1 } else { old_widths.len() as i64 }));
353
353
  let gap = column_spacing.map(|p| p.0).unwrap_or_else(|| dimension("cols", "space", 720));
354
+ let original_gaps = cols.map(|n| xml.doc.children(n).filter(|&n| xml.is_w(n, "col")).collect::<Vec<_>>()).unwrap_or_default();
355
+ let inherited_gap_total = original_gaps.iter().take(original_gaps.len().saturating_sub(1)).fold(0_i64, |total, &n| total.saturating_add(xml.attr(n, "space").and_then(|v| v.parse::<i64>().ok()).unwrap_or(gap)));
356
+ let gap_total = if column_spacing.is_none() && column_widths.is_none() && columns.is_none() && !old_widths.is_empty() { inherited_gap_total } else { gap.saturating_mul(count.saturating_sub(1)) };
354
357
  let unequal: Vec<i64> = column_widths.as_ref().map(|v| v.iter().map(|p| p.0).collect()).unwrap_or_else(|| old_widths.clone());
355
358
  let changes_columns = columns.is_some() || column_spacing.is_some() || column_widths.is_some() || column_separator.is_some();
356
359
  if changes_columns || width.is_some() || orientation.is_some() || page_size.is_some() {
357
- if area <= 0 || gap.saturating_mul(count - 1) >= area { return Err(Outcome::blocked("columns do not fit the section text area")); }
358
- if !unequal.is_empty() && columns.is_none() && (unequal.iter().sum::<i64>() + gap * (count - 1) > area) {
360
+ if area <= 0 || gap_total >= area { return Err(Outcome::blocked("columns do not fit the section text area")); }
361
+ if !unequal.is_empty() && columns.is_none() && (unequal.iter().fold(0_i64, |total, width| total.saturating_add(*width)).saturating_add(gap_total) > area) {
359
362
  return Err(Outcome::blocked("column widths and spacing exceed the section text area"));
360
363
  }
361
- if let Some(widths) = column_widths { if widths.iter().map(|p| p.0).sum::<i64>() + gap * (count - 1) > area { return Err(Outcome::blocked("column widths and spacing exceed the section text area")); } }
364
+ if let Some(widths) = column_widths { if widths.iter().fold(0_i64, |total, p| total.saturating_add(p.0)).saturating_add(gap_total) > area { return Err(Outcome::blocked("column widths and spacing exceed the section text area")); } }
362
365
  }
363
366
  if tracked { snapshot_section_properties(xml, sect, author, ctx)?; }
364
367
  if page_size.is_some() || width.is_some() || orientation.is_some() {
@@ -375,6 +378,10 @@ pub fn set_section_layout(
375
378
  for child in xml.doc.children(node).collect::<Vec<_>>() { if xml.is_w(child, "col") { xml.doc.remove_node(child); } }
376
379
  set_attr(xml, node, "w:equalWidth", if column_widths.is_some() { "0" } else { "1" });
377
380
  }
381
+ if column_spacing.is_some() && column_widths.is_none() && columns.is_none() {
382
+ let children: Vec<_> = xml.doc.children(node).filter(|&n| xml.is_w(n, "col")).collect();
383
+ for (index, &col) in children.iter().enumerate() { set_twips_attr(xml, col, "space", if index + 1 == children.len() { 0 } else { gap }); }
384
+ }
378
385
  if let Some(widths) = column_widths {
379
386
  for (index, width) in widths.iter().enumerate() {
380
387
  let col = xml.doc.create_element("w:col");