Skip to content

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")

By default, links are reduced to their display text ([GitHub](https://github.com) → GitHub). Pass preserve_links=True to keep the full link syntax, including any title. Autolinks and bare URLs come out in angle brackets:

convert("[GitHub](https://github.com)", preserve_links=True)
# [GitHub](https://github.com)

convert("See https://github.com", preserve_links=True)
# See <https://github.com>

out = 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')")  # 𝚙𝚛𝚒𝚗𝚝('𝚑𝚒')

# Choose the style at runtime: "bold", "italic" or "bold_italic"
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 𝙗𝙤𝙡𝙙-𝙞𝙩𝙖𝙡𝙞𝙘
~~strikethrough~~ strikethrough (markers dropped)
==highlight== highlight (markers dropped)
`inline code` Unicode 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 (backticks stripped)
```fenced block``` Unicode 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 (fences stripped)
Four-space indented block Unicode 𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎 (indent stripped)
# H1 Bold uppercase Unicode + ━ borders
## H2–###### H6 Bold Unicode, no border
Title + ===== / ----- (setext) Same as # H1 / ## H2
[text](url) text (URL discarded)
[text][ref] + [ref]: url text (definition consumed)
<url> and bare URLs The URL, unstyled
![alt](url) alt text (URL discarded)
- item • item
- nested ‣ nested
- nested twice ◦ nested twice
1. item / 1) item 1. item (renumbered in sequence)
\| a \| b \| table Bold header, cells joined by \|
> blockquote > prefix removed, at every depth
--- / * * * thematic break Dropped
Two trailing spaces, <br> A line break
<span>...</span> Tags removed, text kept
<script> / <style> Dropped, content and all
<!-- comment --> Dropped
&amp; / &gt; etc. Decoded to & / >
\* backslash escapes Resolved to literal *
Emojis, accented chars Passed through unchanged
Digits inside bold Also converted (**123** → 𝟭𝟮𝟯)
Digits inside italic / bold-italic Unchanged: Unicode has no such digits
_snake_case_ in middle of word Not italicised

Two GitHub extensions are deliberately not enabled, because LinkedIn has nowhere to put their output: task-list checkboxes stay as the literal text [ ] / [x] in front of the bullet, and footnotes are read as ordinary link syntax, so text[^1] becomes text^1 and the [^1]: … definition is consumed. Strip both before converting if you need them gone.


How Conversion Works

convert() is a two-stage pipeline:

  1. comrak, Python bindings to a Rust CommonMark parser, parses the Markdown and renders it to HTML, with the tables, strikethrough, autolink and highlight extensions enabled.
  2. selectolax parses that HTML, and a small renderer walks the tree, replacing each element with its LinkedIn equivalent: Unicode styling for emphasis and code, bullets and numbers for lists, ━ rules around a top-level heading.

Nothing is ever rendered as HTML. The intermediate document only exists so that the renderer sees a structured tree instead of a stream of characters, so raw HTML in the input cannot inject anything into the output — its tags are read for their structure and dropped.

The practical consequence is that every construct is read the way CommonMark defines it, in context: a four-space indent is a code block rather than prose, a > inside a > is a second level of quoting, a [ref]: url definition is consumed rather than printed, and a thematic break inside a list item is a break rather than a bullet.


Handling Edge Cases

Nested Formatting

Emphasis nests structurally, so the styles combine: italic inside bold comes out bold-italic, while the rest of the outer span stays bold:

convert("***very important***")  # → 𝙫𝙚𝙧𝙮 𝙞𝙢𝙥𝙤𝙧𝙩𝙖𝙣𝙩
convert("**bold and *italic* inside**")  # → 𝗯𝗼𝗹𝗱 𝗮𝗻𝗱 𝙞𝙩𝙖𝙡𝙞𝙘 𝗶𝗻𝘀𝗶𝗱𝗲

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 parsed tree, not from the raw indentation, so a document indented by four spaces per level nests exactly like one indented by two. Conversely, an item indented one space past its predecessor stays its sibling, since CommonMark allows a top-level item up to three leading spaces.

convert("- Role\n  - Applications\n    - AI Launchpad")
# • Role
#   ‣ Applications
#     ◦ AI Launchpad

Ordered items are numbered in sequence from the list's first number, so 1., 1., 1. becomes 1., 2., 3. and a list that starts at 3. keeps counting from three. A ) delimiter becomes .. Bullets nested under an ordered item are indented to the width of its marker, so they line up under its text rather than under its number.

Everything else an item holds lines up the same way. A continuation paragraph, a wrapped line or a fenced code block inside an item is indented to the column the marker opened, and a loose list keeps the blank line between its items:

convert("- item\n\n  ```\n  a = 1\n  ```")
# • item
#
#   𝚊 = 𝟷

Code Is Rendered in Monospace

By default, inline code and code blocks are converted to Unicode Mathematical Monospace characters. Only ASCII letters and digits are mapped; everything else, including Markdown syntax like **, passes through unchanged:

convert("Use `**bold**` in Markdown")
# → Use **𝚋𝚘𝚕𝚍** in Markdown   (backticks stripped, letters monospaced, ** kept)

convert("```\nprint('hi')\n```")
# → 𝚙𝚛𝚒𝚗𝚝('𝚑𝚒')   (fences stripped, content monospaced)

To keep code as plain text instead:

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)
# → **not bold**               (fences stripped, content as plain text)

Either way the content itself is untouched: nothing inside a code span or block is read as Markdown, so the ** above survives.

Underscore Italic vs. Snake Case

Single underscores are italicised only when they sit at word boundaries:

convert("_italic_")  # → italic Unicode
convert("snake_case_variable")  # → unchanged

Unmatched / Orphaned Markers

Unmatched asterisks or underscores are left untouched — md2linkedin never crashes on malformed input:

convert("price: $10 * 3")  # → price: $10 * 3 (no crash)
convert("under_score alone")  # → unchanged

Large Documents

convert() does no I/O and holds only the document it is given, the HTML it renders from it, and the output. Parsing happens in Rust and C rather than in Python, and the cost scales linearly with input length, so it is safe to call on large files:

from pathlib import Path

big_md = Path("quarterly_report.md").read_text()
result = convert(big_md)

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)  # 𝗘𝘅𝗰𝗶𝘁𝗲𝗱 𝘁𝗼 𝘀𝗵𝗮𝗿𝗲 🎉 𝘤𝘩𝘦𝘤𝘬 𝘵𝘩𝘪𝘴 𝘰𝘶𝘵!

Integration with Pandoc (LaTeX → LinkedIn)

If you write in LaTeX (e.g. a résumé), you can pipe through pandoc first:

pandoc --from=latex --to=markdown_strict --wrap=none resume.tex | md2linkedin

Or in Python:

import subprocess
from pathlib import Path

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

Run md2linkedin --help for the full text.

Usage: md2linkedin [OPTIONS] [INPUT_FILE]

Options:
  -V, --version        Show the version and exit.
  -o, --output FILE    Output file path. Defaults to INPUT_FILE with a
                       '.linkedin.txt' extension. Ignored when reading from
                       stdin.
  --preserve-links     Keep Markdown link syntax ([text](url)) in the output
                       instead of stripping URLs.
  --no-monospace-code  Disable monospace Unicode rendering for inline code and
                       fenced code blocks. When set, code is kept as plain
                       text, with the backticks and fences stripped.
  -h, --help           Show this message and exit.