Setting the file. One moment.
DOCX · PPTX · anthropics/skills · Skills Docs
ContentsBack to the top of the page Opc Dig Sig
404
def repair
— line 404
This file
Number 11.50
Position 50 of 54
Type Python
Size 17 KB
Lines 466 scripts/office/validators/ docx.py
Python · 466 lines · 17 KB
import
safe_extract
15
16 from .base import BaseSchemaValidator
17
18
19 class DOCXSchemaValidator ( BaseSchemaValidator ):
20
21 WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
22 W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml"
23 W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid"
24
25 ELEMENT_RELATIONSHIP_TYPES = {}
26
27 def validate (self):
28 if not self .validate_xml():
29 return False
30
31 all_valid = True
32 if not self .validate_namespaces():
33 all_valid = False
34
35 if not self .validate_unique_ids():
36 all_valid = False
37
38 if not self .validate_file_references():
39 all_valid = False
40
41 if not self .validate_content_types():
42 all_valid = False
43
44 if not self .validate_against_xsd():
45 all_valid = False
46
47 if not self .validate_whitespace_preservation():
48 all_valid = False
49
50 if not self .validate_deletions():
51 all_valid = False
52
53 if not self .validate_insertions():
54 all_valid = False
55
56 if not self .validate_all_relationship_ids():
57 all_valid = False
58
59 if not self .validate_id_constraints():
60 all_valid = False
61
62 if not self .validate_comment_markers():
63 all_valid = False
64
65 self .compare_paragraph_counts()
66
67 return all_valid
68
69 def validate_whitespace_preservation (self):
70 errors = []
71
72 for xml_file in self .xml_files:
73 if xml_file.name != "document.xml" :
74 continue
75
76 try :
77 root = lxml.etree.parse( str (xml_file)).getroot()
78
79 for elem in root.iter( f " {{{ self . WORD_2006_NAMESPACE }}} t" ):
80 if elem.text:
81 text = elem.text
82 if re.search( r " ^[ \t\n\r ] " , text) or re.search(
83 r " [ \t\n\r ]$ " , text
84 ):
85 xml_space_attr = f " {{{ self . XML_NAMESPACE }}} space"
86 if (
87 xml_space_attr not in elem.attrib
88 or elem.attrib[xml_space_attr] != "preserve"
89 ):
90 text_preview = (
91 repr (text)[: 50 ] + "..."
92 if len ( repr (text)) > 50
93 else repr (text)
94 )
95 errors.append(
96 f " { xml_file.relative_to( self .unpacked_dir) } : "
97 f "Line { elem.sourceline } : w:t element with whitespace missing xml:space='preserve': { text_preview } "
98 )
99
100 except (lxml.etree.XMLSyntaxError, Exception ) as e:
101 errors.append(
102 f " { xml_file.relative_to( self .unpacked_dir) } : Error: { e } "
103 )
104
105 if errors:
106 print ( f "FAILED - Found { len (errors) } whitespace preservation violations:" )
107 for error in errors:
108 print (error)
109 return False
110 else :
111 if self .verbose:
112 print ( "PASSED - All whitespace is properly preserved" )
113 return True
114
115 def validate_deletions (self):
116 errors = []
117
118 for xml_file in self .xml_files:
119 if xml_file.name != "document.xml" :
120 continue
121
122 try :
123 root = lxml.etree.parse( str (xml_file)).getroot()
124 namespaces = { "w" : self . WORD_2006_NAMESPACE }
125
126 for t_elem in root.xpath( ".//w:del//w:t" , namespaces = namespaces):
127 if t_elem.text:
128 text_preview = (
129 repr (t_elem.text)[: 50 ] + "..."
130 if len ( repr (t_elem.text)) > 50
131 else repr (t_elem.text)
132 )
133 errors.append(
134 f " { xml_file.relative_to( self .unpacked_dir) } : "
135 f "Line { t_elem.sourceline } : <w:t> found within <w:del>: { text_preview } "
136 )
137
138 for instr_elem in root.xpath(
139 ".//w:del//w:instrText" , namespaces = namespaces
140 ):
141 text_preview = (
142 repr (instr_elem.text or "" )[: 50 ] + "..."
143 if len ( repr (instr_elem.text or "" )) > 50
144 else repr (instr_elem.text or "" )
145 )
146 errors.append(
147 f " { xml_file.relative_to( self .unpacked_dir) } : "
148 f "Line { instr_elem.sourceline } : <w:instrText> found within <w:del> (use <w:delInstrText>): { text_preview } "
149 )
150
151 except (lxml.etree.XMLSyntaxError, Exception ) as e:
152 errors.append(
153 f " { xml_file.relative_to( self .unpacked_dir) } : Error: { e } "
154 )
155
156 if errors:
157 print ( f "FAILED - Found { len (errors) } deletion validation violations:" )
158 for error in errors:
159 print (error)
160 return False
161 else :
162 if self .verbose:
163 print ( "PASSED - No w:t elements found within w:del elements" )
164 return True
165
166 def count_paragraphs_in_unpacked (self):
167 count = 0
168
169 for xml_file in self .xml_files:
170 if xml_file.name != "document.xml" :
171 continue
172
173 try :
174 root = lxml.etree.parse( str (xml_file)).getroot()
175 paragraphs = root.findall( f ".// {{{ self . WORD_2006_NAMESPACE }}} p" )
176 count = len (paragraphs)
177 except Exception as e:
178 print ( f "Error counting paragraphs in unpacked document: { e } " )
179
180 return count
181
182 def count_paragraphs_in_original (self):
183 original = self .original_file
184 if original is None :
185 return 0
186
187 count = 0
188
189 try :
190 with tempfile.TemporaryDirectory() as temp_dir:
191 with zipfile.ZipFile(original, "r" ) as zip_ref:
192 safe_extract(zip_ref, Path(temp_dir))
193
194 doc_xml_path = temp_dir + "/word/document.xml"
195 root = lxml.etree.parse(doc_xml_path).getroot()
196
197 paragraphs = root.findall( f ".// {{{ self . WORD_2006_NAMESPACE }}} p" )
198 count = len (paragraphs)
199
200 except Exception as e:
201 print ( f "Error counting paragraphs in original document: { e } " )
202
203 return count
204
205 def validate_insertions (self):
206 errors = []
207
208 for xml_file in self .xml_files:
209 if xml_file.name != "document.xml" :
210 continue
211
212 try :
213 root = lxml.etree.parse( str (xml_file)).getroot()
214 namespaces = { "w" : self . WORD_2006_NAMESPACE }
215
216 invalid_elements = root.xpath(
217 ".//w:ins//w:delText[not(ancestor::w:del)]" , namespaces = namespaces
218 )
219
220 for elem in invalid_elements:
221 text_preview = (
222 repr (elem.text or "" )[: 50 ] + "..."
223 if len ( repr (elem.text or "" )) > 50
224 else repr (elem.text or "" )
225 )
226 errors.append(
227 f " { xml_file.relative_to( self .unpacked_dir) } : "
228 f "Line { elem.sourceline } : <w:delText> within <w:ins>: { text_preview } "
229 )
230
231 except (lxml.etree.XMLSyntaxError, Exception ) as e:
232 errors.append(
233 f " { xml_file.relative_to( self .unpacked_dir) } : Error: { e } "
234 )
235
236 if errors:
237 print ( f "FAILED - Found { len (errors) } insertion validation violations:" )
238 for error in errors:
239 print (error)
240 return False
241 else :
242 if self .verbose:
243 print ( "PASSED - No w:delText elements within w:ins elements" )
244 return True
245
246 def compare_paragraph_counts (self):
247 new_count = self .count_paragraphs_in_unpacked()
248 if self .original_file is None :
249 print ( f " \n Paragraphs: { new_count } " )
250 return
251
252 original_count = self .count_paragraphs_in_original()
253 diff = new_count - original_count
254 diff_str = f "+ { diff } " if diff > 0 else str (diff)
255 print ( f " \n Paragraphs: { original_count } → { new_count } ( { diff_str } )" )
256
257 def _parse_id_value (self, val: str , base: int = 16 ) -> int :
258 return int (val, base)
259
260 def validate_id_constraints (self):
261 errors = []
262 para_id_attr = f " {{{ self . W14_NAMESPACE }}} paraId"
263 durable_id_attr = f " {{{ self . W16CID_NAMESPACE }}} durableId"
264
265 for xml_file in self .xml_files:
266 try :
267 for elem in lxml.etree.parse( str (xml_file)).iter():
268 if val := elem.get(para_id_attr):
269 try :
270 if self ._parse_id_value(val, base = 16 ) >= 0x 80000000 :
271 errors.append(
272 f " { xml_file.name } : { elem.sourceline } : paraId= { val } >= 0x80000000"
273 )
274 except ValueError :
275 errors.append(
276 f " { xml_file.name } : { elem.sourceline } : "
277 f "paraId= { val } is not valid hex"
278 )
279
280 if val := elem.get(durable_id_attr):
281 if xml_file.name == "numbering.xml" :
282 try :
283 if self ._parse_id_value(val, base = 10 ) >= 0x 7FFFFFFF :
284 errors.append(
285 f " { xml_file.name } : { elem.sourceline } : "
286 f "durableId= { val } >= 0x7FFFFFFF"
287 )
288 except ValueError :
289 errors.append(
290 f " { xml_file.name } : { elem.sourceline } : "
291 f "durableId= { val } must be decimal in numbering.xml"
292 )
293 else :
294 try :
295 if self ._parse_id_value(val, base = 16 ) >= 0x 7FFFFFFF :
296 errors.append(
297 f " { xml_file.name } : { elem.sourceline } : "
298 f "durableId= { val } >= 0x7FFFFFFF"
299 )
300 except ValueError :
301 errors.append(
302 f " { xml_file.name } : { elem.sourceline } : "
303 f "durableId= { val } is not valid hex"
304 )
305 except lxml.etree.XMLSyntaxError:
306 continue
307
308 if errors:
309 print ( f "FAILED - { len (errors) } ID constraint violations:" )
310 for e in errors:
311 print (e)
312 elif self .verbose:
313 print ( "PASSED - All paraId/durableId values within constraints" )
314 return not errors
315
316 def validate_comment_markers (self):
317 errors = []
318
319 document_xml = None
320 comments_xml = None
321 for xml_file in self .xml_files:
322 if xml_file.name == "document.xml" and "word" in str (xml_file):
323 document_xml = xml_file
324 elif xml_file.name == "comments.xml" :
325 comments_xml = xml_file
326
327 if not document_xml:
328 if self .verbose:
329 print ( "PASSED - No document.xml found (skipping comment validation)" )
330 return True
331
332 try :
333 doc_root = lxml.etree.parse( str (document_xml)).getroot()
334 namespaces = { "w" : self . WORD_2006_NAMESPACE }
335
336 range_starts = {
337 elem.get( f " {{{ self . WORD_2006_NAMESPACE }}} id" )
338 for elem in doc_root.xpath(
339 ".//w:commentRangeStart" , namespaces = namespaces
340 )
341 }
342 range_ends = {
343 elem.get( f " {{{ self . WORD_2006_NAMESPACE }}} id" )
344 for elem in doc_root.xpath(
345 ".//w:commentRangeEnd" , namespaces = namespaces
346 )
347 }
348 references = {
349 elem.get( f " {{{ self . WORD_2006_NAMESPACE }}} id" )
350 for elem in doc_root.xpath(
351 ".//w:commentReference" , namespaces = namespaces
352 )
353 }
354
355 orphaned_ends = range_ends - range_starts
356 for comment_id in sorted (
357 orphaned_ends, key =lambda x: int (x) if x and x.isdigit() else 0
358 ):
359 errors.append(
360 f ' document.xml: commentRangeEnd id=" { comment_id } " has no matching commentRangeStart'
361 )
362
363 orphaned_starts = range_starts - range_ends
364 for comment_id in sorted (
365 orphaned_starts, key =lambda x: int (x) if x and x.isdigit() else 0
366 ):
367 errors.append(
368 f ' document.xml: commentRangeStart id=" { comment_id } " has no matching commentRangeEnd'
369 )
370
371 comment_ids = set ()
372 if comments_xml and comments_xml.exists():
373 comments_root = lxml.etree.parse( str (comments_xml)).getroot()
374 comment_ids = {
375 elem.get( f " {{{ self . WORD_2006_NAMESPACE }}} id" )
376 for elem in comments_root.xpath(
377 ".//w:comment" , namespaces = namespaces
378 )
379 }
380
381 marker_ids = range_starts | range_ends | references
382 invalid_refs = marker_ids - comment_ids
383 for comment_id in sorted (
384 invalid_refs, key =lambda x: int (x) if x and x.isdigit() else 0
385 ):
386 if comment_id:
387 errors.append(
388 f ' document.xml: marker id=" { comment_id } " references non-existent comment'
389 )
390
391 except (lxml.etree.XMLSyntaxError, Exception ) as e:
392 errors.append( f " Error parsing XML: { e } " )
393
394 if errors:
395 print ( f "FAILED - { len (errors) } comment marker violations:" )
396 for error in errors:
397 print (error)
398 return False
399 else :
400 if self .verbose:
401 print ( "PASSED - All comment markers properly paired" )
402 return True
403
404 def repair (self) -> int :
405 repairs = super ().repair()
406 repairs += self .repair_durableId()
407 return repairs
408
409 def repair_durableId (self) -> int :
410 DURABLE_ID_ATTRS = ( "w16cid:durableId" , "w16cex:durableId" )
411 repairs = 0
412 renames: dict = {}
413
414 for xml_file in self .xml_files:
415 try :
416 content = xml_file.read_text( encoding = "utf-8" )
417 dom = defusedxml.minidom.parseString(content)
418 is_numbering = xml_file.name == "numbering.xml"
419 base = 10 if is_numbering else 16
420 pending = []
421 seen_in_file = set ()
422 modified = False
423
424 for elem in dom.getElementsByTagName( "*" ):
425 for attr_name in DURABLE_ID_ATTRS :
426 if not elem.hasAttribute(attr_name):
427 continue
428
429 durable_id = elem.getAttribute(attr_name)
430 try :
431 key = self ._parse_id_value(durable_id, base = base)
432 needs_repair = key >= 0x 7FFFFFFF
433 except ValueError :
434 key = durable_id
435 needs_repair = True
436
437 if needs_repair:
438 if key in seen_in_file:
439 value = random.randint( 1 , 0x 7FFFFFFE )
440 else :
441 seen_in_file.add(key)
442 if key not in renames:
443 renames[key] = random.randint( 1 , 0x 7FFFFFFE )
444 value = renames[key]
445 new_id = str (value) if is_numbering else f " { value :08X} "
446
447 elem.setAttribute(attr_name, new_id)
448 pending.append(
449 f " Repaired: { xml_file.name } : durableId { durable_id } → { new_id } "
450 )
451 modified = True
452
453 if modified:
454 xml_file.write_bytes(dom.toxml( encoding = "UTF-8" ))
455 for message in pending:
456 print (message)
457 repairs += len (pending)
458
459 except Exception :
460 pass
461
462 return repairs
463
464
465 if __name__ == "__main__" :
466 raise RuntimeError ( "This module should not be run directly." )