Skip to content

Latest commit

 

History

287 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Copyright 2026 Andreas Remigius Schmidt

npm Changelog License CI Node Codacy grade Codacy coverage

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.

Round-trip convergence

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 → md or org → 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 recordStyle widens 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 → org preserves Markdown-only constructs ("md-isms") as morg_-prefixed org properties; org → md serializes Org-only constructs ("org-isms") as key:: value conventions (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.

Usage

Web UI

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.

CLI

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.org

Configuration file

Options 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 prettier

The full reference, covering all sections and compatibility snippets for prettier and mdformat, is in docs/CONFIGURATION.md.

Library

npm install @remigius42/morg

morg 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 }): preserveMdisms default true; interpretHtml (default false, CLI --interpret-html) interprets the HTML vocabulary morg itself emits under useHtml (bare <u>, <sup>, <sub>, <dl>) as native Org constructs, the inverse of useHtml: with both enabled the round trip is lossless, with interpretHtml alone it converges away from HTML (cleanup mode); other HTML preserves as usual; recordStyle (default false, 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 }): preserveOrgisms default true; useHtml (default false) renders org-only markup as raw HTML (<u>, <sup>, <sub>, <dl>) instead of keeping it verbatim; taskCheckboxes (default false, CLI --task-checkboxes) is a lossy export mode that maps bare TODO/DONE leaf 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 via onWarning

  • logseq({ nestUnderHeadings }): default true; 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/DONE text 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 } (on convertOrgToMarkdown and normalizeMarkdown; 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.

Architecture

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/.

Status

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.

Contributing

See the contributing guide for setup, conventions and the test-first workflow; participation is governed by the code of conduct.

License

GPL-3.0-or-later (required by the uniorg dependencies).

About

Bidirectional Markdown ↔ Org-mode converter, built on the unified ecosystem (remark for Markdown, uniorg for Org).

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

Contributors

Languages