Skip to main content

mark-my-word

You have a Word template — the right fonts, colours, headers, footers. You have a Markdown file you want to print. mark-my-word connects the two: it reads your Word template and uses it to render your Markdown as a styled PDF.

Is this for me?

It's a good fit if:

  • You have a .docx or .dotx Word template with the styling you want
  • You want your PDFs to match that template without manual CSS work
  • You're happy running a command-line tool

It's probably not for you if you need pixel-perfect fidelity to complex Word layouts (tables with merged cells, tracked changes, etc.) or if you need to render Word documents directly — this tool works with Markdown input.

Install

uv tool install mark-my-word
# or
pipx install mark-my-word

Requires Python 3.11+. WeasyPrint (the PDF engine) needs some system libraries — see the WeasyPrint docs if the install fails.

Basic usage

Pass your Word template directly and mark-my-word does the rest:

mark-my-word render document.md --template my-template.dotx

Output lands next to the input file by default (document.pdf). Use --out (or -o) to put it somewhere else:

mark-my-word render document.md --template my-template.dotx --out /tmp/document.pdf

If the output file already exists, the command refuses rather than silently overwriting. Use --force (or -f) to overwrite:

mark-my-word render document.md --template my-template.dotx --force

Or extract the theme separately

If you render from the same template often, extract it once and reuse the theme directory:

mark-my-word extract my-template.dotx --out themes/my-theme/
mark-my-word render document.md --theme themes/my-theme/

The extracted theme is plain CSS and HTML — easy to inspect or tweak. Add --save-theme themes/my-theme/ to the one-step command if you want to keep the theme without a separate extract step.

Or install the theme under a name

A theme you use often can be installed once and named, so --theme takes a name instead of a path and works from any directory:

mark-my-word theme install my-template.dotx house-style
mark-my-word render document.md --theme house-style

theme install takes either a Word template (extracting it) or an existing theme directory (copying it), and names the theme after the source unless you give a name. The copy is a snapshot: edit the original and the installed theme does not change.

For a theme you are still working on, link it instead — the installed name follows your edits:

mark-my-word theme link ../themes/goalwards

The rest of the commands manage what is installed:

mark-my-word theme list             # names, and where the links point
mark-my-word theme rename OLD NEW
mark-my-word theme remove NAME      # for a link, removes the link only

theme list also names the template each theme came from, and flags one an older version built:

base  (base.dotx, 0.9.0)
goalwards  (Goalwards.dotx, built by 0.8.1 — current 0.9.0)
handmade

That comes from a mark-my-word.json file extract writes into the theme, recording the template's filename, the extraction date, and the version. It holds no path, so a theme you share says nothing about where your templates live. A theme without the file — hand-written, or extracted before 0.9.0 — works exactly the same; the file is a record, not a requirement.

Installing over an existing name fails unless you pass --force. Themes live in $XDG_DATA_HOME/mark-my-word/themes, or ~/.local/share/mark-my-word/themes when that is unset.

When --theme gets a bare name it looks in the registry first, and says Using installed theme: NAME when it finds one. To use a local directory of the same name, write it as a path — --theme ./goalwards.

Document frontmatter

Headers and footers in your theme can include placeholders like {{title}} or {{author}}. You can also supply a fallback: {{title: Untitled}} is used when title is not set. Matching is case-insensitive.

Set values in your Markdown file's YAML frontmatter:

---
title: Q1 Report
author: Daniel
---

# Introduction

...

Pass extra values on the command line with --set:

mark-my-word render document.md --theme themes/my-theme/ --set version=draft

Skipping preamble

If you write notes in an app like NotePlan, there's often metadata at the top — tags, links, app-specific syntax — that you don't want in the PDF. Put <!--more--> on its own line to mark where your real content starts:

@project(Quarterly Review)
[[linked-note]]

<!--more-->

# Q1 Report

...

Everything before <!--more--> is ignored. Everything after renders normally. If <!--more--> appears inside a code block in the body, put a second one at the top of the document to act as the separator instead.

Heading anchors and links

Every heading gets an anchor derived from its text, so you can link between sections. The trouble is that the anchor for "Everything you wanted to know about widgets" is everything-you-wanted-to-know-about-widgets — correct, and miserable to type into a link.

So name it yourself. Put {#your-id} at the end of the heading line:

# Everything you wanted to know about widgets {#widgets}

See [the widget section](#widgets) for the details.

The link resolves to the heading, and the id holds steady even when you reword the heading text. One catch: the {#id} has to sit at the end of the heading line. On its own line above the heading it does not attach — it renders as literal text instead.

The same braces carry CSS classes too, if your theme has a use for them: # Heading {#widgets .callout} adds class="callout".

Table of contents

Put [TOC] on its own line and it becomes a linked table of contents, one entry per heading:

[TOC]

# Introduction

# Details

Newly extracted themes render it as an indented list, each entry followed by a dotted leader and the page its heading lands on. The look is set by the .toc rules in your theme's style.css; the quickest knobs are --toc-leader (the leader: '.' dots, '-' dashes, ' ' blank, '' for none) and --toc-indent (per-level indent), which you can set in :root.

By default the contents lists every heading level. To limit it, set toc.levels in frontmatter — a single number for "up to that level", or a range:

---
toc:
  levels: 2-4    # or 3 for H1–H3
---

A single document can override the leader with toc.leader (same values as --toc-leader) — a one-off flourish without touching the theme. From the command line these are --set toc.levels=2-4 and --set toc.leader=-.

Numbered headings

If your Word template numbers its heading styles, the extracted theme carries that through — headings render as 1, 1.1, 1.1.1, the decimal outline Word would have shown.

You can also switch numbering on, off, or onto a different range per document with heading_numbers in frontmatter:

---
heading_numbers:
  levels: 2-4    # number H2–H4; a single number like 3 means H1–H3
  reset: 1       # optional: an H1 restarts the count under each section
---

levels picks which heading levels are numbered, and its low end is the root of the outline — 2-4 numbers H2 as 1, 2, … and leaves H1 as an unnumbered "big heading" (which pairs with the way the first H1 becomes the document title). Set it to none (or off/false/0) to switch a numbered template off for one document.

By default the count runs continuously, so an H1 between sections does not interrupt it. reset makes a higher level a section break that restarts the count — reset: 1 (or h1) gives each H1 its own 1, 2, …; the reset level must sit above the numbered range. A document setting always wins over the template's own numbering. From the command line: --set heading_numbers.levels=2-4 --set heading_numbers.reset=1.

Footnotes

A footnote renders at the foot of the page it is referenced on. Reference it with [^label] and define it anywhere in the document:

The claim rests on shaky ground[^pub].

[^pub]: Source: overheard at the pub.

The label is yours to name — pub, not a number. You reference and define by the same handle, so inserting or removing a footnote never makes you renumber anything by hand. The rendered marker is a plain number, counted in the order the footnotes appear.

A footnote is defined once and referenced once. Defining the same [^label] twice is an error — there is no telling which definition you meant — and referencing it twice is too, since one note shared across two places is not supported. Either way, render stops and names the label.

What gets extracted

From your Word template, mark-my-word extract pulls:

  • Page margins
  • Theme colours → CSS custom properties (--color-body, --color-heading, etc.)
  • Body text formatting: font family, size, line height, and paragraph spacing (from the Normal style)
  • Heading styles: font size, weight, colour, and spacing for H1–H6
  • Header and footer content, including tab-separated left/right layouts, page numbers, and paragraph borders (top and bottom)
  • Watermark images

The result is a plain directory you can inspect and edit. The stylesheet uses CSS custom properties throughout, so tweaking colours or fonts is a one-line change.

Theme directory layout

themes/my-theme/
  style.css           required
  header.html         header on all pages (or cont-header.html / first-header.html)
  footer.html         footer on all pages (or cont-footer.html / first-footer.html)
  watermark.png       optional full-page background image
  mark-my-word.json   what extracted it: template filename, date, version

If your Word template has a different header on the first page, extract writes first-header.html and cont-header.html separately. render handles both layouts automatically.

Tips for Word templates

Left/right footer layouts

For a footer with content on the left and right (e.g. company name on the left, page number on the right), use a single paragraph with a right-aligned tab stop rather than a table:

  1. In your footer, type the left content
  2. Press Tab
  3. Type the right content
  4. Set a right-aligned tab stop at the right margin (Format → Tabs, or drag the tab marker in the ruler)

mark-my-word converts the tab character into a flex-row layout. Tables work too, but they carry implicit cell spacing that doesn't always translate cleanly.

Horizontal rules

To draw a decorative line above or below footer content, add a paragraph border (Format → Borders and Shading → Borders tab) rather than an actual horizontal rule or a separate table row. Paragraph top and bottom borders are extracted and rendered faithfully.

Options

mark-my-word render --help
mark-my-word extract --help
mark-my-word theme --help

Changelog

0.9.0

  • Themes can be installed under a name: theme install copies a theme (or extracts a Word template) into ~/.local/share/mark-my-word/themes, theme link points at a directory you are still editing, and theme list/rename/remove manage what is there. --theme NAME then works from any directory, and says Using installed theme: NAME when it resolves one. A bare name means an installed theme; write ./name for a directory.
  • extract now writes a mark-my-word.json record into the theme — the template's filename, the date, and the version that built it, with no path in it. theme list shows the template and flags a theme an older version built, so you can see which themes want re-extracting.
  • Fixed: extracted themes now take their colours from the Word template. Theme colour extraction never worked — the parser reached for a _element the theme part does not have, and an over-broad except swallowed the error, so every theme silently got the built-in default palette. Re-extract your themes to pick up their real colours.
  • Fixed: a heading using Word's "automatic" colour produced invalid CSS. The generated stylesheet contained color: #NONE; for that heading level, so it fell back to the browser default rather than the theme colour. Such a heading now inherits the theme heading colour, as it always should have.

0.8.1

  • The flowed first-page title (0.8.0) started at the body top margin, leaving a large gap above it. extract now sets the first page's top margin to the Word template's header distance, so the title sits where the header used to. Re-extract your theme to pick this up.
  • mark-my-word --help (and the bare command) now shows the version.

0.8.0

  • Breaking: the first-page title now flows at the top of the body instead of sitting in a fixed-height page margin box. A title that wraps to two lines no longer clips off the top of the page — the header grows and pushes the body down. Re-extract your theme to pick this up; the change is in the generated style.css (.header-first loses position: running, and @page :first no longer reserves a header margin box).

0.7.0

  • Breaking: the output flag is --out (short -o) on both subcommands. extract's --output is gone; render keeps --out but drops the old --output alias.

0.6.1

  • CLI errors are now consistent and actionable. Running with no command, a mistyped path, or a contradictory set of flags fails with a usage line and a message that names the fix — not a stray "no such command" or a stack trace. A --theme directory must now exist and contain a style.css.

0.6.0

  • Numbered headings. A Word template with numbered heading styles now carries the numbering through to the PDF (1, 1.1, 1.1.1). heading_numbers.levels in frontmatter opts in, overrides the range, or switches it off per document, and heading_numbers.reset restarts the count at a section-break level. See Numbered headings.
  • Breaking: the table-of-contents settings moved under a toc mapping — toc.levels and toc.leader, where they were toc_levels and toc_leader. Nested settings reach the command line through dotted keys, e.g. --set toc.levels=2-4.

0.5.0

  • The [TOC] table of contents now shows the page each heading lands on, with a dotted leader between title and number. toc_levels in frontmatter limits which heading levels it lists (3 for H1–H3, or a range like 2-4), and toc_leader overrides the leader character per document.

0.4.0

  • Footnotes: [^label] references render at the foot of the page they appear on, with named labels you never renumber by hand and CSS-counted numbers. Reusing a label is an error.

0.3.0

  • Headings can carry a hand-picked anchor — # Long heading {#short} — so you can link to #short instead of the auto-generated slug. The same {...} syntax also sets CSS classes and other attributes.
  • [TOC] on its own line expands into a linked table of contents. Newly extracted themes style it as a plain indented list.
  • The Markdown engine moved from markdown2 to Python-Markdown — this is what makes the anchor and table-of-contents support possible. Existing documents render unchanged.

0.2.3

  • More style properties are now extracted from the Word template and applied to the PDF: body text font size, line height, and paragraph spacing (from the Normal style) join heading size, weight, colour, and spacing (H1–H6) as template-driven values rather than hardcoded defaults
  • Internal cleanup: removed unused helper functions

0.2.2

  • README updated with 0.2.1 changes (should have shipped with the release)

0.2.1

  • Paragraph borders (top and bottom) in Word headers and footers are now extracted and rendered with correct spacing
  • Empty paragraphs used as horizontal rules extract at natural line height, matching Word's visual spacing
  • Footer height estimation fixed: <p> elements inside flex-row cells are no longer counted as separate lines, preventing over-large body margins
  • Page break orphan/widow protection relaxed from 3 lines to 2

0.2.0

  • First-page header/footer fallback: if no distinct first-page fragments exist, the running header/footer is used on all pages
  • <!--more--> preamble separator support
  • {{variable: default}} syntax for fragment placeholders
  • H1 extracted as document title and removed from body when no title: frontmatter is set
  • --force flag to overwrite existing output files
  • Watermark extraction from Word VML <w:pict> elements

Links

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mark_my_word-0.9.0.tar.gz (513.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mark_my_word-0.9.0-py3-none-any.whl (42.0 kB view details)

Uploaded Python 3

File details

Details for the file mark_my_word-0.9.0.tar.gz.

File metadata

  • Download URL: mark_my_word-0.9.0.tar.gz
  • Upload date:
  • Size: 513.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mark_my_word-0.9.0.tar.gz
Algorithm Hash digest
SHA256 664af0252dcb7dfc408b92e13a851a0a83ad8a3692538bb3ff4dd3609ae39e7b
MD5 83b4956d4a179d3de50adf27fa73cff6
BLAKE2b-256 3dfe31096a183bd20c66148874d2a4d674b634e839604882c84a18be804296e6

See more details on using hashes here.

File details

Details for the file mark_my_word-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: mark_my_word-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 42.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mark_my_word-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4b203e24cdfb7fb014ed414ca4b17b53f818cef707b9843b22b20d8e455f873e
MD5 28213ad648aee20b8c6679fee718db85
BLAKE2b-256 5247fa3ef993ce87fe6898b257892e81cc2120f98137cc1581bb12acb58680a0

See more details on using hashes here.

Release history Release notifications | RSS feed

0.9.1

2 files

This release

0.9.0 This release

2 files

0.8.0

2 files

0.7.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page