Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mdencode

Renders arbitrary JSON/Python data as readable, CommonMark-safe Markdown.

A Python port of MarkdownEncoder from katmore/micro-encode (PHP).

Not a schema doc generator

jsonschema2md converts a JSON Schema — the shape a family of values is allowed to take — into prose documentation. Given this schema:

{
  "type": "object",
  "properties": {
    "firstName": { "type": "string", "description": "The person's first name." }
  }
}

it produces:

- **`firstName`** *(string)*: The person's first name.

There's no schema in mdencode at all. It renders an actual value you already have:

to_markdown({"firstName": "Doug"})
# '- **firstName:** Doug'

No schema to write, and nothing to keep in sync with the data as it changes.

Not a document builder

json2md (npm) compiles a document you author block-by-block, choosing the Markdown shape yourself:

json2md([{ h1: "Report" }, { ul: ["a", "b"] }])
// => "# Report\n\n - a\n - b"

mdencode takes no authorial input. It infers the rendering purely from an existing value's own shape — a list is a list, a map is a map:

to_markdown({"name": "Doug", "things": ["foo", "bar"]})
# '- **name:** Doug\n- **things:**\n  - foo\n  - bar'

Not a naive "just walk the dict" converter

A ten-minute recursive converter (array -> "- " bullets, dict -> "- **key:**" labels) looks fine on tidy fixtures and quietly corrupts real data. Two examples, checked against an actual CommonMark parser:

A literal - in a string value. A naive converter emits it unescaped:

naive(["- item", "plain"])
# '- - item\n- plain'

That second - isn't text to CommonMark — it's a new marker, so the "item" note becomes a nested bullet list instead of a line of text:

<ul><li><ul><li>item</li></ul></li><li>plain</li></ul>

mdencode escapes the leading - so it stays literal text:

to_markdown(["- item", "plain"])
# '- \\- item\n- plain'
# renders as: <ul><li>- item</li><li>plain</li></ul>

A label directly above a nested list of objects. Naively joining a "- **key:**" label to a nested list with a single \n produces:

- **releases:**
  1.
     - **tag:** 2.0.0
     - **breaking:** true
  2.
     - **tag:** 1.0.0
     - **breaking:** false

CommonMark doesn't parse the 1. as a list at all here — it gets absorbed as literal paragraph text under releases:, and the two release entries come out merged and mis-numbered. mdencode inserts a blank line whenever a label is followed by a nested list that itself starts with a content-less marker:

to_markdown({"releases": [
    {"tag": "2.0.0", "breaking": True},
    {"tag": "1.0.0", "breaking": False},
]})
# '- **releases:**\n\n  1.\n     - **tag:** 2.0.0\n     - **breaking:** true\n  2.\n     - **tag:** 1.0.0\n     - **breaking:** false'

which parses as two clean, separate list entries.

mdencode also promotes a list to numbered markers (1., 2., ...) whenever any element is itself an object, array, or multiline string, regardless of the ordered_lists option — CommonMark can only nest a bare, content-less marker safely under a numbered parent, never under another bare -.

(Not in scope: XML/HTML encoding — the PHP original also ships XmlEncoder/HtmlEncoder; this port is Markdown-only.)

Installation

pip install mdencode

Python usage

from mdencode import to_markdown

to_markdown({
    "name": "Doug",
    "active": True,
    "things": ["foo", "bar"],
})
- **name:** Doug
- **active:** true
- **things:**
  - foo
  - bar

Pass ordered_lists=True to render plain-value lists as 1. foo instead of - foo. A list containing anything other than plain values (a nested object, array, or multiline string) always renders as an ordered list either way, for the CommonMark-nesting reason above.

Command-line usage

echo '{"name":"Doug","active":true}' | json2md
- **name:** Doug
- **active:** true

Convert a file instead by passing its path:

json2md path/to/data.json

Add --ordered for numbered lists where that applies:

echo '{"tags":["php","markdown"]}' | json2md --ordered

Run json2md --help for the full usage. Invalid input (bad JSON, an unreadable file, or nothing on stdin) prints a plain-English error to stderr and exits non-zero — never partial or broken output.

JSONL / NDJSON: jsonl2md

json2md expects one JSON document. Log files and session transcripts — including Claude Code and Codex session files — are usually newline-delimited JSON instead: one independent JSON value per line. jsonl2md renders each line as its own record, separated by a thematic break (---), rather than treating the whole file as one structure:

jsonl2md path/to/session.jsonl | less
head -3 ~/.claude/projects/*/some-session-id.jsonl | jsonl2md
- **type:** queue-operation
- **operation:** enqueue
- **timestamp:** 2026-08-26T05:48:17.617Z
...

---

- **type:** queue-operation
- **operation:** dequeue
...

---

- **parentUuid:** null
- **type:** user
- **message:**
  - **role:** user
  - **content:** ...

Blank lines are skipped; --ordered and -h/--help work the same as json2md.

Legal

Copyright

mdencode — https://github.com/katmore/mdencode

Copyright (c) 2026 Doug Bird. All Rights Reserved.

License

mdencode is copyrighted free software. You may redistribute and modify it under either the terms and conditions of the "The MIT License (MIT)"; or the terms and conditions of the "GPL v3 License". See LICENSE and GPLv3.

About

Renders arbitrary JSON/Python data as readable, CommonMark-safe Markdown.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages