feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Standalone function to add comments to a .docx file using zipfile + lxml.
|
||||
Use AFTER ContractEditor.save() since ContractEditor rewrites document.xml.
|
||||
|
||||
Usage:
|
||||
from add_comments_to_docx import add_comments_to_docx
|
||||
# comments = [(anchor_text, comment_text), ...]
|
||||
placed = add_comments_to_docx('/tmp/【修】contract.docx', comments)
|
||||
"""
|
||||
import zipfile, io
|
||||
from datetime import datetime
|
||||
from lxml import etree
|
||||
|
||||
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
||||
|
||||
def qn(tag):
|
||||
return f'{{{W}}}{tag}'
|
||||
|
||||
def add_comments_to_docx(filepath, comments, author='WB'):
|
||||
"""Add comments to a docx file.
|
||||
|
||||
Args:
|
||||
filepath: path to docx (modified in place)
|
||||
comments: list of (anchor_text, comment_text) tuples
|
||||
author: comment author name (default 'WB')
|
||||
|
||||
Returns:
|
||||
int: number of comments successfully placed
|
||||
"""
|
||||
with open(filepath, 'rb') as f:
|
||||
data = f.read()
|
||||
zin = zipfile.ZipFile(io.BytesIO(data))
|
||||
buf = io.BytesIO()
|
||||
zout = zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED)
|
||||
doc_xml = zin.read('word/document.xml')
|
||||
doc_tree = etree.fromstring(doc_xml)
|
||||
body = doc_tree.find(qn('body'))
|
||||
|
||||
nsmap = {'w': W, 'r': R_NS}
|
||||
comments_xml = etree.Element(qn('comments'), nsmap=nsmap)
|
||||
|
||||
comment_id = 200
|
||||
placed = 0
|
||||
|
||||
for anchor_text, comment_text in comments:
|
||||
cid = str(comment_id)
|
||||
comment_id += 1
|
||||
|
||||
# Create comment element
|
||||
comment_el = etree.SubElement(comments_xml, qn('comment'))
|
||||
comment_el.set(qn('id'), cid)
|
||||
comment_el.set(qn('author'), author)
|
||||
comment_el.set(qn('date'), datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'))
|
||||
cp = etree.SubElement(comment_el, qn('p'))
|
||||
cr = etree.SubElement(cp, qn('r'))
|
||||
ct = etree.SubElement(cr, qn('t'))
|
||||
ct.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
||||
ct.text = comment_text
|
||||
|
||||
# Find anchor in document
|
||||
for p in body.iter(qn('p')):
|
||||
runs = p.findall(f'.//{qn("r")}')
|
||||
full = ''.join(''.join(t.text or '' for t in r.findall(qn('t'))) for r in runs)
|
||||
if anchor_text in full:
|
||||
# Insert commentRangeStart at beginning of paragraph
|
||||
crs = etree.Element(qn('commentRangeStart'))
|
||||
crs.set(qn('id'), cid)
|
||||
p.insert(0, crs)
|
||||
|
||||
# Append commentRangeEnd + reference
|
||||
cre = etree.Element(qn('commentRangeEnd'))
|
||||
cre.set(qn('id'), cid)
|
||||
p.append(cre)
|
||||
|
||||
ref_run = etree.SubElement(p, qn('r'))
|
||||
ref_rpr = etree.SubElement(ref_run, qn('rPr'))
|
||||
ref_style = etree.SubElement(ref_rpr, qn('rStyle'))
|
||||
ref_style.set(qn('val'), 'CommentReference')
|
||||
ref_cr = etree.SubElement(ref_run, qn('commentReference'))
|
||||
ref_cr.set(qn('id'), cid)
|
||||
|
||||
placed += 1
|
||||
break
|
||||
|
||||
# Rewrite zip
|
||||
for item in zin.namelist():
|
||||
if item == 'word/document.xml':
|
||||
zout.writestr(item, etree.tostring(doc_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
||||
elif item == '[Content_Types].xml':
|
||||
ct_xml = zin.read(item)
|
||||
ct_tree = etree.fromstring(ct_xml)
|
||||
if not any('comments.xml' in (el.get('PartName') or '') for el in ct_tree):
|
||||
override = etree.SubElement(ct_tree, 'Override')
|
||||
override.set('PartName', '/word/comments.xml')
|
||||
override.set('ContentType', 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml')
|
||||
zout.writestr(item, etree.tostring(ct_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
||||
elif item == 'word/_rels/document.xml.rels':
|
||||
rels_xml = zin.read(item)
|
||||
rels_tree = etree.fromstring(rels_xml)
|
||||
if not any('comments.xml' in (el.get('Target') or '') for el in rels_tree):
|
||||
rel = etree.SubElement(rels_tree, 'Relationship')
|
||||
rel.set('Id', f'rId{len(rels_tree) + 10}')
|
||||
rel.set('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments')
|
||||
rel.set('Target', 'comments.xml')
|
||||
zout.writestr(item, etree.tostring(rels_tree, xml_declaration=True, encoding='UTF-8', standalone=True))
|
||||
else:
|
||||
zout.writestr(item, zin.read(item))
|
||||
|
||||
zout.writestr('word/comments.xml', etree.tostring(comments_xml, xml_declaration=True, encoding='UTF-8', standalone=True))
|
||||
zout.close()
|
||||
zin.close()
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(buf.getvalue())
|
||||
return placed
|
||||
Reference in New Issue
Block a user