"""Diplomatic cable parser for Mosaic — structured field extraction. Cables have a specific format with fields like SUBJECT, ORIGIN, CLASSIFICATION, TAGS, and a multi-section body. """ import re import logging from typing import Optional from .base import ParseResult logger = logging.getLogger(__name__) # Cable field patterns CABLE_FIELDS = { 'VZCZCXYZ': 'transmission_id', 'DE ': 'from_code', 'ZNR': 'precedence', 'ZNY': 'precedence', 'FM ': 'from', 'TO ': 'to', 'INFO ': 'info', 'BT': 'break', 'UNCLAS': 'classification', 'CONFIDENTIAL': 'classification', 'SECRET': 'classification', 'SIPDIS': 'distribution', 'E.O.': 'executive_order', 'TAGS:': 'tags', 'SUBJECT:': 'subject', 'REF:': 'reference', 'REFS:': 'reference', } # Classification levels CLASSIFICATIONS = ['UNCLASSIFIED', 'CONFIDENTIAL', 'SECRET', 'SECRET//NOFORN', 'CONFIDENTIAL//NOFORN', 'UNCLAS', 'UNCLAS SECTION'] # Section markers in cable bodies SECTION_PATTERN = re.compile(r'^\d+\.\s+\((?:U|C|S|SBU)\)', re.MULTILINE) # ANSI escape pattern ANSI_ESCAPE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07') class CableDocParser: """Parse diplomatic cables into structured fields.""" def parse(self, file_path: str) -> ParseResult: """Parse a diplomatic cable. Extracts structured fields (SUBJECT, ORIGIN, CLASSIFICATION, TAGS, etc.) as discrete fields BEFORE any chunking. """ try: with open(file_path, 'r', encoding='utf-8', errors='replace') as f: content = f.read() # Check if this is HTML (WikiLeaks format) or raw cable if ' ParseResult: """Parse a raw-format diplomatic cable.""" metadata = {} lines = content.split('\n') body_start = 0 # Extract header fields for i, line in enumerate(lines): line_stripped = line.strip() if not line_stripped: continue # Classification for cls in CLASSIFICATIONS: if line_stripped.startswith(cls): metadata['CLASSIFICATION'] = cls break # Named fields if line_stripped.startswith('SUBJECT:'): metadata['SUBJECT'] = line_stripped[8:].strip() elif line_stripped.startswith('TAGS:'): metadata['TAGS'] = line_stripped[5:].strip() elif line_stripped.startswith('REF:') or line_stripped.startswith('REFS:'): ref_start = 4 if line_stripped.startswith('REF:') else 5 metadata['REFERENCE'] = line_stripped[ref_start:].strip() elif line_stripped.startswith('E.O.'): metadata['EXECUTIVE_ORDER'] = line_stripped[4:].strip() elif line_stripped.startswith('FM '): metadata['ORIGIN'] = line_stripped[3:].strip() elif line_stripped.startswith('TO '): metadata.setdefault('TO', []).append(line_stripped[3:].strip()) # Detect body start (after numbered paragraph or after header block) if SECTION_PATTERN.match(line_stripped): body_start = i break if line_stripped == 'BT' and i > 5: body_start = i + 1 break if body_start == 0: # Fallback: body starts after a double blank line for i in range(len(lines) - 1): if not lines[i].strip() and not lines[i+1].strip(): body_start = i + 2 break body = '\n'.join(lines[body_start:]).strip() # Extract sections from body sections = self._extract_sections(body) if sections: metadata['sections'] = sections # Build formatted text text_parts = [] for key in ('SUBJECT', 'ORIGIN', 'CLASSIFICATION', 'TAGS', 'REFERENCE'): if key in metadata: val = metadata[key] if isinstance(val, list): val = ', '.join(val) text_parts.append(f"{key}: {val}") text_parts.append('') text_parts.append(body) text = '\n'.join(text_parts) text = ANSI_ESCAPE.sub('', text) return ParseResult( success=True, text=text, metadata=metadata, doc_type='cable', title=metadata.get('SUBJECT', ''), char_count=len(text), ) def _parse_html_cable(self, content: str) -> ParseResult: """Parse a WikiLeaks HTML-format cable page.""" try: from bs4 import BeautifulSoup soup = BeautifulSoup(content, 'html.parser') metadata = {} # WikiLeaks cable pages have specific CSS classes/structures # Try common patterns title_el = soup.find('title') if title_el: metadata['SUBJECT'] = title_el.get_text(strip=True) # Look for cable metadata in page for label in ('origin', 'classification', 'tags', 'subject', 'date'): el = soup.find(class_=re.compile(label, re.I)) if el: metadata[label.upper()] = el.get_text(strip=True) # Extract main content body_el = (soup.find(id='cable-text') or soup.find(class_='cable-text') or soup.find('pre') or soup.find(class_='content')) if body_el: body = body_el.get_text(separator='\n', strip=True) else: body = soup.get_text(separator='\n', strip=True) # Build text text_parts = [] for key in ('SUBJECT', 'ORIGIN', 'CLASSIFICATION', 'TAGS'): if key in metadata: text_parts.append(f"{key}: {metadata[key]}") text_parts.append('') text_parts.append(body) text = '\n'.join(text_parts) text = ANSI_ESCAPE.sub('', text) return ParseResult( success=True, text=text, metadata=metadata, doc_type='cable', title=metadata.get('SUBJECT', ''), char_count=len(text), ) except Exception as e: return ParseResult(error=f"HTML cable parse error: {str(e)}") def _extract_sections(self, body: str) -> list[dict]: """Extract numbered sections from cable body.""" sections = [] current_num = None current_text = [] for line in body.split('\n'): match = SECTION_PATTERN.match(line.strip()) if match: if current_num is not None: sections.append({ 'number': current_num, 'text': '\n'.join(current_text).strip() }) # Extract section number and classification current_num = line.strip().split('.')[0] current_text = [line] elif current_num is not None: current_text.append(line) if current_num is not None: sections.append({ 'number': current_num, 'text': '\n'.join(current_text).strip() }) return sections