Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libre-paste-md

Paste Markdown into LibreOffice Writer as real formatting — headings, lists, tables and links — instead of a wall of literal ## and **.

Status: installable extension, pre-release. Packaged as an .oxt with a toolbar button, Edit-menu entry and Tools > Add-Ons entry, covered by 24 passing in-office tests including a full clipboard-to-document round trip. Not yet published to the LibreOffice Extensions site — see R1.

Why

Markdown is how text arrives now: from chat clients, LLMs, note apps, code review, docs repos. Writer has no way to interpret it on paste, so you paste raw syntax and reformat by hand.

LibreOffice 26.2 added a native Markdown file filter, which handles File > Open document.md. It does nothing for pasting a fragment into a document you are already writing. That gap is what this fills.

Requirements

  • LibreOffice 26.2 or newer, Writer. The native Markdown filter arrived in 26.2 and this builds directly on it.
  • Nothing else. No pip install, no bundled parser, no network access at any point.

Developed and tested against the Flatpak build (org.libreoffice.LibreOffice) on Linux. Other platforms and packaging should work but are not yet verified.

Install

Download the .oxt from the releases page and open it with LibreOffice, or build it yourself:

git clone https://github.com/cclambie/libre-office-paste-markdown.git
cd libre-office-paste-markdown
./tools/build_oxt.sh      # -> build/libre-paste-md-<version>.oxt
./tools/install_oxt.sh    # installs it via unopkg

Close LibreOffice first — installing while it runs silently does nothing.

Then copy some Markdown and use any of:

  • the Paste as Markdown toolbar button
  • Edit > Paste as Markdown, next to Paste Special
  • Tools > Add-Ons > Paste as Markdown

No keyboard shortcut is bound by default, because Ctrl+Shift+V belongs to Paste Special and anything else risks colliding with your own bindings. Bind one via Tools > Customize > Keyboard if you want it.

To uninstall: ./tools/install_oxt.sh --remove, or use the Extension Manager.

How it works

It does not parse Markdown. It hands the clipboard text to LibreOffice's own Markdown filter and inserts the result at the cursor:

cursor.insertDocumentFromURL(tmp_url, (PropertyValue("FilterName", "Markdown"),))

Markdown compliance is therefore inherited from the filter (MD4C, CommonMark + GFM) rather than reimplemented. The trade-off is the hard 26.2+ requirement.

The interesting part: insertion is not "insert at the cursor"

insertDocumentFromURL corrupts the document and its undo record when it has to merge its first paragraph into an existing one. Measured behaviour of the naive approach:

Insertion point Result after one undo
Start of a paragraph with content after it everything lost
Mid-paragraph adjacent text lost permanently
Selection collapsed to its end (still mid-paragraph) adjacent text still lost
End of a paragraph, even with paragraphs following lossless

So the engine never inserts at an arbitrary offset. It first manoeuvres onto a paragraph boundary, picking one of three strategies:

Cursor is Strategy
at a paragraph end insert directly — already safe
at a paragraph start with one before it insert at the end of the previous paragraph
anywhere else split the paragraph, insert at the end of the first half

It then removes the empty paragraph the filter appends, and restores the host paragraph's style, which the filter otherwise overwrites. The whole operation is wrapped in a single undo context, so one Ctrl+Z reverts it exactly.

Roadmap

R1 — Publish on the LibreOffice Extensions site

Deliberately staged behind a GitHub release: the extension needs LibreOffice 26.2+, which most people browsing the site cannot yet run, and early "doesn't work" ratings from ineligible installs would outlive the situation that caused them. See PUBLISHING.md for the process and the trigger for going ahead.

R2 — Options page and first-run guards

Strip YAML front matter (planned default: on). An actionable message on LibreOffice older than 26.2 rather than a confusing failure.

R3 — Cross-platform verification

Verify on deb/rpm LibreOffice, Windows and macOS. The bundled Python version differs by platform, so the syntax floor needs measuring rather than assuming.

R4 — Images (post-1.0)

Currently out of scope: remote URLs turn a paste into a network request, which is a privacy decision needing explicit opt-in, and local paths make documents non-portable. Alt text is rendered for now.

R5 — Support for LibreOffice older than 26.2 (only on real demand)

Would mean vendoring a Markdown parser and emitting Writer content directly — an order of magnitude more work, reimplementing what the native filter already does well. Deliberately not planned.

R6 — Calc support (speculative)

Pasting a Markdown table into a spreadsheet range. Impress and Draw are not planned.

Investigated and closed

Recorded so they are not rediscovered as bugs:

  • Table cells (was D5). Cells were believed to flatten Markdown to plain text. They do not — headings, list membership and the cell's own style all survive. The original finding came from reading cell.getString(), which simply joins paragraphs with \n.
  • List ids on headings (was D4). Headings appeared to carry a spurious list id. They do not: a heading created by hand, with no paste involved, already carries NumberingRules. That is Writer's own outline numbering, not something this introduces.
  • Host paragraph restyling (was D6). Inserting used to overwrite the host paragraph's style, silently converting a heading or quote to Text body. Fixed — the style is captured and restored, except where the filter legitimately claims an empty paragraph.

Development

src/pastemd/insert.py          boundary-safe insertion
src/pastemd/clipboard.py       clipboard text, platform flavour handling
src/pastemd/paste.py           guards, temp file, undo grouping
src/pastemd/report.py          messages and the error log
oxt/                           extension manifest, Addons.xcu, icons
oxt/python/pastemd_component.py  UNO dispatch component behind the button
scripts/paste_markdown.py      Tools > Macros entry point (development)
tests/integration/             in-office test suites
tools/                         build, install and test scripts

Errors the user should not have to read go to ~/.cache/libre-paste-md/error.log; a Flatpak GUI app has no console, so stderr is simply lost.

./tools/run_office_tests.sh                    # all suites
./tools/run_office_tests.sh tests/integration/test_r1.py   # one suite

Tests cannot run while LibreOffice is open. LibreOffice is single-instance: a second invocation is forwarded to the running one, ignoring the isolated test profile. The runner detects this and refuses rather than failing confusingly.

Tests run in-process via LibreOffice's Python script provider, in a throwaway profile, because the external UNO socket bridge does not work under Flatpak. See CLAUDE.md for the details and the traps worth not rediscovering, and SCOPE.md for the design decisions and their evidence.

Licence

MPL-2.0, matching LibreOffice. No vendored dependencies.

About

Paste Markdown Extension for Libre Office

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages