forked from arechen/EapilSkillMarket
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed.
76 lines
3.7 KiB
Python
76 lines
3.7 KiB
Python
"""Accept tracked changes into a new DOCX using an isolated LibreOffice profile."""
|
|
|
|
import argparse
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from defusedxml import ElementTree
|
|
from office.soffice import run_soffice
|
|
|
|
MACRO = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
|
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
|
Sub AcceptAllTrackedChanges()
|
|
Dim dispatcher As Object
|
|
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
|
dispatcher.executeDispatch(ThisComponent.CurrentController.Frame, ".uno:AcceptAllTrackedChanges", "", 0, Array())
|
|
ThisComponent.store()
|
|
ThisComponent.close(True)
|
|
End Sub
|
|
</script:module>'''
|
|
WORD_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
|
|
|
|
def accept_changes(input_file, output_file, timeout=120):
|
|
source = Path(input_file).resolve()
|
|
output = Path(output_file).resolve()
|
|
if source == output or not source.is_file() or source.suffix.lower() != ".docx":
|
|
return None, "Input must be an existing DOCX and output must be a different file"
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
with tempfile.TemporaryDirectory(prefix="docx-accept-", ignore_cleanup_errors=True) as temporary:
|
|
root = Path(temporary).resolve()
|
|
profile = root / "profile"
|
|
profile_arg = f"-env:UserInstallation={profile.as_uri()}"
|
|
run_soffice([profile_arg, "--headless", "--terminate_after_init"], check=True, capture_output=True, timeout=timeout)
|
|
macro_dir = profile / "user/basic/Standard"
|
|
macro_dir.mkdir(parents=True, exist_ok=True)
|
|
(macro_dir / "Module1.xba").write_text(MACRO, encoding="utf-8")
|
|
candidate = root / "accepted.docx"
|
|
shutil.copy2(source, candidate)
|
|
before = candidate.stat().st_mtime_ns
|
|
run_soffice([
|
|
profile_arg, "--headless", "--norestore",
|
|
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
|
|
str(candidate),
|
|
], check=True, capture_output=True, timeout=timeout)
|
|
if candidate.stat().st_mtime_ns == before:
|
|
return None, "LibreOffice did not save the document"
|
|
with zipfile.ZipFile(candidate) as archive:
|
|
for name in archive.namelist():
|
|
if not name.startswith("word/") or not name.endswith(".xml"):
|
|
continue
|
|
document = ElementTree.fromstring(archive.read(name))
|
|
if any(element.tag in {WORD_NS + "ins", WORD_NS + "del", WORD_NS + "moveFrom", WORD_NS + "moveTo"}
|
|
or (element.tag.startswith(WORD_NS) and element.tag.endswith("Change"))
|
|
for element in document.iter()):
|
|
return None, f"Tracked changes remain in {name}"
|
|
shutil.copy2(candidate, output)
|
|
return None, f"Accepted tracked changes: {output}"
|
|
except (OSError, subprocess.SubprocessError, zipfile.BadZipFile) as exc:
|
|
return None, f"Failed to accept tracked changes: {exc}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("input")
|
|
parser.add_argument("output")
|
|
parser.add_argument("--timeout", type=int, default=120)
|
|
args = parser.parse_args()
|
|
_, message = accept_changes(args.input, args.output, args.timeout)
|
|
print(message)
|
|
raise SystemExit(0 if message.startswith("Accepted tracked changes:") else 1)
|