Hebrew Markdown → DOCX
Converts Hebrew (or mixed Hebrew/English) Markdown to a clean, RTL-aware .docx.
Why this skill exists
Pasting Markdown into Word/Google Docs forces the user to manually right-align
every paragraph, left-align the English ones, and still leaves bugs: inline code
like `a = b` reorders to b = a, numbered markers (1. ) render LTR and
detach from their Hebrew item, parentheses/quotes around English drift to the
wrong side, and a Hebrew paragraph that starts with an English word gets aligned
the wrong way. This skill fixes all of that at the OOXML level.
How to use
The converter is md_to_docx_he.py (Python 3; needs python-docx and mistune):
pip install python-docx mistune --break-system-packages # if not already present
python md_to_docx_he.py INPUT.md -o OUTPUT.docx
Options:
--template BLANK.docx – build on a template so the output matches a specific
set of Word styles/fonts (see below).
--code-font Consolas – monospace font for the Code style. Default Consolas.
--overrides overrides.json – manual direction fixes for ambiguous paragraphs.
Templates (matching the user's Word styles)
A .docx always carries its own styles, so the output's look (fonts, heading
styles, spacing) comes from the template it is built on — not from the user's
Normal.dotm at open time. Three levels:
- Bundled default – if
template.docx sits next to the script, it is used
automatically. The bundled one here matches the user's Word defaults
(Aptos + Arial for Hebrew).
- Per-run override – pass
--template path.docx to use a different one.
- Stock – with no template available, python-docx's standard default
(Calibri) is used.
A template should be an (essentially) empty document saved from the desired Word
template. The converter is template-agnostic: it supplies its own list numbering
and table borders, and only relies on Normal, Heading 1..9, and Quote,
which every Word template has — so any template works without missing-style errors.
Running this skill
Just run the converter with no --template flag — the bundled default is used
automatically, no need to ask the user first. Only pass --template path.docx
if the user explicitly asks for a different template (e.g. "use my company's
Word template" or provides their own .docx to match).
Direction logic (heuristic first, AI for ties)
Each paragraph's base direction is chosen by the majority of strong directional
words (Hebrew vs Latin), not by the first character — so "Claude הוא כלי בינה
מלאכותית" is correctly RTL. When the counts are near-even (within 20%) the script
picks a direction but prints the paragraph to stderr as ambiguous:
Words, not characters. Counting words (maximal runs of one script) instead
of characters keeps a Hebrew paragraph RTL even when its English is a few long or
repeated proper nouns. For example "אנחנו נשתמש ב-Visual Studio Community –
המהדורה החינמית והמלאה של Visual Studio…" has slightly more Latin letters than
Hebrew (long, repeated "Visual Studio"), which would wrongly flip it LTR by
character count; by word count the Hebrew prose clearly wins, so it stays RTL.
URLs and e-mails are excluded. A URL or e-mail address is data, not prose, and
its characters must not sway direction. They are stripped before counting, so a
labelled link line like "הורדה: https://visualstudio.microsoft.com/vs/…" is
driven by the Hebrew label and stays RTL instead of being dragged LTR by the URL.
Headings are a further exception. A single heading's own text is not a
reliable signal — e.g. ### הצהרה (declaration) is a Hebrew heading with an
English gloss. So each heading level (H1, H2, H3, ...) gets one direction,
chosen by the aggregate Hebrew/Latin word count across every heading at that
level in the document. All headings at that level use it, even ones whose own text
would individually score the other way. This assumes each level is predominantly
one language; if a document genuinely mixes fully-Hebrew and fully-English
headings at the same level, use --overrides to fix the minority ones by
paragraph index.
Lists share one direction, like heading levels. All items in a list get a
single base direction from the aggregate word count of the whole list, so short
items that individually score LTR — a Hebrew label followed by a technical term,
e.g. "מקש Ctrl+F5" or "תפריט Debug ▸ Start Without Debugging" — align RTL with
their Hebrew siblings instead of coming out ragged. A genuinely mixed list can be
corrected per item with --overrides.
2 ambiguous paragraph(s) ... Resolve via --overrides (index -> rtl/ltr):
[118] used=rtl: 'scanf אוגרת הקלדות: ה-buffer'
Read those paragraphs, decide which language is primary, and pass an overrides
file keyed by the printed index:
{ "118": "rtl", "169": "ltr" }
then re-run with --overrides overrides.json. Most documents need none.
How direction is represented (matches Word's own output)
- Paragraph direction via
w:bidi; no w:jc — an RTL paragraph then defaults
to right alignment, an LTR one to left, exactly as Word writes it.
- Only the actual Hebrew blocks (
U+0590–U+05FF, U+FB1D–U+FB4F) count as RTL.
Symbols such as ▸, →, – and … are neutral and take their direction from
their neighbours, so a menu path like Build ▸ Build Solution reads as one
left-to-right unit. (The script spells those ranges as \uXXXX escapes on
purpose – written as literal characters, U+FB1D decomposes under Unicode
normalisation and silently widens the class to U+05B4–U+FB4F, which swallows
arrows, dashes and CJK.)
- Only Hebrew (RTL) runs get
w:rtl; English/number/code runs omit it.
- Neutral characters (spaces, parens, quotes, dashes, punctuation) fall back to
the paragraph's own base direction unless both neighbours are the opposite
direction. Under an RTL paragraph a neutral is LTR only when it lies between
two LTR characters (so "format string" stays together, while the
parens/quotes/dashes around English stay on the Hebrew side). Under an LTR
paragraph the rule mirrors: a neutral is RTL only between two RTL characters,
so the period ending a mostly-English sentence that happens to close with a
Hebrew word – "This one, however, is 10% עברית." – stays at the sentence's end
instead of jumping into the Hebrew flow. Digits are LTR so numbers like 1920
never reverse.
- "Word-joiner" symbols (
+, #, *, ., \, /, !, ?, %) that touch a
Latin letter/digit are kept on the Latin side, so tech tokens and embedded
English phrases stay intact: C++/C# don't flip to ++C/#C; .NET,
Node.js, *.txt, 3.14 hold together; paths/URLs like C:\Users\foo,
/home/assaf, https://x/y read left-to-right as one unit; and the !/? of
an embedded English exclamation stays put ("Hello, World!" keeps its !). A
joiner sitting at the boundary between the Latin run and the surrounding Hebrew
is anchored with an invisible LRM (U+200E) so the bidi algorithm can't drag it
across. A joiner that touches no Latin character (a lone + between Hebrew
words, / in a Hebrew כן/לא, the ? in a Hebrew נתקעתם?) is left alone.
The dot is a special case: because it is also the sentence period, it joins a
token only when a Latin letter/digit *follows* it (as in .NET, file.txt), so
a period ending a Hebrew sentence — even after an English word or number, like
Visual Studio. or 123. — correctly stays in the RTL flow.
The percent sign is the mirror case: it always trails its number and never
leads it, so it joins only when a Latin letter/digit *precedes* it. That keeps
80% / 27% intact instead of rendering as %80 / %27, and it puts the
digits and the sign in one LTR run so they also share one font. A lone % with
no adjacent digit or letter keeps the normal neutral treatment; ‰ behaves the
same way. It also joins *forward* to a directly-adjacent Latin letter/digit, so
a printf specifier written outside backticks (%d, %s, %5.2f) holds
together too.
! and ? need the same care, because they end an embedded English phrase
*and* the surrounding Hebrew sentence. They attach to the Latin token only when
something immediately follows them – a closing quote or bracket, or more Latin
("Hello, World!", (Really?)). When the next character is a space or the text
ends there, the mark is terminating the Hebrew sentence and stays in the RTL
flow, so "אין לכם Windows? היחידה עדיין בשבילכם" puts the ? on the Hebrew side
of "Windows", not the English one.
- An ellipsis (
... or a spaced . . .) next to an English phrase stays with it
("…this window . . ." keeps the dots at the end); a Hebrew-side ellipsis is
left in the RTL flow.
- Inline code gets the same anchoring. Marking a code run LTR is not enough on
its own: a neutral character at the run's edge is still resolved by the
paragraph's base direction, so
`%d` inside a Hebrew paragraph came out as
d%. A code run whose first or last character is not strongly LTR is bracketed
with LRMs, which also protects a leading `.NET` / `*.txt` and a
trailing `--flag-`.
- The LRM anchor is the only inserted control character — zero-width, it
never renders as a glyph; nothing else visible is added.
Links
A Markdown link [text](url) — and a bare URL that the parser auto-links — becomes
a real, clickable Word hyperlink (an external relationship plus the built-in
Hyperlink character style, blue and underlined, created if the template lacks
it). The visible text keeps normal bidi splitting, so a URL stays LTR while a
Hebrew link label stays RTL. A labelled line like **הורדה**: https://… is driven
RTL by the Hebrew label (URLs are excluded from direction counting, see above)
while the URL itself is left-to-right and clickable.
Images
A standalone image ( on its own line) is embedded
as a centered picture. If it is wider than the page's content area (page width
minus margins) it is scaled down proportionally to fit; smaller images keep their
native size. The alt text becomes a centered caption directly below the image,
using Word's Caption paragraph style (a smaller italic style, created
automatically if the template lacks one). Caption direction (RTL/LTR) follows the
same Hebrew/Latin majority rule as body text.
Image src paths are resolved relative to the input .md file's folder, so a
reference like Assets/setup.png works as long as the assets sit beside the
Markdown. If a file can't be found or read, a visible [missing image: …]
placeholder is inserted and the problem is reported to stderr instead of aborting.
What gets converted
| Markdown |
Word output |
#/##/### headings |
Heading 1..9, direction per content |
| bold, italic |
bold / italic runs |
`inline code` |
Code character style (Normal + monospace), kept LTR, edges LRM-anchored |
``` fenced blocks |
LTR monospace lines (Code style) |
| tables |
RTL table (bidiVisual) with its own borders, bold header row |
> blockquotes |
built-in Quote style |
1. / - lists |
self-contained numbering (each list restarts at 1), one shared direction per list, bullets render in both directions |
[text](url) / bare URLs |
clickable Word hyperlink (Hyperlink style, blue + underlined) |
 images |
centered picture (scaled to fit page width), alt text as a centered Caption below it |
Limitations
- Nested / multi-level lists render at a single level.
- Raw inline HTML is skipped.
- Only standalone images (a
 on its own line) become centered image
blocks with captions. An image sitting inline within a text paragraph falls back
to rendering just its alt text. Image paths are resolved relative to the input
.md file's folder; a missing/unreadable image is replaced with a visible
[missing image: …] placeholder and reported to stderr.
- Tuned for Hebrew-dominant content; primarily-English docs still convert but the
base direction is RTL.
1---2name: hebrew-md-to-docx3description: Convert a Markdown file that contains Hebrew (usually mixed Hebrew/English) into a properly formatted, RTL-aware Word .docx. Use whenever the user asks to turn a Hebrew or mixed Hebrew/English .md file into a Word document, or complains that pasting Markdown into Word/Google Docs breaks right-to-left alignment, flips inline code, or mangles numbered lists. Handles headings, bold/italic, inline code, fenced code blocks, tables, blockquotes, ordered/unordered lists, clickable hyperlinks, and centered images with captions, and sets per-paragraph text direction automatically.4---56# Hebrew Markdown → DOCX78Converts Hebrew (or mixed Hebrew/English) Markdown to a clean, RTL-aware `.docx`.910## Why this skill exists1112Pasting Markdown into Word/Google Docs forces the user to manually right-align13every paragraph, left-align the English ones, and still leaves bugs: inline code14like `` `a = b` `` reorders to `b = a`, numbered markers (`1. `) render LTR and15detach from their Hebrew item, parentheses/quotes around English drift to the16wrong side, and a Hebrew paragraph that starts with an English word gets aligned17the wrong way. This skill fixes all of that at the OOXML level.1819## How to use2021The converter is `md_to_docx_he.py` (Python 3; needs `python-docx` and `mistune`):2223```24pip install python-docx mistune --break-system-packages # if not already present25python md_to_docx_he.py INPUT.md -o OUTPUT.docx26```2728Options:2930- `--template BLANK.docx` – build on a template so the output matches a specific31 set of Word styles/fonts (see below).32- `--code-font Consolas` – monospace font for the `Code` style. Default `Consolas`.33- `--overrides overrides.json` – manual direction fixes for ambiguous paragraphs.3435## Templates (matching the user's Word styles)3637A `.docx` always carries its own styles, so the output's look (fonts, heading38styles, spacing) comes from the template it is built on — not from the user's39`Normal.dotm` at open time. Three levels:40411. **Bundled default** – if `template.docx` sits next to the script, it is used42 automatically. The bundled one here matches the user's Word defaults43 (Aptos + Arial for Hebrew).442. **Per-run override** – pass `--template path.docx` to use a different one.453. **Stock** – with no template available, python-docx's standard default46 (Calibri) is used.4748A template should be an (essentially) empty document saved from the desired Word49template. The converter is template-agnostic: it supplies its own list numbering50and table borders, and only relies on `Normal`, `Heading 1..9`, and `Quote`,51which every Word template has — so any template works without missing-style errors.5253### Running this skill5455Just run the converter with no `--template` flag — the bundled default is used56automatically, no need to ask the user first. Only pass `--template path.docx`57if the user explicitly asks for a different template (e.g. "use my company's58Word template" or provides their own .docx to match).5960## Direction logic (heuristic first, AI for ties)6162Each paragraph's base direction is chosen by the **majority of strong directional63words** (Hebrew vs Latin), not by the first character — so "Claude הוא כלי בינה64מלאכותית" is correctly RTL. When the counts are near-even (within 20%) the script65picks a direction but prints the paragraph to stderr as **ambiguous**:6667**Words, not characters.** Counting *words* (maximal runs of one script) instead68of characters keeps a Hebrew paragraph RTL even when its English is a few long or69repeated proper nouns. For example "אנחנו נשתמש ב-Visual Studio Community –70המהדורה החינמית והמלאה של Visual Studio…" has slightly more Latin *letters* than71Hebrew (long, repeated "Visual Studio"), which would wrongly flip it LTR by72character count; by word count the Hebrew prose clearly wins, so it stays RTL.7374**URLs and e-mails are excluded.** A URL or e-mail address is data, not prose, and75its characters must not sway direction. They are stripped before counting, so a76labelled link line like "**הורדה**: https://visualstudio.microsoft.com/vs/…" is77driven by the Hebrew label and stays RTL instead of being dragged LTR by the URL.7879**Headings are a further exception.** A single heading's own text is not a80reliable signal — e.g. `### הצהרה (declaration)` is a Hebrew heading with an81English gloss. So each heading *level* (H1, H2, H3, ...) gets one direction,82chosen by the aggregate Hebrew/Latin **word** count across every heading at that83level in the document. All headings at that level use it, even ones whose own text84would individually score the other way. This assumes each level is predominantly85one language; if a document genuinely mixes fully-Hebrew and fully-English86headings at the same level, use `--overrides` to fix the minority ones by87paragraph index.8889**Lists share one direction, like heading levels.** All items in a list get a90single base direction from the aggregate word count of the whole list, so short91items that individually score LTR — a Hebrew label followed by a technical term,92e.g. "מקש Ctrl+F5" or "תפריט Debug ▸ Start Without Debugging" — align RTL with93their Hebrew siblings instead of coming out ragged. A genuinely mixed list can be94corrected per item with `--overrides`.9596```972 ambiguous paragraph(s) ... Resolve via --overrides (index -> rtl/ltr):98 [118] used=rtl: 'scanf אוגרת הקלדות: ה-buffer'99```100101Read those paragraphs, decide which language is primary, and pass an overrides102file keyed by the printed index:103104```json105{ "118": "rtl", "169": "ltr" }106```107108then re-run with `--overrides overrides.json`. Most documents need none.109110## How direction is represented (matches Word's own output)111112- Paragraph direction via `w:bidi`; **no `w:jc`** — an RTL paragraph then defaults113 to right alignment, an LTR one to left, exactly as Word writes it.114- Only the actual Hebrew blocks (`U+0590–U+05FF`, `U+FB1D–U+FB4F`) count as RTL.115 Symbols such as `▸`, `→`, `–` and `…` are neutral and take their direction from116 their neighbours, so a menu path like `Build ▸ Build Solution` reads as one117 left-to-right unit. (The script spells those ranges as `\uXXXX` escapes on118 purpose – written as literal characters, `U+FB1D` decomposes under Unicode119 normalisation and silently widens the class to `U+05B4–U+FB4F`, which swallows120 arrows, dashes and CJK.)121- Only Hebrew (RTL) runs get `w:rtl`; English/number/code runs omit it.122- Neutral characters (spaces, parens, quotes, dashes, punctuation) fall back to123 the **paragraph's own base direction** unless both neighbours are the opposite124 direction. Under an RTL paragraph a neutral is LTR only when it lies between125 two LTR characters (so "format string" stays together, while the126 parens/quotes/dashes around English stay on the Hebrew side). Under an LTR127 paragraph the rule mirrors: a neutral is RTL only between two RTL characters,128 so the period ending a mostly-English sentence that happens to close with a129 Hebrew word – "This one, however, is 10% עברית." – stays at the sentence's end130 instead of jumping into the Hebrew flow. Digits are LTR so numbers like 1920131 never reverse.132- "Word-joiner" symbols (`+`, `#`, `*`, `.`, `\`, `/`, `!`, `?`, `%`) that touch a133 Latin letter/digit are kept on the Latin side, so tech tokens and embedded134 English phrases stay intact: `C++`/`C#` don't flip to `++C`/`#C`; `.NET`,135 `Node.js`, `*.txt`, `3.14` hold together; paths/URLs like `C:\Users\foo`,136 `/home/assaf`, `https://x/y` read left-to-right as one unit; and the `!`/`?` of137 an embedded English exclamation stays put (`"Hello, World!"` keeps its `!`). A138 joiner sitting at the boundary between the Latin run and the surrounding Hebrew139 is anchored with an invisible LRM (`U+200E`) so the bidi algorithm can't drag it140 across. A joiner that touches no Latin character (a lone `+` between Hebrew141 words, `/` in a Hebrew `כן/לא`, the `?` in a Hebrew `נתקעתם?`) is left alone.142 The dot is a special case: because it is also the sentence period, it joins a143 token only when a Latin letter/digit *follows* it (as in `.NET`, `file.txt`), so144 a period ending a Hebrew sentence — even after an English word or number, like145 `Visual Studio.` or `123.` — correctly stays in the RTL flow.146 The percent sign is the mirror case: it always trails its number and never147 leads it, so it joins only when a Latin letter/digit *precedes* it. That keeps148 `80%` / `27%` intact instead of rendering as `%80` / `%27`, and it puts the149 digits and the sign in one LTR run so they also share one font. A lone `%` with150 no adjacent digit or letter keeps the normal neutral treatment; `‰` behaves the151 same way. It also joins *forward* to a directly-adjacent Latin letter/digit, so152 a printf specifier written outside backticks (`%d`, `%s`, `%5.2f`) holds153 together too.154 `!` and `?` need the same care, because they end an embedded English phrase155 *and* the surrounding Hebrew sentence. They attach to the Latin token only when156 something immediately follows them – a closing quote or bracket, or more Latin157 (`"Hello, World!"`, `(Really?)`). When the next character is a space or the text158 ends there, the mark is terminating the Hebrew sentence and stays in the RTL159 flow, so "אין לכם Windows? היחידה עדיין בשבילכם" puts the `?` on the Hebrew side160 of "Windows", not the English one.161- An ellipsis (`...` or a spaced `. . .`) next to an English phrase stays with it162 (`"…this window . . ."` keeps the dots at the end); a Hebrew-side ellipsis is163 left in the RTL flow.164- Inline code gets the same anchoring. Marking a code run LTR is not enough on165 its own: a *neutral* character at the run's edge is still resolved by the166 paragraph's base direction, so `` `%d` `` inside a Hebrew paragraph came out as167 `d%`. A code run whose first or last character is not strongly LTR is bracketed168 with LRMs, which also protects a leading `` `.NET` `` / `` `*.txt` `` and a169 trailing `` `--flag-` ``.170- The LRM anchor is the only inserted control character — zero-width, it171 never renders as a glyph; nothing else visible is added.172173## Links174175A Markdown link `[text](url)` — and a bare URL that the parser auto-links — becomes176a real, clickable Word hyperlink (an external relationship plus the built-in177`Hyperlink` character style, blue and underlined, created if the template lacks178it). The visible text keeps normal bidi splitting, so a URL stays LTR while a179Hebrew link label stays RTL. A labelled line like `**הורדה**: https://…` is driven180RTL by the Hebrew label (URLs are excluded from direction counting, see above)181while the URL itself is left-to-right and clickable.182183## Images184185A standalone image (`` on its own line) is embedded186as a centered picture. If it is wider than the page's content area (page width187minus margins) it is scaled down proportionally to fit; smaller images keep their188native size. The **alt text becomes a centered caption** directly below the image,189using Word's `Caption` paragraph style (a smaller italic style, created190automatically if the template lacks one). Caption direction (RTL/LTR) follows the191same Hebrew/Latin majority rule as body text.192193Image `src` paths are resolved relative to the input `.md` file's folder, so a194reference like `Assets/setup.png` works as long as the assets sit beside the195Markdown. If a file can't be found or read, a visible `[missing image: …]`196placeholder is inserted and the problem is reported to stderr instead of aborting.197198## What gets converted199200| Markdown | Word output |201| --- | --- |202| `#`/`##`/`###` headings | `Heading 1..9`, direction per content |203| **bold**, *italic* | bold / italic runs |204| `` `inline code` `` | `Code` character style (Normal + monospace), kept LTR, edges LRM-anchored |205| ```` ``` ```` fenced blocks | LTR monospace lines (`Code` style) |206| tables | RTL table (`bidiVisual`) with its own borders, bold header row |207| `>` blockquotes | built-in `Quote` style |208| `1.` / `-` lists | self-contained numbering (each list restarts at 1), one shared direction per list, bullets render in both directions |209| `[text](url)` / bare URLs | clickable Word hyperlink (`Hyperlink` style, blue + underlined) |210| `` images | centered picture (scaled to fit page width), alt text as a centered `Caption` below it |211212## Limitations213214- Nested / multi-level lists render at a single level.215- Raw inline HTML is skipped.216- Only standalone images (a `` on its own line) become centered image217 blocks with captions. An image sitting inline within a text paragraph falls back218 to rendering just its alt text. Image paths are resolved relative to the input219 `.md` file's folder; a missing/unreadable image is replaced with a visible220 `[missing image: …]` placeholder and reported to stderr.221- Tuned for Hebrew-dominant content; primarily-English docs still convert but the222 base direction is RTL.