Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Changed
- Requires Python 3.12 or newer; support for Python 3.10 and 3.11 is discontinued.
[0.4.0] — 2026-09-20
Fixed
- Bullets nested three or more levels deep keep their nesting. Every item
indented by at least two spaces was rendered as
‣, so a grandchild item collapsed onto its parent's level. Each level now gets its own marker (•,‣,◦,▪) and two spaces of indentation (fixes #68).
Changed
- Bullet nesting depth is counted from the enclosing list items instead of
from the raw indentation width, so a document indented by four spaces per
level nests exactly like one indented by two. Output indentation is
normalized to two spaces per level. Nesting takes at least two extra
spaces, so an item indented by only one space past its predecessor stays
its sibling, as Markdown allows a top-level item up to three leading
spaces. Ordered markers (
1.) are still kept verbatim, but they are re-indented like bullets and they now open a level for bullets nested under them — except in the middle of a paragraph, where Markdown only lets a list start at number one, so2. prosethere is left as the prose it is instead of nesting what follows it one level too deep. - A heading (ATX or setext), thematic break, blockquote or fenced code block at column zero ends an open list, as Markdown says it does, so an item after one restarts its nesting from its own indentation instead of continuing the list above. None of them is a paragraph either, so an ordered list may start under one whatever its first number is.
- A spaced thematic break (
* * *,- - -) is no longer converted into a bullet holding the remaining markers. Markdown reads such a line as a break wherever a list item would also fit, so it is now dropped like the compact***form. The*and_spellings are dropped before the emphasis steps run, which used to pair up the first two markers of* * *and leave a stray one behind. - A paragraph after a blank line now ends only the list levels it is not indented inside, instead of leaving every level open unless the paragraph starts at column zero. A second paragraph of an outer item therefore ends the list nested in that item, and the next item there nests one level deep rather than two.
- A numbered item only carries on a list that is itself numbered. A
2.standing where a bullet item stands no longer opens a phantom level that pushed the items under it one level too deep; it is left as the text it is. CommonMark would start a second list there instead, which is a known gap rather than the intended reading (see #70). - A tab may separate a list marker from its content (
-\titem,1.\titem), as CommonMark allows. Only a space was recognised before, so a tab-formatted list was left unconverted and its ordered items did not open a level for the bullets nested under them. - Documents holding many lines that begin with a number convert in linear
rather than quadratic time. Deciding whether such a line interrupts a
paragraph re-read everything above it, so a paragraph followed by 1,600
2. proselines took about 0.29 seconds and doubling the count roughly quadrupled that; the same document now converts in a few milliseconds.
Documentation
- Added a
Markdown parsing fidelitysection to the documentation, and a matchingLimitationsentry in the README, recording the constructs a pipeline of regular expressions reads more loosely than a CommonMark parser: indented code blocks, nested blockquotes, link reference definitions, a thematic break indented inside a list item, tables, and list markers that change the kind of list. Each was checked against a reference parser, and #70 tracks the rework that would fix them.
[0.3.0] — 2026-09-18
Fixed
- Inline code spans and fenced code blocks no longer leak their internal
protection placeholder into the output. A code span inside a header
(
# Heading with `code`) or inside emphasis markers (**`code`**) emitted a NUL control character followed by a run of bold hex digits, because the placeholder was built from ASCII alphanumerics that the Unicode mapping steps then rewrote. Placeholders now use Private Use Area characters, which those steps leave untouched (fixes #63). - An inline code span that wraps a fenced block (
` ```x``` `) no longer leaves an unexpanded placeholder in the output. Placeholders are now restored in reverse insertion order, so an outer span is expanded before the keys nested inside it. - A single-line fenced run (
```code```) no longer converts to nothing. Its body was treated as a language tag and discarded, but a language tag is only a language tag when a newline terminates it, so the whole body is content.
Changed
convert()andconvert_file()are now deterministic. Code placeholders were previously derived fromuuid4(), so any input whose placeholder leaked produced different output on every call.- Documents with many code spans convert faster. Placeholders are restored in
a single regex pass rather than one
str.replacescan per placeholder, which removes the quadratic cost of that step. to_sans_bold(),to_sans_italic(),to_sans_bold_italic(), andto_monospace()are faster. They now apply precomputedstr.translate()tables instead of building the mapping per character on every call.clickis now required at version 8.5.0 or newer (previously 8.4.0).
[0.2.3] — 2026-08-05
Changed
- Updated all dependencies to their latest versions to address security vulnerabilities.
[0.2.2] — 2026-04-23
Fixed
- First list item after a heading was misclassified as a sub-item (
‣instead of•) because the bullet-nesting regex matched newlines as indentation (fixes #10).
Changed
- Updated all dependencies to their latest versions to address security vulnerabilities.
[0.2.1] — 2026-04-07
Documentation
- Added a
Limitationssection to the README documenting known trade-offs of Unicode Mathematical Alphanumeric Symbols: visual alignment issues in code blocks and tables, reduced accessibility for screen readers, and reduced searchability on LinkedIn.
[0.2.0] — 2026-04-06
Added
- Code spans and fenced code blocks are now rendered in Unicode Mathematical Monospace font by default, making code visually distinct in LinkedIn posts.
- New
monospace_codeparameter forconvert()andconvert_file()(default:True). Set toFalseto restore the previous plain-text behavior. - New
--no-monospace-codeCLI flag to disable monospace code rendering. - New
to_monospace()Unicode mapping function for programmatic use.
[0.1.1] — 2026-04-06
Fixed
- Fixed logo not appearing on PyPI page.
[0.1.0] — 2026-04-05
Added
- Initial release of
md2linkedin. convert()function for converting Markdown strings to LinkedIn-compatible Unicode plain text.convert_file()function for file-based conversion with automatic.linkedin.txtoutput naming.- Unicode mapping functions:
to_sans_bold(),to_sans_italic(),to_sans_bold_italic(), andapply_style(). - Support for: bold (
**/__), italic (*/_), bold-italic (***/___), ATX headers (#–######), setext headers, fenced code blocks, inline code, links, images, bullet lists, blockquotes, HTML spans, HTML entities, and backslash escapes. --preserve-linksflag to retain Markdown link syntax in output.md2linkedinCLI entry point with stdin support.- Full test suite with 100% code coverage.