add_footnote_before_text
Insert a formatted footnote before specified text in Microsoft Word documents. Ensures proper superscript display and streamlined document editing.
Instructions
Add a footnote before specific text with proper superscript formatting. This enhanced function ensures footnotes display correctly as superscript.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| filename | Yes | ||
| footnote_text | Yes | ||
| output_filename | No | ||
| search_text | Yes |
Implementation Reference
- word_document_server/main.py:309-314 (handler)MCP tool handler and registration for add_footnote_before_text. This is the entry point called by the MCP framework, which delegates to the footnote_tools module.@mcp.tool() def add_footnote_before_text(filename: str, search_text: str, footnote_text: str, output_filename: str = None): """Add a footnote before specific text with proper superscript formatting. This enhanced function ensures footnotes display correctly as superscript.""" return footnote_tools.add_footnote_before_text(filename, search_text, footnote_text, output_filename)
- Implementation wrapper in footnote_tools that performs validation and calls the core add_footnote_robust function with position='before'.async def add_footnote_before_text(filename: str, search_text: str, footnote_text: str, output_filename: Optional[str] = None) -> str: """Add a footnote before specific text in a Word document with proper formatting. This enhanced function ensures proper superscript formatting by managing styles at the XML level. Args: filename: Path to the Word document search_text: Text to search for (footnote will be added before this text) footnote_text: Content of the footnote output_filename: Optional output filename (if None, modifies in place) """ filename = ensure_docx_extension(filename) if not os.path.exists(filename): return f"Document {filename} does not exist" # Check if file is writeable is_writeable, error_message = check_file_writeable(filename) if not is_writeable: return f"Cannot modify document: {error_message}. Consider creating a copy first." try: # Use robust implementation success, message, details = add_footnote_robust( filename=filename, search_text=search_text, footnote_text=footnote_text, output_filename=output_filename, position="before", validate_location=True ) return message except Exception as e: return f"Failed to add footnote: {str(e)}"
- Core implementation of footnote addition. Handles direct XML manipulation of DOCX zip parts. Supports 'position' parameter for inserting the superscript reference before or after target text in the paragraph.def add_footnote_robust( filename: str, search_text: Optional[str] = None, paragraph_index: Optional[int] = None, footnote_text: str = "", output_filename: Optional[str] = None, position: str = "after", validate_location: bool = True, auto_repair: bool = False ) -> Tuple[bool, str, Optional[Dict[str, Any]]]: """ Add a footnote with robust validation and error handling. This is the main production-ready function with all fixes applied. """ # Validate inputs if not search_text and paragraph_index is None: return False, "Must provide either search_text or paragraph_index", None if search_text and paragraph_index is not None: return False, "Cannot provide both search_text and paragraph_index", None if not os.path.exists(filename): return False, f"File not found: {filename}", None # Set working file working_file = output_filename if output_filename else filename if output_filename and filename != output_filename: import shutil shutil.copy2(filename, output_filename) try: # Read document parts doc_parts = {} with zipfile.ZipFile(filename, 'r') as zin: doc_parts['document'] = zin.read('word/document.xml') doc_parts['content_types'] = zin.read('[Content_Types].xml') doc_parts['document_rels'] = zin.read('word/_rels/document.xml.rels') # Read or create footnotes.xml if 'word/footnotes.xml' in zin.namelist(): doc_parts['footnotes'] = zin.read('word/footnotes.xml') else: doc_parts['footnotes'] = _create_minimal_footnotes_xml() # Read styles if 'word/styles.xml' in zin.namelist(): doc_parts['styles'] = zin.read('word/styles.xml') else: # Create minimal styles doc_parts['styles'] = b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>' # Parse XML documents doc_root = etree.fromstring(doc_parts['document']) footnotes_root = etree.fromstring(doc_parts['footnotes']) styles_root = etree.fromstring(doc_parts['styles']) # Find target location nsmap = {'w': W_NS} if search_text: # Search for text in paragraphs found = False for para in doc_root.xpath('//w:p', namespaces=nsmap): para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap)) if search_text in para_text: target_para = para found = True break if not found: return False, f"Text '{search_text}' not found in document", None else: # Use paragraph index paragraphs = doc_root.xpath('//w:p', namespaces=nsmap) if paragraph_index >= len(paragraphs): return False, f"Paragraph index {paragraph_index} out of range", None target_para = paragraphs[paragraph_index] # Validate location if requested if validate_location: # Check if paragraph is in header/footer parent = target_para.getparent() while parent is not None: if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']: return False, "Cannot add footnote in header/footer", None parent = parent.getparent() # Get safe footnote ID footnote_id = _get_safe_footnote_id(footnotes_root) # Add footnote reference to document if position == "after": # Find last run in paragraph or create one runs = target_para.xpath('.//w:r', namespaces=nsmap) if runs: last_run = runs[-1] # Insert after last run insert_pos = target_para.index(last_run) + 1 else: insert_pos = len(target_para) else: # before # Find first run with text runs = target_para.xpath('.//w:r[w:t]', namespaces=nsmap) if runs: first_run = runs[0] insert_pos = target_para.index(first_run) else: insert_pos = 0 # Create footnote reference run ref_run = etree.Element(f'{{{W_NS}}}r') # Add run properties with superscript rPr = etree.SubElement(ref_run, f'{{{W_NS}}}rPr') rStyle = etree.SubElement(rPr, f'{{{W_NS}}}rStyle') rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference') # Add footnote reference fn_ref = etree.SubElement(ref_run, f'{{{W_NS}}}footnoteReference') fn_ref.set(f'{{{W_NS}}}id', str(footnote_id)) # Insert the reference run target_para.insert(insert_pos, ref_run) # Add footnote content new_footnote = etree.Element(f'{{{W_NS}}}footnote', attrib={f'{{{W_NS}}}id': str(footnote_id)} ) # Add paragraph to footnote fn_para = etree.SubElement(new_footnote, f'{{{W_NS}}}p') # Add paragraph properties pPr = etree.SubElement(fn_para, f'{{{W_NS}}}pPr') pStyle = etree.SubElement(pPr, f'{{{W_NS}}}pStyle') pStyle.set(f'{{{W_NS}}}val', 'FootnoteText') # Add the footnote reference marker marker_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') marker_rPr = etree.SubElement(marker_run, f'{{{W_NS}}}rPr') marker_rStyle = etree.SubElement(marker_rPr, f'{{{W_NS}}}rStyle') marker_rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference') marker_ref = etree.SubElement(marker_run, f'{{{W_NS}}}footnoteRef') # Add space after marker space_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') space_text = etree.SubElement(space_run, f'{{{W_NS}}}t') space_text.set(f'{{{XML_NS}}}space', 'preserve') space_text.text = ' ' # Add footnote text text_run = etree.SubElement(fn_para, f'{{{W_NS}}}r') text_elem = etree.SubElement(text_run, f'{{{W_NS}}}t') text_elem.text = footnote_text # Append footnote to footnotes.xml footnotes_root.append(new_footnote) # Ensure styles exist _ensure_footnote_styles(styles_root) # Ensure coherence content_types_xml = _ensure_content_types(doc_parts['content_types']) document_rels_xml = _ensure_document_rels(doc_parts['document_rels']) # Write modified document temp_file = working_file + '.tmp' with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout: with zipfile.ZipFile(filename, 'r') as zin: # Copy unchanged files for item in zin.infolist(): if item.filename not in [ 'word/document.xml', 'word/footnotes.xml', 'word/styles.xml', '[Content_Types].xml', 'word/_rels/document.xml.rels' ]: zout.writestr(item, zin.read(item.filename)) # Write modified files zout.writestr('word/document.xml', etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) zout.writestr('word/footnotes.xml', etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) zout.writestr('word/styles.xml', etree.tostring(styles_root, encoding='UTF-8', xml_declaration=True, standalone="yes")) zout.writestr('[Content_Types].xml', content_types_xml) zout.writestr('word/_rels/document.xml.rels', document_rels_xml) # Replace original with temp file os.replace(temp_file, working_file) details = { 'footnote_id': footnote_id, 'location': 'search_text' if search_text else 'paragraph_index', 'styles_created': ['FootnoteReference', 'FootnoteText'], 'coherence_verified': True } return True, f"Successfully added footnote (ID: {footnote_id}) to {working_file}", details except Exception as e: # Clean up temp file if exists temp_file = working_file + '.tmp' if os.path.exists(temp_file): os.remove(temp_file) return False, f"Error adding footnote: {str(e)}", None