Advanced Usage
Programmatic Use
Converting Strings in Memory
The simplest entry point is convert(), which accepts a Markdown string and
returns a plain-text string ready for LinkedIn:
from md2linkedin import convert
result = convert("**Hello**, *world*!")
print(result) # 𝗛𝗲𝗹𝗹𝗼, 𝘸𝘰𝘳𝘭𝘥!
Converting Files
Use convert_file() to read a .md file and write the output to a .txt
file. The function returns the Path of the written file so you can chain
further operations:
from md2linkedin import convert_file
out = convert_file("post.md")
print(f"Written to: {out}") # post.linkedin.txt
# Override the output path
out = convert_file("post.md", "my_post.txt")
Stripping and Preserving Links
By default, Markdown link syntax is stripped to display text only
([GitHub](https://github.com) → GitHub). This is useful for removing clickable URLs from your output, keeping only the descriptive text.
Pass preserve_links=True to retain the full link syntax:
result = convert("[GitHub](https://github.com)", preserve_links=True)
# [GitHub](https://github.com)
result = convert_file("post.md", preserve_links=True)
Direct Unicode Mapping
The Unicode mapping functions are public and useful for applying a specific style to a plain string programmatically:
from md2linkedin import (
to_sans_bold,
to_sans_italic,
to_sans_bold_italic,
to_monospace,
apply_style,
)
to_sans_bold("Open to Work") # 𝗢𝗽𝗲𝗻 𝘁𝗼 𝗪𝗼𝗿𝗸
to_sans_italic("3 years of exp") # 3 𝘺𝘦𝘢𝘳𝘴 𝘰𝘧 𝘦𝘹𝘱
to_sans_bold_italic("Key insight") # 𝙆𝙚𝙮 𝙞𝙣𝙨𝙞𝙜𝙝𝙩
to_monospace("print('hi')") # 𝚙𝚛𝚒𝚗𝚝('𝚑𝚒')
# Dynamic dispatch
apply_style("Hiring!", "bold")
What Gets Transformed (and What Doesn't)
| Markdown construct | Output |
|---|---|
**bold** / __bold__ |
Unicode 𝗯𝗼𝗹𝗱 |
*italic* / _italic_ |
Unicode 𝘪𝘵𝘢𝘭𝘪𝘤 |
***bold-italic*** / ___bold-italic___ |
Unicode 𝙗𝙤𝙡𝙙-𝙞𝙩𝙖𝙡𝙞𝙘 |
`inline code` |
Unicode 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 (backticks stripped) |
```fenced block``` |
Unicode 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 (fences stripped) |
# H1 |
Bold Unicode + ━ border |
## H2–###### H6 |
Bold Unicode, no border |
[text](url) |
text (URL discarded) |
 |
alt text (URL discarded) |
- item |
• item |
- nested |
‣ nested |
- nested twice |
◦ nested twice |
> blockquote |
> prefix removed |
<span>...</span> |
Tags removed, text kept |
& / > etc. |
Decoded to & / > |
\* backslash escapes |
Resolved to literal * |
| Emojis, accented chars | Passed through unchanged |
| Digits inside bold | Also converted (**123** → 𝟭𝟮𝟯) |
_snake_case_ in middle of word |
Not italicised |
Handling Edge Cases
Nested Formatting
md2linkedin handles nesting by processing bold-italic (***) first,
ensuring it is not accidentally consumed piecemeal:
convert("***very important***") # → bold-italic Unicode
convert("**bold and *italic* inside**") # → bold wrapping italic
Nested Bullet Lists
Each nesting level gets its own marker — •, ‣, ◦, ▪ — and two spaces
of indentation. Levels deeper than the fourth reuse ▪, with the indentation
still growing by two spaces per level.
Depth is counted from the enclosing list items, not from the raw indentation, so a document indented by four spaces per level nests exactly like one indented by two. Nesting takes at least two extra spaces: an item indented by only one space past its predecessor stays its sibling, since Markdown allows a top-level item up to three leading spaces.
A list ends at a heading (ATX or setext), thematic break, blockquote or fenced
code block at column zero. An item after one of those restarts its nesting from
its own indentation. A thematic break is a break even where a list item would
also fit, so a spaced * * * is dropped rather than read as a bullet.
A blank line followed by a paragraph ends the levels that paragraph is not indented inside: one at column zero closes the whole list, while an indented one is a second paragraph of some enclosing item and closes only the list nested within that item.
Ordered markers (1.) are kept verbatim, since the numbers already convey
order, but they are re-indented like bullets and they open a level for any
bullets nested under them. In the middle of a paragraph, only 1. may start
a list, so a line such as 2. prose there is treated as the prose it is and
opens no level. Under a heading or a blank line there is no paragraph to
interrupt, so any number starts a list. Carrying on a list that is already
numbered interrupts nothing either, so 2. under 1. stays an item however
much text the first item holds — but 2. standing where a bullet item
stands is starting a new list, and so is prose.
Markdown Parsing Fidelity
md2linkedin is a pipeline of regular expressions, not a CommonMark parser.
That keeps it fast and dependency-free, and it converts the Markdown people
actually write for LinkedIn — headings, emphasis, code, links, lists — exactly
as expected. What it cannot do is resolve constructs whose meaning depends on
their surroundings the way a real parser does, because it never builds a
document tree to resolve them against.
The known gaps, none of which is on the roadmap to fix with more regular expressions:
| Input | Output | A CommonMark parser would |
|---|---|---|
print(1) (four-space indent) |
passed through as plain text | render it as a code block |
>> inner |
> inner — one level stripped |
strip both levels |
[d]: https://example.com |
left in the output verbatim | consume the definition |
- a then * * * |
the break is mangled to * |
read it as item content |
- x then 2. y beside it |
2. y keeps its source indent |
start a new ordered list there |
1. x then 2) y |
2) carries the 1. list on |
start a second list, ) being a different type |
\| a \| b \| table rows |
passed through as pipe syntax | render the table |
The last two are the same shortcoming twice over: a list marker is read on its own, while a parser reads it against the kind of list it lands next to. A marker that changes the kind of list ends the one above it and opens another, whatever its number, because it is no longer interrupting that list's paragraph.
List nesting is also measured in indentation width rather than in each item's content column, so a document that mixes marker widths inside one list can nest a level differently from a parser. Depth is counted from the enclosing items, which handles every uniform style (two-space, four-space, tabs).
Fixing these properly means parsing Markdown properly. If that becomes worth the dependency, the conversion would be better expressed as a renderer over a parsed syntax tree than as more passes over the text.
Code Is Rendered in Monospace
By default, inline code and fenced code blocks are converted to Unicode
Mathematical Monospace characters. Only ASCII letters and digits are mapped;
all other characters (including Markdown syntax like **) pass through
unchanged — no nested processing is performed:
convert("Use `**bold**` in Markdown")
# → Use **𝚋𝚘𝚕𝚍** in Markdown (backticks stripped, letters monospaced, ** kept)
convert("```\nprint('hi')\n```")
# → 𝚙𝚛𝚒𝚗𝚝('𝚑𝚒') (fences stripped, content monospaced)
To disable monospace rendering and restore the previous plain-text behavior:
convert("Use `**bold**` in Markdown", monospace_code=False)
# → Use **bold** in Markdown (backticks stripped, content as plain text)
convert("```\n**not bold**\n```", monospace_code=False)
# → ```\n**not bold**\n``` (fenced block fully preserved)
Underscore Italic vs. Snake Case
Single underscores are italicised only when they sit at word boundaries:
Unmatched / Orphaned Markers
Unmatched asterisks or underscores are left untouched — md2linkedin never
crashes on malformed input:
Large Documents
convert() is a pure-Python, single-pass function with no I/O. It scales
linearly with input length and is safe to call in hot paths or on large files:
Emojis and Non-ASCII Characters
Emojis and accented characters pass through unchanged — only ASCII letters and digits are remapped to Unicode Mathematical variants:
from md2linkedin import convert
result = convert("**Excited to share** 🎉 *check this out*!")
print(result) # 𝗘𝘅𝗰𝗶𝘁𝗲𝗱 𝘁𝗼 𝘀𝗵𝗮𝗿𝗲 🎉 𝘤𝘩𝘦𝘤𝘬 𝘵𝘩𝘪𝘴 𝘰𝘂𝘵!
Running Without Installing (uvx)
If you only need md2linkedin occasionally, you can run it directly with
uvx without a permanent
installation:
# Convert a Markdown file (output: post.linkedin.txt)
uvx md2linkedin post.md
# Pipe from stdin
echo "**bold** and *italic*" | uvx md2linkedin
# Preserve link syntax
uvx md2linkedin --preserve-links post.md
# Explicit output path
uvx md2linkedin post.md -o linkedin_post.txt
# Disable monospace code rendering
uvx md2linkedin --no-monospace-code post.md
uvx downloads and caches the package in an isolated environment, so
subsequent runs are fast.
Integration with Pandoc (LaTeX → LinkedIn)
If you write in LaTeX (e.g. a résumé), you can pipe through pandoc first:
Or in Python:
import subprocess
from md2linkedin import convert
result = subprocess.run(
["pandoc", "--from=latex", "--to=markdown_strict", "--wrap=none"],
input=Path("resume.tex").read_text(),
capture_output=True,
text=True,
check=True,
)
linkedin_text = convert(result.stdout)
CLI Reference
Usage: md2linkedin [OPTIONS] [INPUT_FILE]
Convert Markdown to LinkedIn-friendly Unicode text.
Options:
-o, --output PATH Output file path.
--preserve-links Keep link syntax in output.
--no-monospace-code Disable monospace Unicode rendering for code.
-V, --version Show the version and exit.
-h, --help Show this message and exit.