docxdriver 0.2.7__tar.gz → 0.3.0__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.
- {docxdriver-0.2.7 → docxdriver-0.3.0}/Cargo.lock +5 -5
- {docxdriver-0.2.7 → docxdriver-0.3.0}/Cargo.toml +1 -1
- {docxdriver-0.2.7 → docxdriver-0.3.0}/PKG-INFO +1 -1
- docxdriver-0.3.0/crates/docxdriver-core/assets/blank.docx +0 -0
- docxdriver-0.3.0/crates/docxdriver-core/examples/plan_scaling.rs +43 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/api.rs +164 -21
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/bin/generate_typed_schema.rs +2 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/headers.rs +53 -3
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/layout.rs +65 -38
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/mod.rs +105 -56
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/typed.rs +416 -37
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/document.rs +51 -8
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/fixtures/projection-parity/parity.docx +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/fixtures/projection-parity/prior-projection.json +190 -190
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/typed_api.rs +328 -1
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/python/docxdriver/repl/prelude.py +2 -2
- {docxdriver-0.2.7 → docxdriver-0.3.0/crates/docxdriver-python}/python/docxdriver/repl/quotes.py +22 -15
- {docxdriver-0.2.7 → docxdriver-0.3.0}/python/docxdriver/repl/prelude.py +2 -2
- {docxdriver-0.2.7/crates/docxdriver-python → docxdriver-0.3.0}/python/docxdriver/repl/quotes.py +22 -15
- docxdriver-0.2.7/crates/docxdriver-core/assets/blank.docx +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/README.md +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/Cargo.toml +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/README.md +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/comments.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/equations.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/reading.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/readonly.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/README.md +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/assets.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/build.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/chrome.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/compact.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/ctx.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/input.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/mod.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/parse.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/render.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/styles.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/html/surface.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/lib.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/media.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/outcome.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/package.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/segments.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/README.md +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/fixtures/projection-parity/features.json +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/fixtures/ts-reference.docx +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/html_e2e.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/interop.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/package_layout.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/projection_parity.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/tests/spike.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/.gitignore +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/Cargo.toml +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/README.md +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/python/docxdriver/__init__.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/python/docxdriver/repl/__init__.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/python/docxdriver/repl/host.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/src/lib.rs +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-python/tests/test_host.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/pyproject.toml +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/python/docxdriver/__init__.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/python/docxdriver/repl/__init__.py +0 -0
- {docxdriver-0.2.7 → docxdriver-0.3.0}/python/docxdriver/repl/host.py +0 -0
|
@@ -416,7 +416,7 @@ dependencies = [
|
|
|
416
416
|
|
|
417
417
|
[[package]]
|
|
418
418
|
name = "docxdriver-cli"
|
|
419
|
-
version = "0.
|
|
419
|
+
version = "0.3.0"
|
|
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.
|
|
430
|
+
version = "0.3.0"
|
|
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.
|
|
447
|
+
version = "0.3.0"
|
|
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.
|
|
465
|
+
version = "0.3.0"
|
|
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.
|
|
474
|
+
version = "0.3.0"
|
|
475
475
|
dependencies = [
|
|
476
476
|
"docxdriver-core",
|
|
477
477
|
"js-sys",
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//! Plan review scaling probe: `cargo run --release -p docxdriver-core --example plan_scaling -- PARAS OPS`.
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
use std::time::Instant;
|
|
4
|
+
|
|
5
|
+
use docxdriver_core::{execute_plan, ChangeMode, EditOp, ParagraphAddress, Plan, PlanRequest, PlanResult, SourceHash};
|
|
6
|
+
|
|
7
|
+
fn package(body: &str) -> Vec<u8> {
|
|
8
|
+
let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
|
9
|
+
let o = zip::write::SimpleFileOptions::default();
|
|
10
|
+
let parts = [
|
|
11
|
+
("[Content_Types].xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"><Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/><Default Extension=\"xml\" ContentType=\"application/xml\"/><Override PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/></Types>".to_string()),
|
|
12
|
+
("_rels/.rels", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"word/document.xml\"/></Relationships>".to_string()),
|
|
13
|
+
("word/document.xml", body.to_string()),
|
|
14
|
+
("word/_rels/document.xml.rels", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"></Relationships>".to_string()),
|
|
15
|
+
];
|
|
16
|
+
for (n, c) in parts {
|
|
17
|
+
w.start_file(n, o).unwrap();
|
|
18
|
+
w.write_all(c.as_bytes()).unwrap();
|
|
19
|
+
}
|
|
20
|
+
w.finish().unwrap().into_inner()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn main() {
|
|
24
|
+
let args: Vec<usize> = std::env::args().skip(1).map(|a| a.parse().unwrap()).collect();
|
|
25
|
+
let (paras, ops) = (args.first().copied().unwrap_or(1500), args.get(1).copied().unwrap_or(300));
|
|
26
|
+
let body: String = (0..paras)
|
|
27
|
+
.map(|i| format!("<w:p w14:paraId=\"{:08X}\"><w:r><w:t>Paragraph {i} says the tenant shall pay rent within thirty days of notice.</w:t></w:r></w:p>", i + 1))
|
|
28
|
+
.collect();
|
|
29
|
+
let bytes = package(&format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\"><w:body>{body}</w:body></w:document>"));
|
|
30
|
+
let operations = (0..ops)
|
|
31
|
+
.map(|i| EditOp::ReplaceText {
|
|
32
|
+
at: ParagraphAddress::id(format!("{:08X}", (i * paras / ops) + 1)),
|
|
33
|
+
select: "thirty days".into(),
|
|
34
|
+
with: "sixty days".into(),
|
|
35
|
+
occurrence: None,
|
|
36
|
+
})
|
|
37
|
+
.collect();
|
|
38
|
+
let plan = Plan { base: SourceHash::from_bytes(&bytes), author: "bench".into(), change_mode: ChangeMode::Track, ops: operations };
|
|
39
|
+
let t = Instant::now();
|
|
40
|
+
let result = execute_plan(&bytes, &PlanRequest { plan, preview_key: None });
|
|
41
|
+
let ok = matches!(result, PlanResult::Previewed { .. });
|
|
42
|
+
println!("paras={paras} ops={ops} previewed={ok} elapsed={:?}", t.elapsed());
|
|
43
|
+
}
|
|
@@ -342,6 +342,19 @@ pub enum EditOp {
|
|
|
342
342
|
#[serde(default)]
|
|
343
343
|
occurrence: Option<u32>,
|
|
344
344
|
},
|
|
345
|
+
/// Several exact replacements inside one bounded scope: the paragraph
|
|
346
|
+
/// `at`, the unique `within` span (in `at`, or anywhere in the body when
|
|
347
|
+
/// `at` is omitted), or the whole body. Every edit resolves against the
|
|
348
|
+
/// scope as it stands when this operation starts.
|
|
349
|
+
ReviseText {
|
|
350
|
+
#[serde(default)]
|
|
351
|
+
at: Option<ParagraphAddress>,
|
|
352
|
+
#[serde(default)]
|
|
353
|
+
within: Option<String>,
|
|
354
|
+
#[serde(default)]
|
|
355
|
+
occurrence: Option<u32>,
|
|
356
|
+
edits: Vec<TextEdit>,
|
|
357
|
+
},
|
|
345
358
|
ReplaceParagraph {
|
|
346
359
|
at: ParagraphAddress,
|
|
347
360
|
with: String,
|
|
@@ -474,6 +487,20 @@ pub enum EditOp {
|
|
|
474
487
|
},
|
|
475
488
|
}
|
|
476
489
|
|
|
490
|
+
/// One replacement inside a `revise_text` scope.
|
|
491
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
|
492
|
+
#[serde(deny_unknown_fields)]
|
|
493
|
+
pub struct TextEdit {
|
|
494
|
+
pub select: String,
|
|
495
|
+
pub with: String,
|
|
496
|
+
/// 1-based match within the scope; omit when `select` is unique there.
|
|
497
|
+
#[serde(default)]
|
|
498
|
+
pub occurrence: Option<u32>,
|
|
499
|
+
/// Replace every match within the scope.
|
|
500
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
501
|
+
pub all: bool,
|
|
502
|
+
}
|
|
503
|
+
|
|
477
504
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
|
|
478
505
|
#[serde(deny_unknown_fields)]
|
|
479
506
|
pub struct CreateCommand {
|
|
@@ -970,6 +997,10 @@ fn validation_field(message: &str) -> Option<&'static str> {
|
|
|
970
997
|
"mathml"
|
|
971
998
|
} else if message.contains("equation must") {
|
|
972
999
|
"equation"
|
|
1000
|
+
} else if message.contains("edits") {
|
|
1001
|
+
"edits"
|
|
1002
|
+
} else if message.contains("within") {
|
|
1003
|
+
"within"
|
|
973
1004
|
} else if message.contains("select") {
|
|
974
1005
|
"select"
|
|
975
1006
|
} else if message.contains("occurrence") {
|
|
@@ -1030,6 +1061,38 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
|
|
|
1030
1061
|
return Err("occurrence must be positive".into());
|
|
1031
1062
|
}
|
|
1032
1063
|
}
|
|
1064
|
+
EditOp::ReviseText {
|
|
1065
|
+
within,
|
|
1066
|
+
occurrence,
|
|
1067
|
+
edits,
|
|
1068
|
+
..
|
|
1069
|
+
} => {
|
|
1070
|
+
if within.as_deref() == Some("") {
|
|
1071
|
+
return Err("within must be non-empty when given".into());
|
|
1072
|
+
}
|
|
1073
|
+
if occurrence == &Some(0) {
|
|
1074
|
+
return Err("occurrence must be positive".into());
|
|
1075
|
+
}
|
|
1076
|
+
if occurrence.is_some() && within.is_none() {
|
|
1077
|
+
return Err("occurrence picks a within match; it requires within".into());
|
|
1078
|
+
}
|
|
1079
|
+
if edits.is_empty() {
|
|
1080
|
+
return Err("edits must list at least one replacement".into());
|
|
1081
|
+
}
|
|
1082
|
+
for (index, edit) in edits.iter().enumerate() {
|
|
1083
|
+
if edit.select.is_empty() {
|
|
1084
|
+
return Err(format!("edits[{index}].select must be non-empty"));
|
|
1085
|
+
}
|
|
1086
|
+
if edit.occurrence == Some(0) {
|
|
1087
|
+
return Err(format!("edits[{index}].occurrence must be positive"));
|
|
1088
|
+
}
|
|
1089
|
+
if edit.all && edit.occurrence.is_some() {
|
|
1090
|
+
return Err(format!(
|
|
1091
|
+
"edits[{index}] sets both all and occurrence; choose one"
|
|
1092
|
+
));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1033
1096
|
EditOp::ReplaceParagraph { .. } => {}
|
|
1034
1097
|
EditOp::FormatText {
|
|
1035
1098
|
select,
|
|
@@ -1431,6 +1494,8 @@ fn fold_typed(
|
|
|
1431
1494
|
}
|
|
1432
1495
|
let mut aliases: BTreeMap<String, Value> = BTreeMap::new();
|
|
1433
1496
|
let mut reports = Vec::new();
|
|
1497
|
+
let mut affected_ids: Vec<Vec<String>> = Vec::new();
|
|
1498
|
+
let plan_seed = crate::commands::plan_seed(plan);
|
|
1434
1499
|
let mut allocated_ids = Vec::new();
|
|
1435
1500
|
let ctx = crate::commands::ExecCtx {
|
|
1436
1501
|
dry_run: !commit,
|
|
@@ -1459,33 +1524,27 @@ fn fold_typed(
|
|
|
1459
1524
|
plan.change_mode,
|
|
1460
1525
|
index,
|
|
1461
1526
|
&seed,
|
|
1462
|
-
|
|
1527
|
+
&plan_seed,
|
|
1463
1528
|
&ctx,
|
|
1464
1529
|
);
|
|
1465
|
-
let
|
|
1466
|
-
report_value = crate::commands::attach_affected_markup(&state, report_value);
|
|
1530
|
+
let report_value = outcome.result_json();
|
|
1467
1531
|
let applied = outcome.status == crate::outcome::Status::Ok;
|
|
1468
|
-
|
|
1469
|
-
.get("markup")
|
|
1470
|
-
.and_then(Value::as_str)
|
|
1471
|
-
.map(|markup| {
|
|
1472
|
-
let para_id = report_value
|
|
1473
|
-
.get("result")
|
|
1474
|
-
.and_then(|r| r.get("paraId"))
|
|
1475
|
-
.and_then(Value::as_str)
|
|
1476
|
-
.unwrap_or("");
|
|
1477
|
-
vec![serde_json::json!({"para_id": para_id, "markup": markup})]
|
|
1478
|
-
})
|
|
1479
|
-
.unwrap_or_default();
|
|
1532
|
+
affected_ids.push(crate::commands::affected_para_ids(&report_value));
|
|
1480
1533
|
reports.push(OperationReport {
|
|
1481
1534
|
index,
|
|
1482
1535
|
op: name,
|
|
1483
1536
|
outcome: if applied { "applied" } else { "rejected" }.into(),
|
|
1484
1537
|
summary: outcome.summary.clone(),
|
|
1485
|
-
affected,
|
|
1538
|
+
affected: Vec::new(),
|
|
1486
1539
|
context_truncated: false,
|
|
1487
1540
|
});
|
|
1488
1541
|
if !applied {
|
|
1542
|
+
if let Some(note) = explain_selector_drift(input, plan, &resolved, index, &affected_ids) {
|
|
1543
|
+
if let Some(report) = reports.last_mut() {
|
|
1544
|
+
report.summary.push_str(¬e);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
attach_affected(&state, &mut reports, &affected_ids);
|
|
1489
1548
|
let report = PlanReport {
|
|
1490
1549
|
completed: index,
|
|
1491
1550
|
stopped_at: Some(index),
|
|
@@ -1494,15 +1553,12 @@ fn fold_typed(
|
|
|
1494
1553
|
allocated_ids: allocated_ids.clone(),
|
|
1495
1554
|
aliases: aliases.clone().into_iter().collect(),
|
|
1496
1555
|
};
|
|
1556
|
+
let message = report.ops[index].summary.clone();
|
|
1497
1557
|
return (
|
|
1498
1558
|
report.clone(),
|
|
1499
1559
|
None,
|
|
1500
1560
|
Some((
|
|
1501
|
-
diag(
|
|
1502
|
-
"operation_rejected",
|
|
1503
|
-
outcome.summary,
|
|
1504
|
-
Some(format!("ops[{index}]")),
|
|
1505
|
-
),
|
|
1561
|
+
diag("operation_rejected", message, Some(format!("ops[{index}]"))),
|
|
1506
1562
|
report,
|
|
1507
1563
|
)),
|
|
1508
1564
|
);
|
|
@@ -1512,6 +1568,7 @@ fn fold_typed(
|
|
|
1512
1568
|
} = op
|
|
1513
1569
|
{
|
|
1514
1570
|
if aliases.contains_key(alias) {
|
|
1571
|
+
attach_affected(&state, &mut reports, &affected_ids);
|
|
1515
1572
|
let report = PlanReport {
|
|
1516
1573
|
completed: index,
|
|
1517
1574
|
stopped_at: Some(index),
|
|
@@ -1544,6 +1601,7 @@ fn fold_typed(
|
|
|
1544
1601
|
}
|
|
1545
1602
|
}
|
|
1546
1603
|
}
|
|
1604
|
+
attach_affected(&state, &mut reports, &affected_ids);
|
|
1547
1605
|
let report = PlanReport {
|
|
1548
1606
|
completed: plan.ops.len(),
|
|
1549
1607
|
stopped_at: None,
|
|
@@ -1559,6 +1617,89 @@ fn fold_typed(
|
|
|
1559
1617
|
);
|
|
1560
1618
|
(report, out.bytes, None)
|
|
1561
1619
|
}
|
|
1620
|
+
/// When a selector matched nothing but matches the same paragraph as it was
|
|
1621
|
+
/// before the plan, name the earlier operations that changed that text:
|
|
1622
|
+
/// selectors address the text left by preceding operations, not the read.
|
|
1623
|
+
fn explain_selector_drift(
|
|
1624
|
+
input: &[u8],
|
|
1625
|
+
plan: &Plan,
|
|
1626
|
+
op: &EditOp,
|
|
1627
|
+
index: usize,
|
|
1628
|
+
affected_ids: &[Vec<String>],
|
|
1629
|
+
) -> Option<String> {
|
|
1630
|
+
let (at, selects): (&ParagraphAddress, Vec<&str>) = match op {
|
|
1631
|
+
EditOp::ReplaceText { at, select, .. } | EditOp::FormatText { at, select, .. } => {
|
|
1632
|
+
(at, vec![select.as_str()])
|
|
1633
|
+
}
|
|
1634
|
+
EditOp::CommentAdd {
|
|
1635
|
+
at,
|
|
1636
|
+
select: Some(select),
|
|
1637
|
+
..
|
|
1638
|
+
} => (at, vec![select.as_str()]),
|
|
1639
|
+
EditOp::ReviseText {
|
|
1640
|
+
at: Some(at),
|
|
1641
|
+
within,
|
|
1642
|
+
edits,
|
|
1643
|
+
..
|
|
1644
|
+
} => (
|
|
1645
|
+
at,
|
|
1646
|
+
within
|
|
1647
|
+
.iter()
|
|
1648
|
+
.map(String::as_str)
|
|
1649
|
+
.chain(edits.iter().map(|e| e.select.as_str()))
|
|
1650
|
+
.collect(),
|
|
1651
|
+
),
|
|
1652
|
+
_ => return None,
|
|
1653
|
+
};
|
|
1654
|
+
if index == 0 {
|
|
1655
|
+
return None;
|
|
1656
|
+
}
|
|
1657
|
+
let ParagraphAddress::Id(id) = at else {
|
|
1658
|
+
return None;
|
|
1659
|
+
};
|
|
1660
|
+
let id = id.to_uppercase();
|
|
1661
|
+
let base = crate::commands::base_visible_text(input, &id)?;
|
|
1662
|
+
if !selects.iter().all(|select| base.contains(select)) {
|
|
1663
|
+
return None;
|
|
1664
|
+
}
|
|
1665
|
+
let earlier: Vec<String> = plan.ops[..index]
|
|
1666
|
+
.iter()
|
|
1667
|
+
.enumerate()
|
|
1668
|
+
.filter(|(i, earlier)| {
|
|
1669
|
+
matches!(earlier, EditOp::RevisionSettle { .. })
|
|
1670
|
+
|| affected_ids[*i].iter().any(|touched| touched.eq_ignore_ascii_case(&id))
|
|
1671
|
+
})
|
|
1672
|
+
.map(|(i, earlier)| format!("ops[{i}] {}", operation_name(earlier)))
|
|
1673
|
+
.collect();
|
|
1674
|
+
if earlier.is_empty() {
|
|
1675
|
+
return None;
|
|
1676
|
+
}
|
|
1677
|
+
Some(format!(
|
|
1678
|
+
"; it matches this paragraph as it was before the plan, but {} changed the text first — selectors address the text left by preceding operations (combine edits to one paragraph in a single revise_text, or settle revisions in a separate plan and re-read)",
|
|
1679
|
+
earlier.join(", ")
|
|
1680
|
+
))
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/// Fill each operation report with the rendered markup of the paragraphs it
|
|
1684
|
+
/// affected, as they stand in `state`, from one render of the document.
|
|
1685
|
+
fn attach_affected(
|
|
1686
|
+
state: &crate::commands::WorkState,
|
|
1687
|
+
reports: &mut [OperationReport],
|
|
1688
|
+
affected_ids: &[Vec<String>],
|
|
1689
|
+
) {
|
|
1690
|
+
let wanted = affected_ids.iter().flatten().cloned().collect();
|
|
1691
|
+
let markup = crate::commands::affected_markup(state, &wanted);
|
|
1692
|
+
for (report, ids) in reports.iter_mut().zip(affected_ids) {
|
|
1693
|
+
report.affected = ids
|
|
1694
|
+
.iter()
|
|
1695
|
+
.filter_map(|id| {
|
|
1696
|
+
markup
|
|
1697
|
+
.get(id)
|
|
1698
|
+
.map(|m| serde_json::json!({"para_id": id, "markup": m}))
|
|
1699
|
+
})
|
|
1700
|
+
.collect();
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1562
1703
|
fn empty_report(_plan: &Plan) -> PlanReport {
|
|
1563
1704
|
PlanReport {
|
|
1564
1705
|
completed: 0,
|
|
@@ -1574,6 +1715,7 @@ fn operation_name(op: &EditOp) -> &'static str {
|
|
|
1574
1715
|
EditOp::ReplaceEquation { .. } => "replace_equation",
|
|
1575
1716
|
EditOp::DeleteEquation { .. } => "delete_equation",
|
|
1576
1717
|
EditOp::ReplaceText { .. } => "replace_text",
|
|
1718
|
+
EditOp::ReviseText { .. } => "revise_text",
|
|
1577
1719
|
EditOp::ReplaceParagraph { .. } => "replace_paragraph",
|
|
1578
1720
|
EditOp::FormatText { .. } => "format_text",
|
|
1579
1721
|
EditOp::FormatParagraph { .. } => "format_paragraph",
|
|
@@ -1618,6 +1760,7 @@ fn resolve_typed_aliases(
|
|
|
1618
1760
|
| EditOp::FormatText { at, .. }
|
|
1619
1761
|
| EditOp::FormatParagraph { at, .. }
|
|
1620
1762
|
| EditOp::CommentAdd { at, .. } => resolve(at)?,
|
|
1763
|
+
EditOp::ReviseText { at: Some(at), .. } => resolve(at)?,
|
|
1621
1764
|
EditOp::InsertParagraph { at, .. } => {
|
|
1622
1765
|
if let InsertAnchor::Paragraph(addr) = at {
|
|
1623
1766
|
resolve(addr)?;
|
{docxdriver-0.2.7 → docxdriver-0.3.0}/crates/docxdriver-core/src/bin/generate_typed_schema.rs
RENAMED
|
@@ -96,6 +96,7 @@ export type EditOp =
|
|
|
96
96
|
| { op: 'replace_equation'; at: ParagraphAddress; mathml: string; equation?: number; display?: boolean }
|
|
97
97
|
| { op: 'delete_equation'; at: ParagraphAddress; equation?: number }
|
|
98
98
|
| { op: 'replace_text'; at: ParagraphAddress; select: string; with: string; occurrence?: number }
|
|
99
|
+
| { op: 'revise_text'; at?: ParagraphAddress; within?: string; occurrence?: number; edits: [TextEdit, ...TextEdit[]] }
|
|
99
100
|
| { op: 'replace_paragraph'; at: ParagraphAddress; with: string }
|
|
100
101
|
| { op: 'format_text'; at: ParagraphAddress; select: string; occurrence?: number; bold?: boolean; italic?: boolean; underline?: boolean; strike?: boolean; superscript?: boolean; subscript?: boolean; color?: string; font_size?: Point; clear?: string[] }
|
|
101
102
|
| { op: 'format_paragraph'; at: ParagraphAddress; style?: string; alignment?: 'left'|'center'|'right'|'justify'; indent_left?: Point; indent_right?: Point; space_before?: Point; space_after?: Point; line_spacing?: LineSpacing; clear?: string[] }
|
|
@@ -112,6 +113,7 @@ export type EditOp =
|
|
|
112
113
|
| { op: 'comment_set_status'; comment_id: string; status: 'open'|'resolved' }
|
|
113
114
|
| { op: 'comment_delete'; comment_id: string }
|
|
114
115
|
| { op: 'revision_settle'; target: string; action: 'accept'|'reject' };
|
|
116
|
+
export type TextEdit = { select: string; with: string; occurrence?: number; all?: boolean };
|
|
115
117
|
export type CreateCommand = { kind: 'create'; paragraphs?: string[]; html?: string };
|
|
116
118
|
export type ReadCommand = { kind: 'read'; read_kind?: 'document'|'document_ui'|'styles'|'comments'|'revisions'|'assets'; view?: 'markup'|'final'|'original' };
|
|
117
119
|
export type FindCommand = { kind: 'find'; query: string; ignore_case?: boolean; view?: 'markup'|'final'|'original' };
|
|
@@ -193,7 +193,23 @@ fn chrome_part_xml(scope: &str, inner: &str) -> String {
|
|
|
193
193
|
fn ensure_title_pg(xml: &mut DocxXml, sect: NodeId) {
|
|
194
194
|
if xml.child_w(sect, "titlePg").is_none() {
|
|
195
195
|
let node = xml.doc.create_element("w:titlePg");
|
|
196
|
-
|
|
196
|
+
// CT_SectPr is a sequence; titlePg precedes these.
|
|
197
|
+
const SUCCESSORS: &[&str] = &[
|
|
198
|
+
"textDirection",
|
|
199
|
+
"bidi",
|
|
200
|
+
"rtlGutter",
|
|
201
|
+
"docGrid",
|
|
202
|
+
"printerSettings",
|
|
203
|
+
"sectPrChange",
|
|
204
|
+
];
|
|
205
|
+
let successor = xml
|
|
206
|
+
.doc
|
|
207
|
+
.children(sect)
|
|
208
|
+
.find(|&child| SUCCESSORS.iter().any(|local| xml.is_w(child, local)));
|
|
209
|
+
match successor {
|
|
210
|
+
Some(next) => xml.doc.insert_before(next, node),
|
|
211
|
+
None => xml.doc.append_child(sect, node),
|
|
212
|
+
}
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
215
|
|
|
@@ -251,7 +267,14 @@ fn set_chrome_typed(
|
|
|
251
267
|
let ref_node = xml.doc.create_element(&format!("w:{ref_tag}"));
|
|
252
268
|
xml.doc.set_attribute(ref_node, "w:type", kind_str);
|
|
253
269
|
xml.doc.set_attribute(ref_node, "r:id", &rid);
|
|
254
|
-
|
|
270
|
+
// Header and footer references lead CT_SectPr: insert after the last one.
|
|
271
|
+
let successor = xml.doc.children(sect).find(|&child| {
|
|
272
|
+
!xml.is_w(child, "headerReference") && !xml.is_w(child, "footerReference")
|
|
273
|
+
});
|
|
274
|
+
match successor {
|
|
275
|
+
Some(next) => xml.doc.insert_before(next, ref_node),
|
|
276
|
+
None => xml.doc.append_child(sect, ref_node),
|
|
277
|
+
}
|
|
255
278
|
|
|
256
279
|
Ok((
|
|
257
280
|
format!("section {section} {kind_str} {scope} set"),
|
|
@@ -345,7 +368,34 @@ pub fn set_even_and_odd_headers_typed(
|
|
|
345
368
|
match (even_odd, existing) {
|
|
346
369
|
(true, None) => {
|
|
347
370
|
let node = settings.doc.create_element("w:evenAndOddHeaders");
|
|
348
|
-
|
|
371
|
+
// CT_Settings is a sequence: insert before the first later element.
|
|
372
|
+
const SUCCESSORS: &[&str] = &[
|
|
373
|
+
"bookFoldRevPrinting", "bookFoldPrinting", "bookFoldPrintingSheets",
|
|
374
|
+
"drawingGridHorizontalSpacing", "drawingGridVerticalSpacing",
|
|
375
|
+
"displayHorizontalDrawingGridEvery", "displayVerticalDrawingGridEvery",
|
|
376
|
+
"doNotUseMarginsForDrawingGridOrigin", "drawingGridHorizontalOrigin",
|
|
377
|
+
"drawingGridVerticalOrigin", "doNotShadeFormData", "noPunctuationKerning",
|
|
378
|
+
"characterSpacingControl", "printTwoOnOne", "strictFirstAndLastChars",
|
|
379
|
+
"noLineBreaksAfter", "noLineBreaksBefore", "savePreviewPicture",
|
|
380
|
+
"doNotValidateAgainstSchema", "saveInvalidXml", "ignoreMixedContent",
|
|
381
|
+
"alwaysShowPlaceholderText", "doNotDemarcateInvalidXml", "saveXmlDataOnly",
|
|
382
|
+
"useXSLTWhenSaving", "saveThroughXslt", "showXMLTags",
|
|
383
|
+
"alwaysMergeEmptyNamespace", "updateFields", "hdrShapeDefaults", "footnotePr",
|
|
384
|
+
"endnotePr", "compat", "docVars", "rsids", "mathPr", "attachedSchema",
|
|
385
|
+
"themeFontLang", "clrSchemeMapping", "doNotIncludeSubdocsInStats",
|
|
386
|
+
"doNotAutoCompressPictures", "forceUpgrade", "captions", "readModeInkLockDown",
|
|
387
|
+
"smartTagType", "schemaLibrary", "shapeDefaults", "doNotEmbedSmartTags",
|
|
388
|
+
"decimalSymbol", "listSeparator",
|
|
389
|
+
];
|
|
390
|
+
let successor = settings.doc.children(root).find(|&child| {
|
|
391
|
+
settings
|
|
392
|
+
.local_name(child)
|
|
393
|
+
.is_some_and(|local| SUCCESSORS.contains(&local))
|
|
394
|
+
});
|
|
395
|
+
match successor {
|
|
396
|
+
Some(next) => settings.doc.insert_before(next, node),
|
|
397
|
+
None => settings.doc.append_child(root, node),
|
|
398
|
+
}
|
|
349
399
|
}
|
|
350
400
|
(true, Some(node)) => {
|
|
351
401
|
if settings
|
|
@@ -32,6 +32,19 @@ fn ensure_pg_mar(sect: NodeId, xml: &mut DocxXml) -> NodeId {
|
|
|
32
32
|
return node;
|
|
33
33
|
}
|
|
34
34
|
let node = xml.doc.create_element("w:pgMar");
|
|
35
|
+
// CT_PageMar requires all seven attributes; start from Word's defaults
|
|
36
|
+
// (1in margins, 0.5in header/footer, no gutter) before applying values.
|
|
37
|
+
for (attr, twips) in [
|
|
38
|
+
("top", 1440),
|
|
39
|
+
("right", 1440),
|
|
40
|
+
("bottom", 1440),
|
|
41
|
+
("left", 1440),
|
|
42
|
+
("header", 720),
|
|
43
|
+
("footer", 720),
|
|
44
|
+
("gutter", 0),
|
|
45
|
+
] {
|
|
46
|
+
set_twips_attr(xml, node, attr, twips);
|
|
47
|
+
}
|
|
35
48
|
if let Some(successor) = xml.doc.children(sect).find(|&child| {
|
|
36
49
|
[
|
|
37
50
|
"pgBorders",
|
|
@@ -42,6 +55,7 @@ fn ensure_pg_mar(sect: NodeId, xml: &mut DocxXml) -> NodeId {
|
|
|
42
55
|
"formProtection",
|
|
43
56
|
"vAlign",
|
|
44
57
|
"noEndnote",
|
|
58
|
+
"titlePg",
|
|
45
59
|
"textDirection",
|
|
46
60
|
"bidi",
|
|
47
61
|
"rtlGutter",
|
|
@@ -125,49 +139,62 @@ pub(crate) fn ensure_ppr(xml: &mut DocxXml, paragraph: NodeId) -> NodeId {
|
|
|
125
139
|
ppr
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
/// `CT_PPr` child sequence (`CT_PPrBase` followed by `rPr`, `sectPr`,
|
|
143
|
+
/// `pPrChange`).
|
|
144
|
+
const PPR_SEQUENCE: &[&str] = &[
|
|
145
|
+
"pStyle",
|
|
146
|
+
"keepNext",
|
|
147
|
+
"keepLines",
|
|
148
|
+
"pageBreakBefore",
|
|
149
|
+
"framePr",
|
|
150
|
+
"widowControl",
|
|
151
|
+
"numPr",
|
|
152
|
+
"suppressLineNumbers",
|
|
153
|
+
"pBdr",
|
|
154
|
+
"shd",
|
|
155
|
+
"tabs",
|
|
156
|
+
"suppressAutoHyphens",
|
|
157
|
+
"kinsoku",
|
|
158
|
+
"wordWrap",
|
|
159
|
+
"overflowPunct",
|
|
160
|
+
"topLinePunct",
|
|
161
|
+
"autoSpaceDE",
|
|
162
|
+
"autoSpaceDN",
|
|
163
|
+
"bidi",
|
|
164
|
+
"adjustRightInd",
|
|
165
|
+
"snapToGrid",
|
|
166
|
+
"spacing",
|
|
167
|
+
"ind",
|
|
168
|
+
"contextualSpacing",
|
|
169
|
+
"mirrorIndents",
|
|
170
|
+
"suppressOverlap",
|
|
171
|
+
"jc",
|
|
172
|
+
"textDirection",
|
|
173
|
+
"textAlignment",
|
|
174
|
+
"textboxTightWrap",
|
|
175
|
+
"outlineLvl",
|
|
176
|
+
"divId",
|
|
177
|
+
"cnfStyle",
|
|
178
|
+
"rPr",
|
|
179
|
+
"sectPr",
|
|
180
|
+
"pPrChange",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
/// Return the `w:pPr` child `local`, creating it at its schema sequence
|
|
184
|
+
/// position.
|
|
128
185
|
pub(crate) fn ensure_ppr_child(xml: &mut DocxXml, ppr: NodeId, local: &str) -> NodeId {
|
|
129
186
|
if let Some(node) = xml.child_w(ppr, local) {
|
|
130
187
|
return node;
|
|
131
188
|
}
|
|
132
|
-
let successors: &[&str] = match local {
|
|
133
|
-
"pStyle" => &["*"],
|
|
134
|
-
"spacing" => &[
|
|
135
|
-
"ind",
|
|
136
|
-
"contextualSpacing",
|
|
137
|
-
"mirrorIndents",
|
|
138
|
-
"suppressOverlap",
|
|
139
|
-
"jc",
|
|
140
|
-
"textDirection",
|
|
141
|
-
"textAlignment",
|
|
142
|
-
"textboxTightWrap",
|
|
143
|
-
"outlineLvl",
|
|
144
|
-
],
|
|
145
|
-
"ind" => &[
|
|
146
|
-
"contextualSpacing",
|
|
147
|
-
"mirrorIndents",
|
|
148
|
-
"suppressOverlap",
|
|
149
|
-
"jc",
|
|
150
|
-
"textDirection",
|
|
151
|
-
"textAlignment",
|
|
152
|
-
"textboxTightWrap",
|
|
153
|
-
"outlineLvl",
|
|
154
|
-
],
|
|
155
|
-
"jc" => &[
|
|
156
|
-
"textDirection",
|
|
157
|
-
"textAlignment",
|
|
158
|
-
"textboxTightWrap",
|
|
159
|
-
"outlineLvl",
|
|
160
|
-
],
|
|
161
|
-
_ => &[],
|
|
162
|
-
};
|
|
163
189
|
let node = xml.doc.create_element(&format!("w:{local}"));
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
let successors = PPR_SEQUENCE
|
|
191
|
+
.iter()
|
|
192
|
+
.position(|name| *name == local)
|
|
193
|
+
.map_or(&[][..], |index| &PPR_SEQUENCE[index + 1..]);
|
|
194
|
+
let successor = xml
|
|
195
|
+
.doc
|
|
196
|
+
.children(ppr)
|
|
197
|
+
.find(|&child| successors.iter().any(|name| xml.is_w(child, name)));
|
|
171
198
|
if let Some(next) = successor {
|
|
172
199
|
xml.doc.insert_before(next, node);
|
|
173
200
|
} else {
|