Copyright 2026 Andreas Remigius Schmidt
Bidirectional Markdown ↔ Org-mode converter, built on the unified ecosystem (remark for Markdown, uniorg for Org).
morg treats Org as a canonical plain-text format and Markdown (Obsidian,
generic) as the interop surface. Dialect conventions, such as
Logseq's heading:: properties and outline
nesting, are supported via presets.
Strict byte-losslessness between the two formats is impossible. morg's guarantee is to be semantically faithful and convergent instead (see ADR 0001):
- One round trip (
md → org → mdororg → md → org) may normalize formatting, but its output is a fixed point: converting again reproduces it byte-for-byte. - Input already in canonical form is a round-trip identity. Opt-in
recordStylewidens that set: a file whose bullet, emphasis, fence and rule markers are used consistently has them recorded in the org file and restored on the way back, so it is left untouched (ADR 0004). md → orgpreserves Markdown-only constructs ("md-isms") asmorg_-prefixed org properties;org → mdserializes Org-only constructs ("org-isms") askey:: valueconventions (ADR 0002).- The few constructs that cannot be carried are documented in the mapping reference and reported as warnings.
Round-trip fixture tests are the backbone of the test suite
(tests/roundtrip.spec.ts). The Web UI is covered by vitest specs under
happy-dom and by a Playwright suite (tests/e2e/) that drives the built
pages in Chromium and WebKit, including axe accessibility audits in both
color schemes.
Try morg without installing anything at
morg.binarypoetry.ch. All conversion
happens in your browser, nothing is uploaded (see ADR
0003). The
chrome-less embed page (/embed.html, optionally with
?theme=dark|light) can be iframed into other sites. It posts its
content height to the host on every change, so the frame can follow it
rather than scrolling inside a page that already scrolls:
addEventListener("message", event => {
if (event.origin !== "https://morg.binarypoetry.ch") return
if (event.data?.type === "morg:height") {
frame.style.height = `${event.data.height}px`
}
})Give the frame at least 768px of width if you can; below that the
input and output stack, which doubles its height. allow="clipboard-write"
lets the Copy button use the clipboard rather than falling back to
selecting the output.
Besides pasting, a file can be opened with the picker or dropped
anywhere on the page: a .toml lands in the config panel, a document
in the input, and the conversion direction follows the extension. Drop
both at once and each goes where it belongs; an overlay names what is
accepted while a drag is in flight, and anything that turns out not to
be text is named in the warning list rather than loaded. The result can
be copied or saved with the Copy and Download buttons; a normalized file
is saved as notes.normalized.org, and switching to a direction that no
longer reads the opened file falls back to a generic name, so neither
lands on top of its own source. Files are read and written by the
browser itself; this is not an upload.
Typing is converted once you pause, not once per keystroke, and the conversion itself runs in a web worker, so the page stays responsive even while a large document is being converted. Copy and Download are unavailable for as long as a conversion is running, so they can never save the previous document's output.
Run it without installing, or install it globally:
npx @remigius42/morg --input notes.md --output notes.org
npm install --global @remigius42/morg# Every flag, with examples; also shown for a bare `morg`
morg --help
# Formats inferred from file extensions; --from and --to take a format
# name (markdown or org), not a path
morg --input notes.md --output notes.org
# stdin/stdout with explicit format
echo "# Hello" | morg --from markdown
# Apply a dialect preset
morg --input page.md --output page.org --preset logseq
# Dropped constructs are reported on stderr; -s / --silent suppresses.
# Boolean flags take an optional value, so --silent false overrides a
# morg.toml that sets it
morg --input notes.md --output notes.org --silent
# Record the source's own markdown style (bullet, emphasis, fence,
# rule) in the org file, so the return trip restores it instead of
# canonicalizing it; markers used inconsistently warn and are skipped
morg --input notes.md --output notes.org --record-style
# Normalize to canonical form (same format in and out); this
# canonicalizes (the one-time reformat a first conversion would apply
# anyway, ADR 0001); it is not a style formatter like prettier
morg normalize --input notes.org --output notes.orgOptions can live in a morg.toml (auto-discovered in the working
directory, or passed via --config path). Precedence: CLI flags >
config file > defaults:
preset = "logseq"
[orgToMarkdown.markdownStyle]
emphasis = "_" # align with prettierThe full reference, covering all sections and compatibility snippets for prettier and mdformat, is in docs/CONFIGURATION.md.
npm install @remigius42/morgmorg is ESM-only and ships its own type declarations:
import {
convertMarkdownToOrg,
convertOrgToMarkdown,
logseq
} from "@remigius42/morg"
const org = convertMarkdownToOrg("# Hello\n\nWorld.")
const md = convertOrgToMarkdown(org)
// Logseq dialect
const logseqOrg = convertMarkdownToOrg(markdown, { preset: logseq() })Options (flags accept boolean or a per-construct Record<string, boolean>):
-
convertMarkdownToOrg(md, { preserveMdisms, interpretHtml, recordStyle, preset }):preserveMdismsdefaulttrue;interpretHtml(defaultfalse, CLI--interpret-html) interprets the HTML vocabulary morg itself emits underuseHtml(bare<u>,<sup>,<sub>,<dl>) as native Org constructs, the inverse ofuseHtml: with both enabled the round trip is lossless, withinterpretHtmlalone it converges away from HTML (cleanup mode); other HTML preserves as usual;recordStyle(defaultfalse, CLI--record-style) records the document-level markdown style as a#+MORG_MARKDOWN_STYLE:keyword so the round trip restores it (ADR 0004) -
convertOrgToMarkdown(org, { preserveOrgisms, useHtml, taskCheckboxes, preset }):preserveOrgismsdefaulttrue;useHtml(defaultfalse) renders org-only markup as raw HTML (<u>,<sup>,<sub>,<dl>) instead of keeping it verbatim;taskCheckboxes(defaultfalse, CLI--task-checkboxes) is a lossy export mode that maps bareTODO/DONEleaf headlines to GFM task items (- [ ]/- [x]); headings become list items and do not restore on the return trip; anything with priority, tags or content keeps its heading and reports viaonWarning -
logseq({ nestUnderHeadings }): defaulttrue; content following a heading nests as child blocks of that heading: paragraphs become child headlines one level deeper (in Logseq org every outline block is a headline), other constructs stay in the preceding block's body. The reverse direction restores headings from:heading:properties and turns plain block headlines back into paragraphs. Hiccup blocks ([:div …]) pass through as plain text and are emitted unescaped in Markdown. Logseq's own syntax maps both directions:TODO/DONEtext markers and[#A]priorities ↔ org keywords/priorities, page references[[page]]and labeled forms[label]([[page]])↔ org fuzzy links[[page][label]], block refs[label](((uuid)))↔[[((uuid))][label]], and^^highlight^^markup survives verbatim (it would otherwise re-parse as superscripts). -
obsidian(): wikilinks[[Page]]/[[Page|alias]]↔ org fuzzy links -
normalizeMarkdown(md, { preset })/normalizeOrg(org, { preset })(CLI:morg normalize): one full round trip to morg's canonical form, a fixed point. Canonicalization, not styling: org-isms and md-isms are rewritten exactly as a conversion would rewrite them. Normalize with the same preset/config you will convert with, since convergence is per-config (ADR 0002). -
markdownStyle: { bullet, emphasis, strong, fence, rule, ruleRepetition }(onconvertOrgToMarkdownandnormalizeMarkdown; CLI--bullet,--emphasis,--strong,--fence,--rule,--rule-repetition) are Markdown output style knobs. Defaults match prettier except emphasis (*italic*);--emphasis _aligns fully with prettier. Canonical form is per-config (ADR 0001): round trips must use the same style. Note CommonMark/GFM prescribe no style; these defaults are morg's canonical choices, not a standard.
Both convert functions also accept onWarning: message => …, called for
each construct dropped without an equivalent (e.g. image titles, LaTeX
fragments). The CLI wires this to stderr unless -s / --silent is
given; the library is silent unless a callback is passed.
Two pipelines, each with a two-phase transformation separating the dialect-agnostic core from dialect presets:
md → org: remark-parse → mdast→uniorg (core) → preset transforms → uniorg-stringify
org → md: uniorg-parse → preset extraction → uniorg→mdast (core) → remark-stringify
Formatting is controlled by shaping the AST (e.g. inserting newline text nodes), not by custom stringifier handlers. The default, battle-tested stringifiers do the rendering.
Project vocabulary lives in CONTEXT.md; design decisions in docs/adr/.
The core conversion surface is feature-complete and validated against
real-world Logseq org vaults (edge cases found there live on as
anonymized fixtures, e.g. tests/fixtures/logseq-vault.org); the
client-side Web UI is deployed from
main. The npm package is @remigius42/morg, since the bare morg
name is taken, and pushing a v* tag publishes it. Most of the code is
written with an AI coding agent under human direction, test-first and
CI-gated. See
the contributing guide.
How each construct maps, including deliberate normalizations and documented drops, is covered in the mapping reference. Notable changes are tracked in the changelog.
See the contributing guide for setup, conventions and the test-first workflow; participation is governed by the code of conduct.
GPL-3.0-or-later (required by the uniorg dependencies).