← Back to Workflow
Internal

track_changes_importer.py

Helper script for the track_changes workflow.

import sys
import docx
from docx.oxml.ns import qn

def is_bold(run_element):
    """Check if a Word run element has bold formatting (w:b in w:rPr)."""
    rPr = run_element.find(qn('w:rPr'))
    if rPr is None:
        return False
    b = rPr.find(qn('w:b'))
    if b is None:
        return False
    # w:b with val="0" or val="false" means explicitly NOT bold
    val = b.get(qn('w:val'))
    if val is not None and val.lower() in ('0', 'false'):
        return False
    return True

def get_run_text(run_element):
    """Extract text from w:t elements inside a run."""
    return ''.join(t.text for t in run_element.findall(qn('w:t')) if t.text)

def extract_runs(para_element):
    """Extract (text, is_bold) tuples from a paragraph.
    
    Includes normal runs and inserted content (w:ins).
    Skips deleted content (w:del) since that text was removed.
    Handles hyperlinks (w:hyperlink) which also contain runs.
    """
    runs = []
    for child in para_element:
        if child.tag == qn('w:del'):
            continue
        elif child.tag == qn('w:r'):
            text = get_run_text(child)
            if text:
                runs.append((text, is_bold(child)))
        elif child.tag in (qn('w:ins'), qn('w:hyperlink')):
            for r in child.findall(qn('w:r')):
                text = get_run_text(r)
                if text:
                    runs.append((text, is_bold(r)))
    return runs

def rebuild_line(runs):
    """Reconstruct a text line from runs, wrapping bold runs in ** markers."""
    if not runs:
        return ''
    parts = []
    prev_bold = False
    for text, bold in runs:
        if bold != prev_bold:
            parts.append('**')
            prev_bold = bold
        parts.append(text)
    if prev_bold:
        parts.append('**')
    return ''.join(parts)

def extract(docx_file, out):
    doc = docx.Document(docx_file)
    with open(out, 'w', encoding='utf-8') as f:
        for p in doc.paragraphs:
            runs = extract_runs(p._element)
            line = rebuild_line(runs)
            f.write(line + '\n')

if __name__ == '__main__':
    extract(*sys.argv[1:3])

This is used in: