docxdriver 0.4.2__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 (62) hide show
  1. {docxdriver-0.4.2 → docxdriver-0.4.4}/Cargo.lock +6 -6
  2. {docxdriver-0.4.2 → docxdriver-0.4.4}/Cargo.toml +1 -1
  3. {docxdriver-0.4.2 → docxdriver-0.4.4}/PKG-INFO +2 -1
  4. {docxdriver-0.4.2/crates/docxdriver-python → docxdriver-0.4.4}/README.md +1 -0
  5. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/api.rs +167 -4
  6. docxdriver-0.4.4/crates/docxdriver-core/src/commands/bookmarks.rs +879 -0
  7. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/mod.rs +23 -2
  8. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/table_structure.rs +47 -2
  9. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/typed.rs +79 -6
  10. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/facade.rs +7 -1
  11. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/segments.rs +5 -0
  12. {docxdriver-0.4.2 → docxdriver-0.4.4/crates/docxdriver-python}/README.md +1 -0
  13. {docxdriver-0.4.2 → docxdriver-0.4.4/crates/docxdriver-python}/python/docxdriver/document.py +35 -4
  14. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/python/docxdriver/plan.py +60 -0
  15. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/src/lib.rs +6 -0
  16. docxdriver-0.4.4/crates/docxdriver-python/tests/test_document.py +126 -0
  17. {docxdriver-0.4.2/crates/docxdriver-python → docxdriver-0.4.4}/python/docxdriver/document.py +35 -4
  18. {docxdriver-0.4.2 → docxdriver-0.4.4}/python/docxdriver/plan.py +60 -0
  19. docxdriver-0.4.2/crates/docxdriver-python/tests/test_document.py +0 -75
  20. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/Cargo.toml +0 -0
  21. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/LICENSE +0 -0
  22. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/README.md +0 -0
  23. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/assets/blank.docx +0 -0
  24. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/comments.rs +0 -0
  25. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/equations.rs +0 -0
  26. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/headers.rs +0 -0
  27. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/layout.rs +0 -0
  28. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/lists.rs +0 -0
  29. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/reading.rs +0 -0
  30. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/readonly.rs +0 -0
  31. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
  32. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/sections.rs +0 -0
  33. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/commands/tables.rs +0 -0
  34. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/document.rs +0 -0
  35. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/assets.rs +0 -0
  36. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/build.rs +0 -0
  37. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/chrome.rs +0 -0
  38. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/compact.rs +0 -0
  39. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/ctx.rs +0 -0
  40. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/input.rs +0 -0
  41. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
  42. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/mod.rs +0 -0
  43. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
  44. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
  45. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/parse.rs +0 -0
  46. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/render.rs +0 -0
  47. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/styles.rs +0 -0
  48. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/html/surface.rs +0 -0
  49. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/mod.rs +0 -0
  50. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/schema.rs +0 -0
  51. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/tree.rs +0 -0
  52. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/integrity/wml-model.json +0 -0
  53. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/lib.rs +0 -0
  54. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/media.rs +0 -0
  55. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/outcome.rs +0 -0
  56. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-core/src/package.rs +0 -0
  57. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/.gitignore +0 -0
  58. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/Cargo.toml +0 -0
  59. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/python/docxdriver/__init__.py +0 -0
  60. {docxdriver-0.4.2 → docxdriver-0.4.4}/crates/docxdriver-python/tests/test_plan.py +0 -0
  61. {docxdriver-0.4.2 → docxdriver-0.4.4}/pyproject.toml +0 -0
  62. {docxdriver-0.4.2 → docxdriver-0.4.4}/python/docxdriver/__init__.py +0 -0
@@ -416,7 +416,7 @@ dependencies = [
416
416
 
417
417
  [[package]]
418
418
  name = "docxdriver-cli"
419
- version = "0.4.2"
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.2"
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.2"
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.2"
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.2"
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.2"
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.2"
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.2
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
@@ -29,6 +29,7 @@ from docxdriver.plan import Plan, ReplaceText
29
29
  doc = Document(Path("contract.docx").read_bytes())
30
30
  doc.markup() # body with the ids plans address
31
31
  doc.comments(); doc.revisions(); doc.styles(); doc.assets()
32
+ doc.bookmarks() # same as doc.read(kind="bookmarks")
32
33
  doc.find("thirty")
33
34
  review = doc.review(Plan(author="Reviewer", ops=[
34
35
  ReplaceText(at="0F537164", select="thirty", with_="sixty"),
@@ -20,6 +20,7 @@ from docxdriver.plan import Plan, ReplaceText
20
20
  doc = Document(Path("contract.docx").read_bytes())
21
21
  doc.markup() # body with the ids plans address
22
22
  doc.comments(); doc.revisions(); doc.styles(); doc.assets()
23
+ doc.bookmarks() # same as doc.read(kind="bookmarks")
23
24
  doc.find("thirty")
24
25
  review = doc.review(Plan(author="Reviewer", ops=[
25
26
  ReplaceText(at="0F537164", select="thirty", with_="sixty"),
@@ -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 },
@@ -748,6 +761,35 @@ pub enum EditOp {
748
761
  CommentDelete {
749
762
  comment_id: String,
750
763
  },
764
+ /// Bookmark the paragraph at `at`: its whole content, or the `select`
765
+ /// span. Applied directly in both change modes (Word does not track
766
+ /// bookmarks). Body paragraphs only.
767
+ BookmarkAdd {
768
+ at: ParagraphAddress,
769
+ name: String,
770
+ #[serde(default)]
771
+ select: Option<String>,
772
+ #[serde(default)]
773
+ occurrence: Option<u32>,
774
+ },
775
+ /// Rename a body bookmark; `new_name` follows the add rules.
776
+ BookmarkRename {
777
+ name: String,
778
+ new_name: String,
779
+ },
780
+ /// Re-anchor a body bookmark, keeping its name and `w:id`.
781
+ BookmarkMove {
782
+ name: String,
783
+ at: ParagraphAddress,
784
+ #[serde(default)]
785
+ select: Option<String>,
786
+ #[serde(default)]
787
+ occurrence: Option<u32>,
788
+ },
789
+ /// Remove a body bookmark's start and end markers; the text stays.
790
+ BookmarkDelete {
791
+ name: String,
792
+ },
751
793
  RevisionSettle {
752
794
  target: RevisionTarget,
753
795
  action: RevisionAction,
@@ -1260,7 +1302,11 @@ fn validate_plan(mut p: Plan) -> Result<Plan, Diagnostic> {
1260
1302
  }
1261
1303
 
1262
1304
  fn validation_field(message: &str) -> Option<&'static str> {
1263
- let field = if message.contains("MathML") {
1305
+ let field = if message.starts_with("new_name ") {
1306
+ "new_name"
1307
+ } else if message.starts_with("name ") {
1308
+ "name"
1309
+ } else if message.contains("MathML") {
1264
1310
  "mathml"
1265
1311
  } else if message.contains("equation must") {
1266
1312
  "equation"
@@ -1361,9 +1407,31 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1361
1407
  }
1362
1408
  }
1363
1409
  EditOp::EditTable { at, action } => crate::commands::table_structure::validate(at, action)?,
1364
- EditOp::InsertTable { rows, columns, alias, .. } => {
1410
+ EditOp::InsertTable { rows, columns, cells, alias, .. } => {
1365
1411
  if *rows == 0 || *columns == 0 || *rows > 100 || *columns > 100 { return Err("insert_table dimensions must be 1..100".into()); }
1366
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
+ }
1367
1435
  }
1368
1436
  EditOp::FormatTable { at, format } => crate::commands::tables::validate_table(at, format)?,
1369
1437
  EditOp::FormatRow { at, format } => crate::commands::tables::validate_row(at, format)?,
@@ -1566,6 +1634,31 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1566
1634
  return Err("set_page_margins requires a margin".into());
1567
1635
  }
1568
1636
  }
1637
+ EditOp::BookmarkAdd {
1638
+ name,
1639
+ select,
1640
+ occurrence,
1641
+ ..
1642
+ } => {
1643
+ crate::commands::bookmarks::validate_new_name(name, "name")?;
1644
+ validate_bookmark_select(select, occurrence)?;
1645
+ }
1646
+ EditOp::BookmarkMove {
1647
+ name,
1648
+ select,
1649
+ occurrence,
1650
+ ..
1651
+ } => {
1652
+ crate::commands::bookmarks::validate_existing_name(name)?;
1653
+ validate_bookmark_select(select, occurrence)?;
1654
+ }
1655
+ EditOp::BookmarkRename { name, new_name } => {
1656
+ crate::commands::bookmarks::validate_existing_name(name)?;
1657
+ crate::commands::bookmarks::validate_new_name(new_name, "new_name")?;
1658
+ }
1659
+ EditOp::BookmarkDelete { name } => {
1660
+ crate::commands::bookmarks::validate_existing_name(name)?;
1661
+ }
1569
1662
  EditOp::CommentAdd {
1570
1663
  text,
1571
1664
  occurrence,
@@ -1620,6 +1713,18 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
1620
1713
  }
1621
1714
  Ok(())
1622
1715
  }
1716
+ fn validate_bookmark_select(select: &Option<String>, occurrence: &Option<u32>) -> Result<(), String> {
1717
+ if select.as_deref() == Some("") {
1718
+ return Err("select must be non-empty; omit it to bookmark the whole paragraph".into());
1719
+ }
1720
+ if occurrence == &Some(0) {
1721
+ return Err("occurrence must be positive".into());
1722
+ }
1723
+ if occurrence.is_some() && select.is_none() {
1724
+ return Err("occurrence requires select".into());
1725
+ }
1726
+ Ok(())
1727
+ }
1623
1728
  fn diag(code: impl Into<String>, message: impl Into<String>, path: Option<String>) -> Diagnostic {
1624
1729
  Diagnostic {
1625
1730
  code: code.into(),
@@ -1932,6 +2037,37 @@ fn fold_typed(
1932
2037
  if matches!(op, EditOp::InsertTable { .. } | EditOp::EditTable { .. }) {
1933
2038
  aliases.insert(alias.clone(), Value::String(format!("${alias}")));
1934
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
+ }
1935
2071
  if let Some(id) = outcome
1936
2072
  .result
1937
2073
  .as_ref()
@@ -1942,6 +2078,12 @@ fn fold_typed(
1942
2078
  aliases.insert(alias.clone(), id);
1943
2079
  }
1944
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
+ }
1945
2087
  }
1946
2088
  attach_affected(&mut state, &mut reports, &affected_ids);
1947
2089
  let report = PlanReport {
@@ -1992,6 +2134,16 @@ fn explain_selector_drift(
1992
2134
  at,
1993
2135
  select: Some(select),
1994
2136
  ..
2137
+ }
2138
+ | EditOp::BookmarkAdd {
2139
+ at,
2140
+ select: Some(select),
2141
+ ..
2142
+ }
2143
+ | EditOp::BookmarkMove {
2144
+ at,
2145
+ select: Some(select),
2146
+ ..
1995
2147
  } => (at, vec![select.as_str()]),
1996
2148
  EditOp::ReviseText {
1997
2149
  at: Some(at),
@@ -2098,6 +2250,10 @@ fn operation_name(op: &EditOp) -> &'static str {
2098
2250
  EditOp::CommentReply { .. } => "comment_reply",
2099
2251
  EditOp::CommentSetStatus { .. } => "comment_set_status",
2100
2252
  EditOp::CommentDelete { .. } => "comment_delete",
2253
+ EditOp::BookmarkAdd { .. } => "bookmark_add",
2254
+ EditOp::BookmarkRename { .. } => "bookmark_rename",
2255
+ EditOp::BookmarkMove { .. } => "bookmark_move",
2256
+ EditOp::BookmarkDelete { .. } => "bookmark_delete",
2101
2257
  EditOp::RevisionSettle { .. } => "revision_settle",
2102
2258
  }
2103
2259
  }
@@ -2128,7 +2284,9 @@ fn resolve_typed_aliases(
2128
2284
  | EditOp::InsertTable { at, .. }
2129
2285
  | EditOp::SetSectionBoundary { at, .. }
2130
2286
  | EditOp::FormatParagraph { at, .. }
2131
- | EditOp::CommentAdd { at, .. } => resolve(at)?,
2287
+ | EditOp::CommentAdd { at, .. }
2288
+ | EditOp::BookmarkAdd { at, .. }
2289
+ | EditOp::BookmarkMove { at, .. } => resolve(at)?,
2132
2290
  EditOp::ReviseText { at: Some(at), .. } => resolve(at)?,
2133
2291
  EditOp::InsertParagraph { at, .. } => {
2134
2292
  if let InsertAnchor::Paragraph(addr) = at {
@@ -2478,6 +2636,11 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
2478
2636
  "revisions",
2479
2637
  source,
2480
2638
  ),
2639
+ "bookmarks" => typed_listing(
2640
+ crate::commands::bookmarks::list_bookmarks(&state.package, &state.xml, source),
2641
+ "bookmarks",
2642
+ source,
2643
+ ),
2481
2644
  other => crate::outcome::Outcome::error(format!("unknown read kind: {other}")),
2482
2645
  }
2483
2646
  }