@farazirfan/costar-server-executor 1.7.37 → 1.7.39
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/dist/agent/agent.d.ts +90 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +606 -0
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/pi-embedded-runner/run.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/run.js +2 -1
- package/dist/agent/pi-embedded-runner/run.js.map +1 -1
- package/dist/agent/pi-embedded-runner/system-prompt.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/system-prompt.js +16 -37
- package/dist/agent/pi-embedded-runner/system-prompt.js.map +1 -1
- package/dist/agent/pi-embedded-runner/tools.d.ts +4 -1
- package/dist/agent/pi-embedded-runner/tools.d.ts.map +1 -1
- package/dist/agent/pi-embedded-runner/tools.js +3 -1
- package/dist/agent/pi-embedded-runner/tools.js.map +1 -1
- package/dist/agent/pi-embedded-runner/types.d.ts +4 -0
- package/dist/agent/pi-embedded-runner/types.d.ts.map +1 -1
- package/dist/cli/env-loader.d.ts.map +1 -1
- package/dist/cli/env-loader.js +1 -0
- package/dist/cli/env-loader.js.map +1 -1
- package/dist/cli/setup.js +2 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/cron/normalize.d.ts +31 -0
- package/dist/cron/normalize.d.ts.map +1 -0
- package/dist/cron/normalize.js +211 -0
- package/dist/cron/normalize.js.map +1 -0
- package/dist/cron/scheduler.d.ts +33 -3
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +253 -48
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/heartbeat/runner.d.ts +27 -12
- package/dist/heartbeat/runner.d.ts.map +1 -1
- package/dist/heartbeat/runner.js +82 -104
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/infra/heartbeat-events-filter.d.ts +29 -0
- package/dist/infra/heartbeat-events-filter.d.ts.map +1 -0
- package/dist/infra/heartbeat-events-filter.js +80 -0
- package/dist/infra/heartbeat-events-filter.js.map +1 -0
- package/dist/infra/index.d.ts +9 -0
- package/dist/infra/index.d.ts.map +1 -0
- package/dist/infra/index.js +9 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/infra/system-events.d.ts +58 -2
- package/dist/infra/system-events.d.ts.map +1 -1
- package/dist/infra/system-events.js +80 -14
- package/dist/infra/system-events.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -1
- package/dist/server.js.map +1 -1
- package/dist/services/platform-keys.d.ts +19 -0
- package/dist/services/platform-keys.d.ts.map +1 -0
- package/dist/services/platform-keys.js +74 -0
- package/dist/services/platform-keys.js.map +1 -0
- package/dist/subagent/registry.d.ts +96 -0
- package/dist/subagent/registry.d.ts.map +1 -0
- package/dist/subagent/registry.js +180 -0
- package/dist/subagent/registry.js.map +1 -0
- package/dist/tools/complete-turn.d.ts +2 -2
- package/dist/tools/complete-turn.js +10 -10
- package/dist/tools/complete-turn.js.map +1 -1
- package/dist/tools/contacts.d.ts +13 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/contacts.js +80 -0
- package/dist/tools/contacts.js.map +1 -0
- package/dist/tools/cron.d.ts +17 -2
- package/dist/tools/cron.d.ts.map +1 -1
- package/dist/tools/cron.js +117 -35
- package/dist/tools/cron.js.map +1 -1
- package/dist/tools/google-maps.d.ts +6 -6
- package/dist/tools/google-maps.d.ts.map +1 -1
- package/dist/tools/google-maps.js +207 -262
- package/dist/tools/google-maps.js.map +1 -1
- package/dist/tools/index.d.ts +17 -7
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +40 -9
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/phone-call.d.ts +11 -0
- package/dist/tools/phone-call.d.ts.map +1 -0
- package/dist/tools/phone-call.js +151 -0
- package/dist/tools/phone-call.js.map +1 -0
- package/dist/tools/sessions-spawn.d.ts +33 -0
- package/dist/tools/sessions-spawn.d.ts.map +1 -0
- package/dist/tools/sessions-spawn.js +164 -0
- package/dist/tools/sessions-spawn.js.map +1 -0
- package/dist/tools/spotify.d.ts +12 -0
- package/dist/tools/spotify.d.ts.map +1 -0
- package/dist/tools/spotify.js +251 -0
- package/dist/tools/spotify.js.map +1 -0
- package/dist/tools/subagents.d.ts +23 -0
- package/dist/tools/subagents.d.ts.map +1 -0
- package/dist/tools/subagents.js +209 -0
- package/dist/tools/subagents.js.map +1 -0
- package/dist/tools/whatsapp.d.ts +13 -0
- package/dist/tools/whatsapp.d.ts.map +1 -0
- package/dist/tools/whatsapp.js +215 -0
- package/dist/tools/whatsapp.js.map +1 -0
- package/dist/tools/youtube.d.ts +12 -0
- package/dist/tools/youtube.d.ts.map +1 -0
- package/dist/tools/youtube.js +218 -0
- package/dist/tools/youtube.js.map +1 -0
- package/dist/utils/asterizk-auth.d.ts +43 -0
- package/dist/utils/asterizk-auth.d.ts.map +1 -0
- package/dist/utils/asterizk-auth.js +125 -0
- package/dist/utils/asterizk-auth.js.map +1 -0
- package/dist/web-server.d.ts.map +1 -1
- package/dist/web-server.js +132 -0
- package/dist/web-server.js.map +1 -1
- package/dist/workspace/index.d.ts +3 -4
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +3 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/templates.d.ts +8 -7
- package/dist/workspace/templates.d.ts.map +1 -1
- package/dist/workspace/templates.js +18 -127
- package/dist/workspace/templates.js.map +1 -1
- package/dist/workspace/workspace.d.ts +2 -4
- package/dist/workspace/workspace.d.ts.map +1 -1
- package/dist/workspace/workspace.js +7 -16
- package/dist/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +231 -0
- package/skills/docx/SKILL.md +468 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/accept_changes.py +181 -0
- package/skills/docx/scripts/comment.py +347 -0
- package/skills/docx/scripts/helpers/__init__.py +0 -0
- package/skills/docx/scripts/helpers/merge_runs.py +231 -0
- package/skills/docx/scripts/helpers/simplify_redlines.py +240 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/scripts/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/scripts/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/scripts/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/scripts/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/scripts/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/scripts/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/scripts/ooxml/scripts/pack.py +159 -0
- package/skills/docx/scripts/ooxml/scripts/unpack.py +29 -0
- package/skills/docx/scripts/ooxml/scripts/validate.py +106 -0
- package/skills/docx/scripts/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/docx/scripts/ooxml/scripts/validation/base.py +1023 -0
- package/skills/docx/scripts/ooxml/scripts/validation/docx.py +519 -0
- package/skills/docx/scripts/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/docx/scripts/ooxml/scripts/validation/redlining.py +284 -0
- package/skills/docx/scripts/pack.py +166 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/docx/scripts/unpack.py +134 -0
- package/skills/longform-video-generation/SKILL.md +298 -0
- package/skills/longform-video-generation/references/advanced_techniques.md +474 -0
- package/skills/longform-video-generation/references/google_api_guide.md +288 -0
- package/skills/longform-video-generation/scripts/video_generator.py +579 -0
- package/skills/pdf/FORMS.md +305 -0
- package/skills/pdf/REFERENCE.md +612 -0
- package/skills/pdf/SKILL.md +293 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/skills/pdf/scripts/create_validation_image.py +41 -0
- package/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/skills/pdf/scripts/extract_form_structure.py +124 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +116 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +136 -0
- package/skills/pptx/SKILL.md +171 -0
- package/skills/pptx/editing.md +205 -0
- package/skills/pptx/pptxgenjs.md +377 -0
- package/skills/pptx/scripts/add_slide.py +225 -0
- package/skills/pptx/scripts/clean.py +309 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/scripts/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/scripts/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/scripts/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/scripts/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/scripts/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/scripts/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/scripts/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/scripts/ooxml/scripts/pack.py +159 -0
- package/skills/pptx/scripts/ooxml/scripts/unpack.py +29 -0
- package/skills/pptx/scripts/ooxml/scripts/validate.py +106 -0
- package/skills/pptx/scripts/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/pptx/scripts/ooxml/scripts/validation/base.py +1023 -0
- package/skills/pptx/scripts/ooxml/scripts/validation/docx.py +519 -0
- package/skills/pptx/scripts/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/pptx/scripts/ooxml/scripts/validation/redlining.py +284 -0
- package/skills/pptx/scripts/pack.py +168 -0
- package/skills/pptx/scripts/thumbnail.py +318 -0
- package/skills/pptx/scripts/unpack.py +86 -0
- package/skills/xlsx/SKILL.md +291 -0
- package/skills/xlsx/recalc.py +247 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validator for PowerPoint presentation XML files against XSD schemas.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .base import BaseSchemaValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PPTXSchemaValidator(BaseSchemaValidator):
|
|
11
|
+
"""Validator for PowerPoint presentation XML files against XSD schemas."""
|
|
12
|
+
|
|
13
|
+
# PowerPoint presentation namespace
|
|
14
|
+
PRESENTATIONML_NAMESPACE = (
|
|
15
|
+
"http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# PowerPoint-specific element to relationship type mappings
|
|
19
|
+
ELEMENT_RELATIONSHIP_TYPES = {
|
|
20
|
+
"sldid": "slide",
|
|
21
|
+
"sldmasterid": "slidemaster",
|
|
22
|
+
"notesmasterid": "notesmaster",
|
|
23
|
+
"sldlayoutid": "slidelayout",
|
|
24
|
+
"themeid": "theme",
|
|
25
|
+
"tablestyleid": "tablestyles",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def validate(self):
|
|
29
|
+
"""Run all validation checks and return True if all pass."""
|
|
30
|
+
# Test 0: XML well-formedness
|
|
31
|
+
if not self.validate_xml():
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
# Test 1: Namespace declarations
|
|
35
|
+
all_valid = True
|
|
36
|
+
if not self.validate_namespaces():
|
|
37
|
+
all_valid = False
|
|
38
|
+
|
|
39
|
+
# Test 2: Unique IDs
|
|
40
|
+
if not self.validate_unique_ids():
|
|
41
|
+
all_valid = False
|
|
42
|
+
|
|
43
|
+
# Test 3: UUID ID validation
|
|
44
|
+
if not self.validate_uuid_ids():
|
|
45
|
+
all_valid = False
|
|
46
|
+
|
|
47
|
+
# Test 4: Relationship and file reference validation
|
|
48
|
+
if not self.validate_file_references():
|
|
49
|
+
all_valid = False
|
|
50
|
+
|
|
51
|
+
# Test 5: Slide layout ID validation
|
|
52
|
+
if not self.validate_slide_layout_ids():
|
|
53
|
+
all_valid = False
|
|
54
|
+
|
|
55
|
+
# Test 6: Content type declarations
|
|
56
|
+
if not self.validate_content_types():
|
|
57
|
+
all_valid = False
|
|
58
|
+
|
|
59
|
+
# Test 7: XSD schema validation
|
|
60
|
+
if not self.validate_against_xsd():
|
|
61
|
+
all_valid = False
|
|
62
|
+
|
|
63
|
+
# Test 8: Notes slide reference validation
|
|
64
|
+
if not self.validate_notes_slide_references():
|
|
65
|
+
all_valid = False
|
|
66
|
+
|
|
67
|
+
# Test 9: Relationship ID reference validation
|
|
68
|
+
if not self.validate_all_relationship_ids():
|
|
69
|
+
all_valid = False
|
|
70
|
+
|
|
71
|
+
# Test 10: Duplicate slide layout references validation
|
|
72
|
+
if not self.validate_no_duplicate_slide_layouts():
|
|
73
|
+
all_valid = False
|
|
74
|
+
|
|
75
|
+
return all_valid
|
|
76
|
+
|
|
77
|
+
def validate_uuid_ids(self):
|
|
78
|
+
"""Validate that ID attributes that look like UUIDs contain only hex values."""
|
|
79
|
+
import lxml.etree
|
|
80
|
+
|
|
81
|
+
errors = []
|
|
82
|
+
# UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens
|
|
83
|
+
uuid_pattern = re.compile(
|
|
84
|
+
r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for xml_file in self.xml_files:
|
|
88
|
+
try:
|
|
89
|
+
root = lxml.etree.parse(str(xml_file)).getroot()
|
|
90
|
+
|
|
91
|
+
# Check all elements for ID attributes
|
|
92
|
+
for elem in root.iter():
|
|
93
|
+
for attr, value in elem.attrib.items():
|
|
94
|
+
# Check if this is an ID attribute
|
|
95
|
+
attr_name = attr.split("}")[-1].lower()
|
|
96
|
+
if attr_name == "id" or attr_name.endswith("id"):
|
|
97
|
+
# Check if value looks like a UUID (has the right length and pattern structure)
|
|
98
|
+
if self._looks_like_uuid(value):
|
|
99
|
+
# Validate that it contains only hex characters in the right positions
|
|
100
|
+
if not uuid_pattern.match(value):
|
|
101
|
+
errors.append(
|
|
102
|
+
f" {xml_file.relative_to(self.unpacked_dir)}: "
|
|
103
|
+
f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
except (lxml.etree.XMLSyntaxError, Exception) as e:
|
|
107
|
+
errors.append(
|
|
108
|
+
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if errors:
|
|
112
|
+
print(f"FAILED - Found {len(errors)} UUID ID validation errors:")
|
|
113
|
+
for error in errors:
|
|
114
|
+
print(error)
|
|
115
|
+
return False
|
|
116
|
+
else:
|
|
117
|
+
if self.verbose:
|
|
118
|
+
print("PASSED - All UUID-like IDs contain valid hex values")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def _looks_like_uuid(self, value):
|
|
122
|
+
"""Check if a value has the general structure of a UUID."""
|
|
123
|
+
# Remove common UUID delimiters
|
|
124
|
+
clean_value = value.strip("{}()").replace("-", "")
|
|
125
|
+
# Check if it's 32 hex-like characters (could include invalid hex chars)
|
|
126
|
+
return len(clean_value) == 32 and all(c.isalnum() for c in clean_value)
|
|
127
|
+
|
|
128
|
+
def validate_slide_layout_ids(self):
|
|
129
|
+
"""Validate that sldLayoutId elements in slide masters reference valid slide layouts."""
|
|
130
|
+
import lxml.etree
|
|
131
|
+
|
|
132
|
+
errors = []
|
|
133
|
+
|
|
134
|
+
# Find all slide master files
|
|
135
|
+
slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml"))
|
|
136
|
+
|
|
137
|
+
if not slide_masters:
|
|
138
|
+
if self.verbose:
|
|
139
|
+
print("PASSED - No slide masters found")
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
for slide_master in slide_masters:
|
|
143
|
+
try:
|
|
144
|
+
# Parse the slide master file
|
|
145
|
+
root = lxml.etree.parse(str(slide_master)).getroot()
|
|
146
|
+
|
|
147
|
+
# Find the corresponding _rels file for this slide master
|
|
148
|
+
rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
|
|
149
|
+
|
|
150
|
+
if not rels_file.exists():
|
|
151
|
+
errors.append(
|
|
152
|
+
f" {slide_master.relative_to(self.unpacked_dir)}: "
|
|
153
|
+
f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}"
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# Parse the relationships file
|
|
158
|
+
rels_root = lxml.etree.parse(str(rels_file)).getroot()
|
|
159
|
+
|
|
160
|
+
# Build a set of valid relationship IDs that point to slide layouts
|
|
161
|
+
valid_layout_rids = set()
|
|
162
|
+
for rel in rels_root.findall(
|
|
163
|
+
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
|
|
164
|
+
):
|
|
165
|
+
rel_type = rel.get("Type", "")
|
|
166
|
+
if "slideLayout" in rel_type:
|
|
167
|
+
valid_layout_rids.add(rel.get("Id"))
|
|
168
|
+
|
|
169
|
+
# Find all sldLayoutId elements in the slide master
|
|
170
|
+
for sld_layout_id in root.findall(
|
|
171
|
+
f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId"
|
|
172
|
+
):
|
|
173
|
+
r_id = sld_layout_id.get(
|
|
174
|
+
f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id"
|
|
175
|
+
)
|
|
176
|
+
layout_id = sld_layout_id.get("id")
|
|
177
|
+
|
|
178
|
+
if r_id and r_id not in valid_layout_rids:
|
|
179
|
+
errors.append(
|
|
180
|
+
f" {slide_master.relative_to(self.unpacked_dir)}: "
|
|
181
|
+
f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' "
|
|
182
|
+
f"references r:id='{r_id}' which is not found in slide layout relationships"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except (lxml.etree.XMLSyntaxError, Exception) as e:
|
|
186
|
+
errors.append(
|
|
187
|
+
f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if errors:
|
|
191
|
+
print(f"FAILED - Found {len(errors)} slide layout ID validation errors:")
|
|
192
|
+
for error in errors:
|
|
193
|
+
print(error)
|
|
194
|
+
print(
|
|
195
|
+
"Remove invalid references or add missing slide layouts to the relationships file."
|
|
196
|
+
)
|
|
197
|
+
return False
|
|
198
|
+
else:
|
|
199
|
+
if self.verbose:
|
|
200
|
+
print("PASSED - All slide layout IDs reference valid slide layouts")
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
def validate_no_duplicate_slide_layouts(self):
|
|
204
|
+
"""Validate that each slide has exactly one slideLayout reference."""
|
|
205
|
+
import lxml.etree
|
|
206
|
+
|
|
207
|
+
errors = []
|
|
208
|
+
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
|
|
209
|
+
|
|
210
|
+
for rels_file in slide_rels_files:
|
|
211
|
+
try:
|
|
212
|
+
root = lxml.etree.parse(str(rels_file)).getroot()
|
|
213
|
+
|
|
214
|
+
# Find all slideLayout relationships
|
|
215
|
+
layout_rels = [
|
|
216
|
+
rel
|
|
217
|
+
for rel in root.findall(
|
|
218
|
+
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
|
|
219
|
+
)
|
|
220
|
+
if "slideLayout" in rel.get("Type", "")
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
if len(layout_rels) > 1:
|
|
224
|
+
errors.append(
|
|
225
|
+
f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
errors.append(
|
|
230
|
+
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if errors:
|
|
234
|
+
print("FAILED - Found slides with duplicate slideLayout references:")
|
|
235
|
+
for error in errors:
|
|
236
|
+
print(error)
|
|
237
|
+
return False
|
|
238
|
+
else:
|
|
239
|
+
if self.verbose:
|
|
240
|
+
print("PASSED - All slides have exactly one slideLayout reference")
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
def validate_notes_slide_references(self):
|
|
244
|
+
"""Validate that each notesSlide file is referenced by only one slide."""
|
|
245
|
+
import lxml.etree
|
|
246
|
+
|
|
247
|
+
errors = []
|
|
248
|
+
notes_slide_references = {} # Track which slides reference each notesSlide
|
|
249
|
+
|
|
250
|
+
# Find all slide relationship files
|
|
251
|
+
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
|
|
252
|
+
|
|
253
|
+
if not slide_rels_files:
|
|
254
|
+
if self.verbose:
|
|
255
|
+
print("PASSED - No slide relationship files found")
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
for rels_file in slide_rels_files:
|
|
259
|
+
try:
|
|
260
|
+
# Parse the relationships file
|
|
261
|
+
root = lxml.etree.parse(str(rels_file)).getroot()
|
|
262
|
+
|
|
263
|
+
# Find all notesSlide relationships
|
|
264
|
+
for rel in root.findall(
|
|
265
|
+
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
|
|
266
|
+
):
|
|
267
|
+
rel_type = rel.get("Type", "")
|
|
268
|
+
if "notesSlide" in rel_type:
|
|
269
|
+
target = rel.get("Target", "")
|
|
270
|
+
if target:
|
|
271
|
+
# Normalize the target path to handle relative paths
|
|
272
|
+
normalized_target = target.replace("../", "")
|
|
273
|
+
|
|
274
|
+
# Track which slide references this notesSlide
|
|
275
|
+
slide_name = rels_file.stem.replace(
|
|
276
|
+
".xml", ""
|
|
277
|
+
) # e.g., "slide1"
|
|
278
|
+
|
|
279
|
+
if normalized_target not in notes_slide_references:
|
|
280
|
+
notes_slide_references[normalized_target] = []
|
|
281
|
+
notes_slide_references[normalized_target].append(
|
|
282
|
+
(slide_name, rels_file)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
except (lxml.etree.XMLSyntaxError, Exception) as e:
|
|
286
|
+
errors.append(
|
|
287
|
+
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Check for duplicate references
|
|
291
|
+
for target, references in notes_slide_references.items():
|
|
292
|
+
if len(references) > 1:
|
|
293
|
+
slide_names = [ref[0] for ref in references]
|
|
294
|
+
errors.append(
|
|
295
|
+
f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}"
|
|
296
|
+
)
|
|
297
|
+
for slide_name, rels_file in references:
|
|
298
|
+
errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}")
|
|
299
|
+
|
|
300
|
+
if errors:
|
|
301
|
+
print(
|
|
302
|
+
f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:"
|
|
303
|
+
)
|
|
304
|
+
for error in errors:
|
|
305
|
+
print(error)
|
|
306
|
+
print("Each slide may optionally have its own slide file.")
|
|
307
|
+
return False
|
|
308
|
+
else:
|
|
309
|
+
if self.verbose:
|
|
310
|
+
print("PASSED - All notes slide references are unique")
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
raise RuntimeError("This module should not be run directly.")
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validator for tracked changes in Word documents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RedliningValidator:
|
|
12
|
+
"""Validator for tracked changes in Word documents."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"):
|
|
15
|
+
self.unpacked_dir = Path(unpacked_dir)
|
|
16
|
+
self.original_docx = Path(original_docx)
|
|
17
|
+
self.verbose = verbose
|
|
18
|
+
self.author = author
|
|
19
|
+
self.namespaces = {
|
|
20
|
+
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def repair(self) -> int:
|
|
24
|
+
"""No auto-repairs for redlining validation. Returns 0."""
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
def validate(self):
|
|
28
|
+
"""Main validation method that returns True if valid, False otherwise."""
|
|
29
|
+
# Verify unpacked directory exists and has correct structure
|
|
30
|
+
modified_file = self.unpacked_dir / "word" / "document.xml"
|
|
31
|
+
if not modified_file.exists():
|
|
32
|
+
print(f"FAILED - Modified document.xml not found at {modified_file}")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# First, check if there are any tracked changes by the author to validate
|
|
36
|
+
try:
|
|
37
|
+
import xml.etree.ElementTree as ET
|
|
38
|
+
|
|
39
|
+
tree = ET.parse(modified_file)
|
|
40
|
+
root = tree.getroot()
|
|
41
|
+
|
|
42
|
+
# Check for w:del or w:ins tags by the specified author
|
|
43
|
+
del_elements = root.findall(".//w:del", self.namespaces)
|
|
44
|
+
ins_elements = root.findall(".//w:ins", self.namespaces)
|
|
45
|
+
|
|
46
|
+
# Filter to only include changes by the specified author
|
|
47
|
+
author_del_elements = [
|
|
48
|
+
elem
|
|
49
|
+
for elem in del_elements
|
|
50
|
+
if elem.get(f"{{{self.namespaces['w']}}}author") == self.author
|
|
51
|
+
]
|
|
52
|
+
author_ins_elements = [
|
|
53
|
+
elem
|
|
54
|
+
for elem in ins_elements
|
|
55
|
+
if elem.get(f"{{{self.namespaces['w']}}}author") == self.author
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Redlining validation is only needed if tracked changes by the author have been used.
|
|
59
|
+
if not author_del_elements and not author_ins_elements:
|
|
60
|
+
if self.verbose:
|
|
61
|
+
print(f"PASSED - No tracked changes by {self.author} found.")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
except Exception:
|
|
65
|
+
# If we can't parse the XML, continue with full validation
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Create temporary directory for unpacking original docx
|
|
69
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
70
|
+
temp_path = Path(temp_dir)
|
|
71
|
+
|
|
72
|
+
# Unpack original docx
|
|
73
|
+
try:
|
|
74
|
+
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
|
|
75
|
+
zip_ref.extractall(temp_path)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"FAILED - Error unpacking original docx: {e}")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
original_file = temp_path / "word" / "document.xml"
|
|
81
|
+
if not original_file.exists():
|
|
82
|
+
print(
|
|
83
|
+
f"FAILED - Original document.xml not found in {self.original_docx}"
|
|
84
|
+
)
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Parse both XML files using xml.etree.ElementTree for redlining validation
|
|
88
|
+
try:
|
|
89
|
+
import xml.etree.ElementTree as ET
|
|
90
|
+
|
|
91
|
+
modified_tree = ET.parse(modified_file)
|
|
92
|
+
modified_root = modified_tree.getroot()
|
|
93
|
+
original_tree = ET.parse(original_file)
|
|
94
|
+
original_root = original_tree.getroot()
|
|
95
|
+
except ET.ParseError as e:
|
|
96
|
+
print(f"FAILED - Error parsing XML files: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# Remove the author's tracked changes from both documents
|
|
100
|
+
self._remove_author_tracked_changes(original_root)
|
|
101
|
+
self._remove_author_tracked_changes(modified_root)
|
|
102
|
+
|
|
103
|
+
# Extract and compare text content
|
|
104
|
+
modified_text = self._extract_text_content(modified_root)
|
|
105
|
+
original_text = self._extract_text_content(original_root)
|
|
106
|
+
|
|
107
|
+
if modified_text != original_text:
|
|
108
|
+
# Show detailed character-level differences for each paragraph
|
|
109
|
+
error_message = self._generate_detailed_diff(
|
|
110
|
+
original_text, modified_text
|
|
111
|
+
)
|
|
112
|
+
print(error_message)
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
if self.verbose:
|
|
116
|
+
print(f"PASSED - All changes by {self.author} are properly tracked")
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def _generate_detailed_diff(self, original_text, modified_text):
|
|
120
|
+
"""Generate detailed word-level differences using git word diff."""
|
|
121
|
+
error_parts = [
|
|
122
|
+
f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes",
|
|
123
|
+
"",
|
|
124
|
+
"Likely causes:",
|
|
125
|
+
" 1. Modified text inside another author's <w:ins> or <w:del> tags",
|
|
126
|
+
" 2. Made edits without proper tracked changes",
|
|
127
|
+
" 3. Didn't nest <w:del> inside <w:ins> when deleting another's insertion",
|
|
128
|
+
"",
|
|
129
|
+
"For pre-redlined documents, use correct patterns:",
|
|
130
|
+
" - To reject another's INSERTION: Nest <w:del> inside their <w:ins>",
|
|
131
|
+
" - To restore another's DELETION: Add new <w:ins> AFTER their <w:del>",
|
|
132
|
+
"",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Show git word diff
|
|
136
|
+
git_diff = self._get_git_word_diff(original_text, modified_text)
|
|
137
|
+
if git_diff:
|
|
138
|
+
error_parts.extend(["Differences:", "============", git_diff])
|
|
139
|
+
else:
|
|
140
|
+
error_parts.append("Unable to generate word diff (git not available)")
|
|
141
|
+
|
|
142
|
+
return "\n".join(error_parts)
|
|
143
|
+
|
|
144
|
+
def _get_git_word_diff(self, original_text, modified_text):
|
|
145
|
+
"""Generate word diff using git with character-level precision."""
|
|
146
|
+
try:
|
|
147
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
148
|
+
temp_path = Path(temp_dir)
|
|
149
|
+
|
|
150
|
+
# Create two files
|
|
151
|
+
original_file = temp_path / "original.txt"
|
|
152
|
+
modified_file = temp_path / "modified.txt"
|
|
153
|
+
|
|
154
|
+
original_file.write_text(original_text, encoding="utf-8")
|
|
155
|
+
modified_file.write_text(modified_text, encoding="utf-8")
|
|
156
|
+
|
|
157
|
+
# Try character-level diff first for precise differences
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
[
|
|
160
|
+
"git",
|
|
161
|
+
"diff",
|
|
162
|
+
"--word-diff=plain",
|
|
163
|
+
"--word-diff-regex=.", # Character-by-character diff
|
|
164
|
+
"-U0", # Zero lines of context - show only changed lines
|
|
165
|
+
"--no-index",
|
|
166
|
+
str(original_file),
|
|
167
|
+
str(modified_file),
|
|
168
|
+
],
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if result.stdout.strip():
|
|
174
|
+
# Clean up the output - remove git diff header lines
|
|
175
|
+
lines = result.stdout.split("\n")
|
|
176
|
+
# Skip the header lines (diff --git, index, +++, ---, @@)
|
|
177
|
+
content_lines = []
|
|
178
|
+
in_content = False
|
|
179
|
+
for line in lines:
|
|
180
|
+
if line.startswith("@@"):
|
|
181
|
+
in_content = True
|
|
182
|
+
continue
|
|
183
|
+
if in_content and line.strip():
|
|
184
|
+
content_lines.append(line)
|
|
185
|
+
|
|
186
|
+
if content_lines:
|
|
187
|
+
return "\n".join(content_lines)
|
|
188
|
+
|
|
189
|
+
# Fallback to word-level diff if character-level is too verbose
|
|
190
|
+
result = subprocess.run(
|
|
191
|
+
[
|
|
192
|
+
"git",
|
|
193
|
+
"diff",
|
|
194
|
+
"--word-diff=plain",
|
|
195
|
+
"-U0", # Zero lines of context
|
|
196
|
+
"--no-index",
|
|
197
|
+
str(original_file),
|
|
198
|
+
str(modified_file),
|
|
199
|
+
],
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if result.stdout.strip():
|
|
205
|
+
lines = result.stdout.split("\n")
|
|
206
|
+
content_lines = []
|
|
207
|
+
in_content = False
|
|
208
|
+
for line in lines:
|
|
209
|
+
if line.startswith("@@"):
|
|
210
|
+
in_content = True
|
|
211
|
+
continue
|
|
212
|
+
if in_content and line.strip():
|
|
213
|
+
content_lines.append(line)
|
|
214
|
+
return "\n".join(content_lines)
|
|
215
|
+
|
|
216
|
+
except (subprocess.CalledProcessError, FileNotFoundError, Exception):
|
|
217
|
+
# Git not available or other error, return None to use fallback
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _remove_author_tracked_changes(self, root):
|
|
223
|
+
"""Remove tracked changes authored by the specified author from the XML root."""
|
|
224
|
+
ins_tag = f"{{{self.namespaces['w']}}}ins"
|
|
225
|
+
del_tag = f"{{{self.namespaces['w']}}}del"
|
|
226
|
+
author_attr = f"{{{self.namespaces['w']}}}author"
|
|
227
|
+
|
|
228
|
+
# Remove w:ins elements
|
|
229
|
+
for parent in root.iter():
|
|
230
|
+
to_remove = []
|
|
231
|
+
for child in parent:
|
|
232
|
+
if child.tag == ins_tag and child.get(author_attr) == self.author:
|
|
233
|
+
to_remove.append(child)
|
|
234
|
+
for elem in to_remove:
|
|
235
|
+
parent.remove(elem)
|
|
236
|
+
|
|
237
|
+
# Unwrap content in w:del elements where author matches
|
|
238
|
+
deltext_tag = f"{{{self.namespaces['w']}}}delText"
|
|
239
|
+
t_tag = f"{{{self.namespaces['w']}}}t"
|
|
240
|
+
|
|
241
|
+
for parent in root.iter():
|
|
242
|
+
to_process = []
|
|
243
|
+
for child in parent:
|
|
244
|
+
if child.tag == del_tag and child.get(author_attr) == self.author:
|
|
245
|
+
to_process.append((child, list(parent).index(child)))
|
|
246
|
+
|
|
247
|
+
# Process in reverse order to maintain indices
|
|
248
|
+
for del_elem, del_index in reversed(to_process):
|
|
249
|
+
# Convert w:delText to w:t before moving
|
|
250
|
+
for elem in del_elem.iter():
|
|
251
|
+
if elem.tag == deltext_tag:
|
|
252
|
+
elem.tag = t_tag
|
|
253
|
+
|
|
254
|
+
# Move all children of w:del to its parent before removing w:del
|
|
255
|
+
for child in reversed(list(del_elem)):
|
|
256
|
+
parent.insert(del_index, child)
|
|
257
|
+
parent.remove(del_elem)
|
|
258
|
+
|
|
259
|
+
def _extract_text_content(self, root):
|
|
260
|
+
"""Extract text content from Word XML, preserving paragraph structure.
|
|
261
|
+
|
|
262
|
+
Empty paragraphs are skipped to avoid false positives when tracked
|
|
263
|
+
insertions add only structural elements without text content.
|
|
264
|
+
"""
|
|
265
|
+
p_tag = f"{{{self.namespaces['w']}}}p"
|
|
266
|
+
t_tag = f"{{{self.namespaces['w']}}}t"
|
|
267
|
+
|
|
268
|
+
paragraphs = []
|
|
269
|
+
for p_elem in root.findall(f".//{p_tag}"):
|
|
270
|
+
# Get all text elements within this paragraph
|
|
271
|
+
text_parts = []
|
|
272
|
+
for t_elem in p_elem.findall(f".//{t_tag}"):
|
|
273
|
+
if t_elem.text:
|
|
274
|
+
text_parts.append(t_elem.text)
|
|
275
|
+
paragraph_text = "".join(text_parts)
|
|
276
|
+
# Skip empty paragraphs - they don't affect content validation
|
|
277
|
+
if paragraph_text:
|
|
278
|
+
paragraphs.append(paragraph_text)
|
|
279
|
+
|
|
280
|
+
return "\n".join(paragraphs)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
raise RuntimeError("This module should not be run directly.")
|