Hi @lachb, I’ve modified the word action to have a better formatting (code below). Note @cortcorwin went even further so you might want to reach out to him.
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 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
# Constants for formatting
DEFAULT_LINE_SPACING = 2.5 # line spacing for paragraphs (change this value to suit)
LIST_INDENT_PER_LEVEL = 44 # in points (change this value to suit)
def ultimate_word_doc_generator(markdown_input: str, file_name: str, template_link: str, caption: 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): the link of the word template
caption (string): the action caption
"""
# Insert your PYTHON code below. You can access environment variables using os.environ[].
# Currently, only the requests library is supported, but more libraries will be available soon.
# Use print statements or return values to display results to the user.
# If you save a png, pdf, csv, jpg, webp, gif, or html file in the root directory, it will be automatically displayed to the user.
# You do not have to call this function as the bot will automatically call and fill in the parameters.
# Convert Markdown to HTML
html_content = markdown.markdown(markdown_input, extensions=['extra'])
# Parse HTML using BeautifulSoup
soup = BeautifulSoup(html_content, "html.parser")
# Decide whether to load a template or create a new document
# Replace with your Dropbox / Google Drive shared link, ensuring it ends with '?dl=1' for direct download
# Example of dropbox_link = "https://www.dropbox.com/scl/fi/kul8ybun0mll786454gd7q/Pickaxe_Template.docx?rlkey=s91uqsvapy2n27ur6q6lzb5hu&st=uqzx7jy3&dl=1"
# 1) Decide whether to load a template or create a new doc
if template_link and template_link.strip():
# Attempt to download the template
try:
response = requests.get(template_link.strip())
except Exception as e:
# If there's any error with the request, create a blank doc instead
print(f"Error fetching template link ({template_link}): {e}")
print("Falling back to a new blank document.")
doc = Document()
else:
if response.status_code == 200:
try:
template_stream = io.BytesIO(response.content)
doc = Document(template_stream)
except Exception as e:
# If the file is not a valid Word doc, fall back
print(f"Error loading template as Word doc: {e}")
print("Falling back to a new blank document.")
doc = Document()
else:
print(f"Template download failed with status {response.status_code}.")
print("Falling back to a new blank document.")
doc = Document()
else:
# No link provided or empty link => create new doc
doc = Document()
# 1A) Create a new Word document (comment out if using template)
# doc = Document()
# 1B) Import a Template (comment out if creating a new document)
# Replace with your Dropbox shared link, ensuring it ends with '?dl=1' for direct download
#dropbox_link = "https://www.dropbox.com/scl/fi/kul8ybun0mll1v868gd7q/Pickaxe_Template.docx?rlkey=s91uqsvapy2n27ur6q6lzb5hu&st=uqzx7hr3&dl=1"
#response = requests.get(dropbox_link)
#if response.status_code == 200:
# template_stream = io.BytesIO(response.content)
# doc = Document(template_stream)
#else:
# raise Exception("Failed to download the template from Dropbox")
# 4) Set Normal style for paragraphs
style_normal = doc.styles['Normal']
font_normal = style_normal.font
font_normal.name = 'Roboto' # Change font here as required
font_normal.size = Pt(11) # Change size here as required
# 5A) Set Heading 1
style_h1 = doc.styles['Heading 1']
font_h1 = style_h1.font
font_h1.name = 'Roboto' # Change font here as required
font_h1.size = Pt(38) # Change size here as required
# 5B) Set Heading 2 style
style_h2 = doc.styles['Heading 2']
font_h2 = style_h2.font
font_h2.name = 'Roboto' # Change font here as required
font_h2.size = Pt(28) # Change size here as required
# 5C) Set Heading 3
style_h3 = doc.styles['Heading 3']
font_h3 = style_h3.font
font_h3.name = 'Roboto' # Change font here as required
font_h3.size = Pt(28) # Change size here as required
# 6) Set color to black for Normal and Heading 1 styles
font_normal.color.rgb = RGBColor(0x00, 0x00, 0x00) # Change RGB colour here as required (e.g. blue would be 0x00, 0x00, 0xFF)
font_h1.color.rgb = RGBColor(0x00, 0x00, 0x00) # Change RGB colour here as required (e.g. blue would be 0x00, 0x00, 0xFF)
# 7) Styling of the text
def parse_inline(soup_element, paragraph):
"""
Recursively adds text and inline formatting (e.g., bold) to a paragraph,
forcing all runs to be black and trimming extra spaces.
"""
for content in soup_element.contents:
if isinstance(content, NavigableString):
text = content.strip()
if text:
run = paragraph.add_run(text)
run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
elif content.name in ["strong", "b"]:
text = content.get_text(strip=True)
if text:
run = paragraph.add_run(text)
run.bold = True
run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
else:
parse_inline(content, paragraph)
def parse_element(element):
"""
Processes top-level elements: headings, paragraphs, and lists.
"""
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)
elif element.name == "p":
para = doc.add_paragraph()
# Set text justification and line spacing
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)
# 8) Styling of the lists
def parse_list(list_element, level):
"""
Processes list elements. Supports one level of nesting.
"""
list_style = "List Number" if list_element.name == "ol" else "List Bullet"
for li in list_element.find_all("li", recursive=False):
nested_list = li.find(["ul", "ol"], recursive=False)
if nested_list:
nested_list.extract()
paragraph = doc.add_paragraph(style=list_style)
# Set left indent based on level
paragraph.paragraph_format.left_indent = Pt(LIST_INDENT_PER_LEVEL * (level + 1))
# Optionally, you can also justify list items if desired:
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
# Set line spacing for list paragraphs
paragraph.paragraph_format.line_spacing = DEFAULT_LINE_SPACING
parse_inline(li, paragraph)
if nested_list:
parse_list(nested_list, level + 1)
# Process each top-level element in the HTML
for element in soup.find_all(recursive=False):
parse_element(element)
# 9) Styling of the table --- Add the approval table at the end ---
table = doc.add_table(rows=3, cols=2) # Chnage size of the table by enetering the number of rows / columns
table.style = 'Table Grid'
table.cell(0, 0).text = "Approved By:" # Change text of selected cell. In this case 0,0. If you are adding more rows or want to change the text in other cells add a line that points to the correct cell.
table.cell(1, 0).text = "Date:"
table.cell(2, 0).text = "Signature:"
table.cell(0, 1).text = ""
table.cell(1, 1).text = ""
table.cell(2, 1).text = ""
table.columns[0].width = Inches(2) # Change the width of the table column 0
table.columns[1].width = Inches(4) # Change the width of the table column 1
desired_row_height = 500 # Change the height of the rows
for row in table.rows:
tr = row._tr
trPr = tr.get_or_add_trPr()
trHeight = OxmlElement('w:trHeight')
trHeight.set(qn('w:val'), str(desired_row_height))
trHeight.set(qn('w:hRule'), 'exact')
trPr.append(trHeight)
for row in table.rows:
for cell in row.cells:
shading_elm = parse_xml(
r'<w:shd {} w:fill="f3f6fa"/>'.format(nsdecls('w'))
)
cell._tc.get_or_add_tcPr().append(shading_elm)
# 10) The below code avoids the text in the table to be split on different pages
def set_cant_split(row):
trPr = row._tr.get_or_add_trPr()
cant_split = OxmlElement('w:cantSplit')
trPr.append(cant_split)
for row in table.rows:
set_cant_split(row)
# 11) New function to force font on all runs
def force_font(doc, font_name):
for para in doc.paragraphs:
for run in para.runs:
run.font.name = font_name
try:
rPr = run._element.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = OxmlElement('w:rFonts')
rPr.append(rFonts)
rFonts.set(qn('w:ascii'), font_name)
rFonts.set(qn('w:hAnsi'), font_name)
rFonts.set(qn('w:eastAsia'), font_name)
rFonts.set(qn('w:cs'), font_name)
except Exception as e:
print("Error forcing font on run:", e)
# Force all runs to use the desired font (e.g., Arial, Times New Roman)
force_font(doc, "Verdana")
# 11) Save the Word document
doc.save("MyDocument.docx")
#12) Send the file to a Make.com webhook
# Open the generated document in binary mode
with open("MyDocument.docx", "rb") as file_data:
files = {
"file": ("MyDocument.docx", file_data, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
}
# Replace this URL with your actual webhook endpoint URL
webhook_url = "https://hook.eu2.make.com/xxx"
response = requests.post(webhook_url, files=files)
print("Webhook response status:", response.status_code)
print("Webhook response:", response.text)`