Word generator appears to run then when I click on the link to the file I get this:
1 Like
Had the same error last week during intense testing, but have not been able to reproduce it after removing the “container” function input. Here is my modified word doc generator code spawned from @ab2308 's work.
import requests
import markdown
import io
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.enum.section import WD_SECTION_START # Explicitly importing from docx.enum.section
from bs4 import BeautifulSoup
from bs4 import NavigableString
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
import re
# Constants for formatting
DEFAULT_LINE_SPACING = 1.5 # Updated from 2.5 to 1.5
LIST_INDENT_PER_LEVEL = 44
def ultimate_word_doc_generator_v3(markdown_input: str, file_name: str, template_link: str):
"""
Allows the user to generate a word doc.
Args:
markdown_input (string): Markdown that will be turned into the word document
file_name (string): Name of the document
template_link (string): Dropbox template link
"""
# Create a blank document
doc = Document()
print("Blank document created.")
# Set default margins (optional, adjust if needed)
for section in doc.sections:
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
# --- Add Header to All Pages ---
header_url = "https://i.ibb.co/v6wXP48j/test-header.png"
header_response = requests.get(header_url)
if header_response.status_code == 200:
header_stream = io.BytesIO(header_response.content)
header = doc.sections[0].header
header_para = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
header_para.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT
run = header_para.add_run()
run.add_picture(header_stream, width=Inches(3.5))
print("Header image added to first section.")
else:
print(f"Failed to download header image: {header_response.status_code}")
# --- Add Space at the Top of the First Page ---
p = doc.add_paragraph("")
p.paragraph_format.space_after = Pt(72) # 1 inch = 72 points
# --- Extract Cover Page Data and Proposal Content in One Pass ---
cover_data = {}
proposal_content = []
in_cover_section = False
markdown_lines = markdown_input.split('\n')
for line in markdown_lines:
if line.strip() == '# Cover Page':
in_cover_section = True
elif in_cover_section and line.startswith('# '):
in_cover_section = False
proposal_content.append(line)
elif in_cover_section and line.strip().startswith('**'):
match = re.match(r'\*\*(.*?):\*\*\s*(.*)', line.strip())
if match:
key, value = match.groups()
cover_data[key.strip()] = value.strip()
elif not in_cover_section:
proposal_content.append(line)
print("Cover page data extracted:", cover_data)
# --- Add Cover Page Content ---
# Add a blank line with 28pt font size to push content down (existing spacer)
p = doc.add_paragraph("")
p.paragraph_format.space_after = Pt(0)
for run in p.runs:
run.font.size = Pt(28)
# Add cover page data
for key, value in cover_data.items():
p = doc.add_paragraph(f"{key}: {value}")
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
for run in p.runs:
run.font.name = 'Arial'
run.font.size = Pt(16)
run.bold = True
run.font.color.rgb = RGBColor(0, 0, 0)
p.paragraph_format.space_after = Pt(12)
# Add placeholder for customer logo
p = doc.add_paragraph("Customer Logo HERE")
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
for run in p.runs:
run.font.name = 'Arial'
run.font.size = Pt(12)
run.bold = True
run.font.color.rgb = RGBColor(0, 0, 0)
p.paragraph_format.space_after = Pt(48)
# --- Add Footer Image to Cover Page Footer ---
footer_url = "https://i.ibb.co/vCYxqwqX/test-footer.png"
image_response = requests.get(footer_url)
if image_response.status_code == 200:
image_stream = io.BytesIO(image_response.content)
section = doc.sections[0]
footer = section.footer
footer_para = footer.paragraphs[0] if footer.paragraphs is not None and len(footer.paragraphs) > 0 else footer.add_paragraph()
footer_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = footer_para.add_run()
run.add_picture(image_stream, width=Inches(6.5))
print("Footer image added to cover page footer.")
else:
print(f"Failed to download footer image: {image_response.status_code}")
# Add section break to separate cover page from proposal
doc.add_section(WD_SECTION_START.NEW_PAGE)
for section in doc.sections[1:]:
section.header.is_linked_to_previous = True
section.footer.is_linked_to_previous = False
section.footer.paragraphs[0].clear()
# --- Add Page Numbers to Footer on All Pages ---
for section in doc.sections:
footer = section.footer
footer_para = footer.add_paragraph()
footer_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = footer_para.add_run()
field_code = OxmlElement('w:fldChar')
field_code.set(qn('w:fldCharType'), 'begin')
instr_text = OxmlElement('w:instrText')
instr_text.text = "PAGE"
field_code_end = OxmlElement('w:fldChar')
field_code_end.set(qn('w:fldCharType'), 'end')
run._element.append(field_code)
run._element.append(instr_text)
run._element.append(field_code_end)
for r in footer_para.runs:
r.font.name = 'Arial'
r.font.size = Pt(10)
# --- Process Proposal Content ---
proposal_markdown = '\n'.join(proposal_content)
proposal_html = markdown.markdown(proposal_markdown, extensions=['extra', 'tables'])
proposal_soup = BeautifulSoup(proposal_html, "html.parser")
print("Proposal content converted to HTML.")
# Set styles for proposal content
style_normal = doc.styles['Normal']
font_normal = style_normal.font
font_normal.name = 'Arial'
font_normal.size = Pt(11)
font_normal.color.rgb = RGBColor(0, 0, 0)
style_h1 = doc.styles['Heading 1']
font_h1 = style_h1.font
font_h1.name = 'Arial'
font_h1.size = Pt(18)
font_h1.color.rgb = RGBColor(0, 0, 0)
style_h2 = doc.styles['Heading 2']
font_h2 = style_h2.font
font_h2.name = 'Arial'
font_h2.size = Pt(16)
style_h3 = doc.styles['Heading 3']
font_h3 = style_h3.font
font_h3.name = 'Arial'
font_h3.size = Pt(14)
# Helper functions for proposal content
def parse_inline(soup_element, paragraph):
if soup_element.contents is not None and len(soup_element.contents) > 0:
for content in soup_element.contents:
if isinstance(content, NavigableString):
text = content.strip()
if text:
run = paragraph.add_run(text)
run.font.name = 'Arial'
run.font.color.rgb = RGBColor(0, 0, 0)
elif content.name in ["strong", "b"]:
text = content.get_text(strip=True)
if text:
run = paragraph.add_run(text)
run.font.name = 'Arial'
run.bold = True
run.font.color.rgb = RGBColor(0, 0, 0)
else:
parse_inline(content, paragraph)
def parse_table(table_element, doc):
rows = table_element.find_all('tr')
if not rows:
return
num_rows = len(rows)
num_cols = len(rows[0].find_all(['th', 'td'])) if rows else 0
if num_rows == 0 or num_cols == 0:
return
table = doc.add_table(rows=num_rows, cols=num_cols)
table.style = 'Table Grid'
for i, row in enumerate(rows):
cells = row.find_all(['th', 'td'])
for j, cell in enumerate(cells):
cell_text = cell.get_text(strip=True)
table.cell(i, j).text = cell_text
if i == 0 or cell.find(['strong', 'b']):
for run in table.cell(i, j).paragraphs[0].runs:
run.font.name = 'Arial'
run.bold = True
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(12)
print("Table added to document with space after.")
def parse_element(element, doc):
if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
level = int(element.name[1])
heading = doc.add_heading(level=level)
parse_inline(element, heading)
if level in [1, 2, 3]:
heading.paragraph_format.space_after = Pt(12)
elif element.name == "p":
para = doc.add_paragraph()
para.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
para.paragraph_format.line_spacing = DEFAULT_LINE_SPACING
parse_inline(element, para)
elif element.name in ["ul", "ol"]:
parse_list(element, level=0)
elif element.name == "table":
parse_table(element, doc)
def parse_list(list_element, level):
list_style = "List Number" if list_element.name == "ol" else "List Bullet"
items = list_element.find_all("li", recursive=False)
if items is not None and len(items) > 0:
for li in items:
nested_list = li.find(["ul", "ol"], recursive=False)
if nested_list:
nested_list.extract()
paragraph = doc.add_paragraph(style=list_style)
paragraph.paragraph_format.left_indent = Pt(LIST_INDENT_PER_LEVEL * (level + 1))
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
paragraph.paragraph_format.line_spacing = DEFAULT_LINE_SPACING
parse_inline(li, paragraph)
if nested_list:
parse_list(nested_list, level + 1)
# Process proposal content
for element in proposal_soup.find_all(recursive=False):
parse_element(element, doc)
print("Proposal content processed.")
# --- Add Page Break and Notes Image ---
doc.add_page_break()
p = doc.add_paragraph("")
p.paragraph_format.space_after = Pt(50)
notes_url = "https://i.ibb.co/4nWTxq6w/test-notes.png"
image_response = requests.get(notes_url)
if image_response.status_code == 200:
image_stream = io.BytesIO(image_response.content)
p = doc.add_paragraph()
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = p.add_run()
run.add_picture(image_stream, width=Inches(6.5))
print("Notes image added successfully.")
else:
print(f"Failed to download notes image: {image_response.status_code}")
p = doc.add_paragraph("Notes image could not be loaded due to a download error.")
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
for run in p.runs:
run.font.name = 'Arial'
run.font.size = Pt(12)
run.font.color.rgb = RGBColor(255, 0, 0)
# --- Save the Document ---
doc.save(file_name)
print(f"Document saved as {file_name}")
return {"filename": file_name}
2 Likes
Cheers @cortcorwin
I take it this just gets pasted into the action code on a cloned Word Action?
Correct, forgot to mention that. The pickaxe youtube channel has a few videos on actions. You’ll have to remove the top of the code I provided because it’s already defined and provided by the function input / package install, etc. https://www.youtube.com/watch?v=wQKbL0kLbAk&t
1 Like