@kortix/sandbox 0.4.1
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.
- package/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- package/services/opencode-web/run +21 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Accept all tracked changes in a DOCX file using LibreOffice.
|
|
2
|
+
|
|
3
|
+
Requires LibreOffice (soffice) to be installed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from office.soffice import get_soffice_env
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
|
|
17
|
+
MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
|
|
18
|
+
|
|
19
|
+
ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
|
21
|
+
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
|
22
|
+
Sub AcceptAllTrackedChanges()
|
|
23
|
+
Dim document As Object
|
|
24
|
+
Dim dispatcher As Object
|
|
25
|
+
|
|
26
|
+
document = ThisComponent.CurrentController.Frame
|
|
27
|
+
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
|
28
|
+
|
|
29
|
+
dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
|
|
30
|
+
ThisComponent.store()
|
|
31
|
+
ThisComponent.close(True)
|
|
32
|
+
End Sub
|
|
33
|
+
</script:module>"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def accept_changes(input_file: str, output_file: str) -> tuple[None, str]:
|
|
37
|
+
input_path = Path(input_file)
|
|
38
|
+
output_path = Path(output_file)
|
|
39
|
+
|
|
40
|
+
if not input_path.exists():
|
|
41
|
+
return None, f"Error: Input file not found: {input_file}"
|
|
42
|
+
if not input_path.suffix.lower() == ".docx":
|
|
43
|
+
return None, f"Error: Input file is not a DOCX file: {input_file}"
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
shutil.copy2(input_path, output_path)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return None, f"Error: Failed to copy input file: {e}"
|
|
50
|
+
|
|
51
|
+
if not _setup_libreoffice_macro():
|
|
52
|
+
return None, "Error: Failed to setup LibreOffice macro"
|
|
53
|
+
|
|
54
|
+
cmd = [
|
|
55
|
+
"soffice", "--headless",
|
|
56
|
+
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
|
|
57
|
+
"--norestore",
|
|
58
|
+
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
|
|
59
|
+
str(output_path.absolute()),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=False, env=get_soffice_env())
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return None, f"Successfully accepted all tracked changes: {input_file} -> {output_file}"
|
|
66
|
+
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
return None, f"Error: LibreOffice failed: {result.stderr}"
|
|
69
|
+
|
|
70
|
+
return None, f"Successfully accepted all tracked changes: {input_file} -> {output_file}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _setup_libreoffice_macro() -> bool:
|
|
74
|
+
macro_dir = Path(MACRO_DIR)
|
|
75
|
+
macro_file = macro_dir / "Module1.xba"
|
|
76
|
+
|
|
77
|
+
if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
if not macro_dir.exists():
|
|
81
|
+
subprocess.run(
|
|
82
|
+
["soffice", "--headless", f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", "--terminate_after_init"],
|
|
83
|
+
capture_output=True, timeout=10, check=False, env=get_soffice_env(),
|
|
84
|
+
)
|
|
85
|
+
macro_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
macro_file.write_text(ACCEPT_CHANGES_MACRO)
|
|
89
|
+
return True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning(f"Failed to setup LibreOffice macro: {e}")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
parser = argparse.ArgumentParser(description="Accept all tracked changes in a DOCX file")
|
|
97
|
+
parser.add_argument("input_file", help="Input DOCX file with tracked changes")
|
|
98
|
+
parser.add_argument("output_file", help="Output DOCX file (clean, no tracked changes)")
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
_, message = accept_changes(args.input_file, args.output_file)
|
|
102
|
+
print(message)
|
|
103
|
+
if "Error" in message:
|
|
104
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Add comments to DOCX documents.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python comment.py unpacked/ 0 "Comment text"
|
|
5
|
+
python comment.py unpacked/ 1 "Reply text" --parent 0
|
|
6
|
+
|
|
7
|
+
Text should be pre-escaped XML (e.g., & for &, ’ for smart quotes).
|
|
8
|
+
|
|
9
|
+
After running, add markers to document.xml:
|
|
10
|
+
<w:commentRangeStart w:id="0"/>
|
|
11
|
+
... commented content ...
|
|
12
|
+
<w:commentRangeEnd w:id="0"/>
|
|
13
|
+
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import random
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import defusedxml.minidom
|
|
24
|
+
|
|
25
|
+
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
|
26
|
+
NS = {
|
|
27
|
+
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
|
28
|
+
"w14": "http://schemas.microsoft.com/office/word/2010/wordml",
|
|
29
|
+
"w15": "http://schemas.microsoft.com/office/word/2012/wordml",
|
|
30
|
+
"w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
|
31
|
+
"w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
COMMENT_XML = """\
|
|
35
|
+
<w:comment w:id="{id}" w:author="{author}" w:date="{date}" w:initials="{initials}">
|
|
36
|
+
<w:p w14:paraId="{para_id}" w14:textId="77777777">
|
|
37
|
+
<w:r>
|
|
38
|
+
<w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
|
|
39
|
+
<w:annotationRef/>
|
|
40
|
+
</w:r>
|
|
41
|
+
<w:r>
|
|
42
|
+
<w:rPr>
|
|
43
|
+
<w:color w:val="000000"/>
|
|
44
|
+
<w:sz w:val="20"/>
|
|
45
|
+
<w:szCs w:val="20"/>
|
|
46
|
+
</w:rPr>
|
|
47
|
+
<w:t>{text}</w:t>
|
|
48
|
+
</w:r>
|
|
49
|
+
</w:p>
|
|
50
|
+
</w:comment>"""
|
|
51
|
+
|
|
52
|
+
SMART_QUOTE_ENTITIES = {
|
|
53
|
+
"\u201c": "“",
|
|
54
|
+
"\u201d": "”",
|
|
55
|
+
"\u2018": "‘",
|
|
56
|
+
"\u2019": "’",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _generate_hex_id() -> str:
|
|
61
|
+
return f"{random.randint(0, 0x7FFFFFFE):08X}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _encode_smart_quotes(text: str) -> str:
|
|
65
|
+
for char, entity in SMART_QUOTE_ENTITIES.items():
|
|
66
|
+
text = text.replace(char, entity)
|
|
67
|
+
return text
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _append_xml(xml_path: Path, root_tag: str, content: str) -> None:
|
|
71
|
+
dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8"))
|
|
72
|
+
root = dom.getElementsByTagName(root_tag)[0]
|
|
73
|
+
ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items())
|
|
74
|
+
wrapper_dom = defusedxml.minidom.parseString(f"<root {ns_attrs}>{content}</root>")
|
|
75
|
+
for child in wrapper_dom.documentElement.childNodes:
|
|
76
|
+
if child.nodeType == child.ELEMENT_NODE:
|
|
77
|
+
root.appendChild(dom.importNode(child, True))
|
|
78
|
+
output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8"))
|
|
79
|
+
xml_path.write_text(output, encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_para_id(comments_path: Path, comment_id: int) -> str | None:
|
|
83
|
+
dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8"))
|
|
84
|
+
for c in dom.getElementsByTagName("w:comment"):
|
|
85
|
+
if c.getAttribute("w:id") == str(comment_id):
|
|
86
|
+
for p in c.getElementsByTagName("w:p"):
|
|
87
|
+
if pid := p.getAttribute("w14:paraId"):
|
|
88
|
+
return pid
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_next_rid(rels_path: Path) -> int:
|
|
93
|
+
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
|
94
|
+
max_rid = 0
|
|
95
|
+
for rel in dom.getElementsByTagName("Relationship"):
|
|
96
|
+
rid = rel.getAttribute("Id")
|
|
97
|
+
if rid and rid.startswith("rId"):
|
|
98
|
+
try:
|
|
99
|
+
max_rid = max(max_rid, int(rid[3:]))
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass
|
|
102
|
+
return max_rid + 1
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _has_relationship(rels_path: Path, target: str) -> bool:
|
|
106
|
+
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
|
107
|
+
for rel in dom.getElementsByTagName("Relationship"):
|
|
108
|
+
if rel.getAttribute("Target") == target:
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _has_content_type(ct_path: Path, part_name: str) -> bool:
|
|
114
|
+
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
|
115
|
+
for override in dom.getElementsByTagName("Override"):
|
|
116
|
+
if override.getAttribute("PartName") == part_name:
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _ensure_comment_relationships(unpacked_dir: Path) -> None:
|
|
122
|
+
rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels"
|
|
123
|
+
if not rels_path.exists():
|
|
124
|
+
return
|
|
125
|
+
if _has_relationship(rels_path, "comments.xml"):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
|
129
|
+
root = dom.documentElement
|
|
130
|
+
next_rid = _get_next_rid(rels_path)
|
|
131
|
+
|
|
132
|
+
rels = [
|
|
133
|
+
("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml"),
|
|
134
|
+
("http://schemas.microsoft.com/office/2011/relationships/commentsExtended", "commentsExtended.xml"),
|
|
135
|
+
("http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", "commentsIds.xml"),
|
|
136
|
+
("http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", "commentsExtensible.xml"),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for rel_type, target in rels:
|
|
140
|
+
rel = dom.createElement("Relationship")
|
|
141
|
+
rel.setAttribute("Id", f"rId{next_rid}")
|
|
142
|
+
rel.setAttribute("Type", rel_type)
|
|
143
|
+
rel.setAttribute("Target", target)
|
|
144
|
+
root.appendChild(rel)
|
|
145
|
+
next_rid += 1
|
|
146
|
+
|
|
147
|
+
rels_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ensure_comment_content_types(unpacked_dir: Path) -> None:
|
|
151
|
+
ct_path = unpacked_dir / "[Content_Types].xml"
|
|
152
|
+
if not ct_path.exists():
|
|
153
|
+
return
|
|
154
|
+
if _has_content_type(ct_path, "/word/comments.xml"):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
|
158
|
+
root = dom.documentElement
|
|
159
|
+
|
|
160
|
+
overrides = [
|
|
161
|
+
("/word/comments.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"),
|
|
162
|
+
("/word/commentsExtended.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml"),
|
|
163
|
+
("/word/commentsIds.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml"),
|
|
164
|
+
("/word/commentsExtensible.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml"),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
for part_name, content_type in overrides:
|
|
168
|
+
override = dom.createElement("Override")
|
|
169
|
+
override.setAttribute("PartName", part_name)
|
|
170
|
+
override.setAttribute("ContentType", content_type)
|
|
171
|
+
root.appendChild(override)
|
|
172
|
+
|
|
173
|
+
ct_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def add_comment(
|
|
177
|
+
unpacked_dir: str,
|
|
178
|
+
comment_id: int,
|
|
179
|
+
text: str,
|
|
180
|
+
author: str = "Kortix Agent",
|
|
181
|
+
initials: str = "K",
|
|
182
|
+
parent_id: int | None = None,
|
|
183
|
+
) -> tuple[str, str]:
|
|
184
|
+
word = Path(unpacked_dir) / "word"
|
|
185
|
+
if not word.exists():
|
|
186
|
+
return "", f"Error: {word} not found"
|
|
187
|
+
|
|
188
|
+
para_id, durable_id = _generate_hex_id(), _generate_hex_id()
|
|
189
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
190
|
+
|
|
191
|
+
comments = word / "comments.xml"
|
|
192
|
+
first_comment = not comments.exists()
|
|
193
|
+
if first_comment:
|
|
194
|
+
shutil.copy(TEMPLATE_DIR / "comments.xml", comments)
|
|
195
|
+
_ensure_comment_relationships(Path(unpacked_dir))
|
|
196
|
+
_ensure_comment_content_types(Path(unpacked_dir))
|
|
197
|
+
_append_xml(comments, "w:comments", COMMENT_XML.format(
|
|
198
|
+
id=comment_id, author=author, date=ts, initials=initials, para_id=para_id, text=text,
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
ext = word / "commentsExtended.xml"
|
|
202
|
+
if not ext.exists():
|
|
203
|
+
shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext)
|
|
204
|
+
if parent_id is not None:
|
|
205
|
+
parent_para = _find_para_id(comments, parent_id)
|
|
206
|
+
if not parent_para:
|
|
207
|
+
return "", f"Error: Parent comment {parent_id} not found"
|
|
208
|
+
_append_xml(ext, "w15:commentsEx", f'<w15:commentEx w15:paraId="{para_id}" w15:paraIdParent="{parent_para}" w15:done="0"/>')
|
|
209
|
+
else:
|
|
210
|
+
_append_xml(ext, "w15:commentsEx", f'<w15:commentEx w15:paraId="{para_id}" w15:done="0"/>')
|
|
211
|
+
|
|
212
|
+
ids = word / "commentsIds.xml"
|
|
213
|
+
if not ids.exists():
|
|
214
|
+
shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids)
|
|
215
|
+
_append_xml(ids, "w16cid:commentsIds", f'<w16cid:commentId w16cid:paraId="{para_id}" w16cid:durableId="{durable_id}"/>')
|
|
216
|
+
|
|
217
|
+
extensible = word / "commentsExtensible.xml"
|
|
218
|
+
if not extensible.exists():
|
|
219
|
+
shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible)
|
|
220
|
+
_append_xml(extensible, "w16cex:commentsExtensible", f'<w16cex:commentExtensible w16cex:durableId="{durable_id}" w16cex:dateUtc="{ts}"/>')
|
|
221
|
+
|
|
222
|
+
action = "reply" if parent_id is not None else "comment"
|
|
223
|
+
return para_id, f"Added {action} {comment_id} (para_id={para_id})"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
p = argparse.ArgumentParser(description="Add comments to DOCX documents")
|
|
228
|
+
p.add_argument("unpacked_dir", help="Unpacked DOCX directory")
|
|
229
|
+
p.add_argument("comment_id", type=int, help="Comment ID (must be unique)")
|
|
230
|
+
p.add_argument("text", help="Comment text")
|
|
231
|
+
p.add_argument("--author", default="Kortix Agent", help="Author name")
|
|
232
|
+
p.add_argument("--initials", default="K", help="Author initials")
|
|
233
|
+
p.add_argument("--parent", type=int, help="Parent comment ID (for replies)")
|
|
234
|
+
args = p.parse_args()
|
|
235
|
+
|
|
236
|
+
para_id, msg = add_comment(args.unpacked_dir, args.comment_id, args.text, args.author, args.initials, args.parent)
|
|
237
|
+
print(msg)
|
|
238
|
+
if "Error" in msg:
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
cid = args.comment_id
|
|
241
|
+
if args.parent is not None:
|
|
242
|
+
print(f"\nNest markers inside parent {args.parent}'s markers:\n <w:commentRangeStart w:id=\"{args.parent}\"/><w:commentRangeStart w:id=\"{cid}\"/>\n <w:r>...</w:r>\n <w:commentRangeEnd w:id=\"{cid}\"/><w:commentRangeEnd w:id=\"{args.parent}\"/>")
|
|
243
|
+
else:
|
|
244
|
+
print(f"\nAdd to document.xml:\n <w:commentRangeStart w:id=\"{cid}\"/>\n <w:r>...</w:r>\n <w:commentRangeEnd w:id=\"{cid}\"/>\n <w:r><w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr><w:commentReference w:id=\"{cid}\"/></w:r>")
|
|
File without changes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Merge adjacent runs with identical formatting in DOCX.
|
|
2
|
+
|
|
3
|
+
Merges adjacent <w:r> elements that have identical <w:rPr> properties.
|
|
4
|
+
Works on runs in paragraphs and inside tracked changes (<w:ins>, <w:del>).
|
|
5
|
+
|
|
6
|
+
Also:
|
|
7
|
+
- Removes rsid attributes from runs (revision metadata that doesn't affect rendering)
|
|
8
|
+
- Removes proofErr elements (spell/grammar markers that block merging)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import defusedxml.minidom
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def merge_runs(input_dir: str) -> tuple[int, str]:
|
|
17
|
+
doc_xml = Path(input_dir) / "word" / "document.xml"
|
|
18
|
+
|
|
19
|
+
if not doc_xml.exists():
|
|
20
|
+
return 0, f"Error: {doc_xml} not found"
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
|
24
|
+
root = dom.documentElement
|
|
25
|
+
|
|
26
|
+
_remove_elements(root, "proofErr")
|
|
27
|
+
_strip_run_rsid_attrs(root)
|
|
28
|
+
|
|
29
|
+
containers = {run.parentNode for run in _find_elements(root, "r")}
|
|
30
|
+
|
|
31
|
+
merge_count = 0
|
|
32
|
+
for container in containers:
|
|
33
|
+
merge_count += _merge_runs_in(container)
|
|
34
|
+
|
|
35
|
+
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
|
|
36
|
+
return merge_count, f"Merged {merge_count} runs"
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return 0, f"Error: {e}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _find_elements(root, tag: str) -> list:
|
|
45
|
+
results = []
|
|
46
|
+
|
|
47
|
+
def traverse(node):
|
|
48
|
+
if node.nodeType == node.ELEMENT_NODE:
|
|
49
|
+
name = node.localName or node.tagName
|
|
50
|
+
if name == tag or name.endswith(f":{tag}"):
|
|
51
|
+
results.append(node)
|
|
52
|
+
for child in node.childNodes:
|
|
53
|
+
traverse(child)
|
|
54
|
+
|
|
55
|
+
traverse(root)
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_child(parent, tag: str):
|
|
60
|
+
for child in parent.childNodes:
|
|
61
|
+
if child.nodeType == child.ELEMENT_NODE:
|
|
62
|
+
name = child.localName or child.tagName
|
|
63
|
+
if name == tag or name.endswith(f":{tag}"):
|
|
64
|
+
return child
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_children(parent, tag: str) -> list:
|
|
69
|
+
results = []
|
|
70
|
+
for child in parent.childNodes:
|
|
71
|
+
if child.nodeType == child.ELEMENT_NODE:
|
|
72
|
+
name = child.localName or child.tagName
|
|
73
|
+
if name == tag or name.endswith(f":{tag}"):
|
|
74
|
+
results.append(child)
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_adjacent(elem1, elem2) -> bool:
|
|
79
|
+
node = elem1.nextSibling
|
|
80
|
+
while node:
|
|
81
|
+
if node == elem2:
|
|
82
|
+
return True
|
|
83
|
+
if node.nodeType == node.ELEMENT_NODE:
|
|
84
|
+
return False
|
|
85
|
+
if node.nodeType == node.TEXT_NODE and node.data.strip():
|
|
86
|
+
return False
|
|
87
|
+
node = node.nextSibling
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _remove_elements(root, tag: str):
|
|
94
|
+
for elem in _find_elements(root, tag):
|
|
95
|
+
if elem.parentNode:
|
|
96
|
+
elem.parentNode.removeChild(elem)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _strip_run_rsid_attrs(root):
|
|
100
|
+
for run in _find_elements(root, "r"):
|
|
101
|
+
for attr in list(run.attributes.values()):
|
|
102
|
+
if "rsid" in attr.name.lower():
|
|
103
|
+
run.removeAttribute(attr.name)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _merge_runs_in(container) -> int:
|
|
109
|
+
merge_count = 0
|
|
110
|
+
run = _first_child_run(container)
|
|
111
|
+
|
|
112
|
+
while run:
|
|
113
|
+
while True:
|
|
114
|
+
next_elem = _next_element_sibling(run)
|
|
115
|
+
if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
|
|
116
|
+
_merge_run_content(run, next_elem)
|
|
117
|
+
container.removeChild(next_elem)
|
|
118
|
+
merge_count += 1
|
|
119
|
+
else:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
_consolidate_text(run)
|
|
123
|
+
run = _next_sibling_run(run)
|
|
124
|
+
|
|
125
|
+
return merge_count
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _first_child_run(container):
|
|
129
|
+
for child in container.childNodes:
|
|
130
|
+
if child.nodeType == child.ELEMENT_NODE and _is_run(child):
|
|
131
|
+
return child
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _next_element_sibling(node):
|
|
136
|
+
sibling = node.nextSibling
|
|
137
|
+
while sibling:
|
|
138
|
+
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
139
|
+
return sibling
|
|
140
|
+
sibling = sibling.nextSibling
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _next_sibling_run(node):
|
|
145
|
+
sibling = node.nextSibling
|
|
146
|
+
while sibling:
|
|
147
|
+
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
148
|
+
if _is_run(sibling):
|
|
149
|
+
return sibling
|
|
150
|
+
sibling = sibling.nextSibling
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _is_run(node) -> bool:
|
|
155
|
+
name = node.localName or node.tagName
|
|
156
|
+
return name == "r" or name.endswith(":r")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _can_merge(run1, run2) -> bool:
|
|
160
|
+
rpr1 = _get_child(run1, "rPr")
|
|
161
|
+
rpr2 = _get_child(run2, "rPr")
|
|
162
|
+
|
|
163
|
+
if (rpr1 is None) != (rpr2 is None):
|
|
164
|
+
return False
|
|
165
|
+
if rpr1 is None:
|
|
166
|
+
return True
|
|
167
|
+
return rpr1.toxml() == rpr2.toxml()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _merge_run_content(target, source):
|
|
171
|
+
for child in list(source.childNodes):
|
|
172
|
+
if child.nodeType == child.ELEMENT_NODE:
|
|
173
|
+
name = child.localName or child.tagName
|
|
174
|
+
if name != "rPr" and not name.endswith(":rPr"):
|
|
175
|
+
target.appendChild(child)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _consolidate_text(run):
|
|
179
|
+
t_elements = _get_children(run, "t")
|
|
180
|
+
|
|
181
|
+
for i in range(len(t_elements) - 1, 0, -1):
|
|
182
|
+
curr, prev = t_elements[i], t_elements[i - 1]
|
|
183
|
+
|
|
184
|
+
if _is_adjacent(prev, curr):
|
|
185
|
+
prev_text = prev.firstChild.data if prev.firstChild else ""
|
|
186
|
+
curr_text = curr.firstChild.data if curr.firstChild else ""
|
|
187
|
+
merged = prev_text + curr_text
|
|
188
|
+
|
|
189
|
+
if prev.firstChild:
|
|
190
|
+
prev.firstChild.data = merged
|
|
191
|
+
else:
|
|
192
|
+
prev.appendChild(run.ownerDocument.createTextNode(merged))
|
|
193
|
+
|
|
194
|
+
if merged.startswith(" ") or merged.endswith(" "):
|
|
195
|
+
prev.setAttribute("xml:space", "preserve")
|
|
196
|
+
elif prev.hasAttribute("xml:space"):
|
|
197
|
+
prev.removeAttribute("xml:space")
|
|
198
|
+
|
|
199
|
+
run.removeChild(curr)
|