Introducing FMJL
FMJL, short for Format, Markdown, JSON Lines, is a document format built for retrieval. A document becomes a list of small parts called elements: headings, paragraphs, lists, tables, formulas, images, captions, footnotes. Each element has a stable id, a content hash, its page and position on the page, and who may read it. That is exactly what a search engine or a RAG system needs, and it is what every other format leaves out.
FMJL is not one more markup language. It combines four languages that already exist, each doing the one job it is best at:
| Language | Job in FMJL | Why that one |
|---|---|---|
| JSON Lines | The container: one element per line, with its id, hash, page, position and permissions | Every programming language reads JSON; one line per element means a file can be streamed, searched and cut without a parser |
| Markdown | All readable text: headings, paragraphs, lists, simple tables, image descriptions | People can read and write it directly; it is the language of GitHub, VS Code and every editor |
| LaTeX | Math formulas | The only language that writes every formula exactly |
| HTML | Tables with merged cells or multi-level headers | Markdown tables cannot merge cells; HTML can |
One document, two forms
Every FMJL document exists in two forms that hold exactly the same information. Machines read the storage form. People read and write the authoring form. A tool converts either way, and elements that did not change keep their ids and hashes.
The storage form
A .fmjl file is UTF-8 text with one JSON object per line. The first line is the header and describes the document. Every following line is one element, in reading order.
.fmjl file.The header has twelve required fields: the rulebook version, the document name, the original file and its SHA-256, how the original was protected, whether it was signed, the converter, whether headings were found, the number of elements, when it was created, its language, and the default permission.
{"type":"document","version":"1.1","doc":"leave_policy","source":"leave_policy.pdf","sha256":"af17...da68","protection":"none","signed":false,"converter":"fmjl 1.1","structure":true,"elements":6,"created":"2026-09-26T10:30:00Z","lang":"en","access":["all"],"title":"Leave Policy","last_id":6}
An element has six required fields and a few optional ones:
{"id":"leave_policy#e3","hash":"79e15a433bf84d1c","type":"table","label":"leave-types","parent":"leave_policy#e1","page":0,"bbox":[80,190,700,330],"characters":89,"md":"| Leave type | Days per year |\n| --- | --- |\n| Sick leave | 12 |\n| Paternity leave | 15 |"}
{"id":"leave_policy#e4","hash":"fd6cd4502fca6a4a","type":"caption","parent":"leave_policy#e1","page":0,"bbox":[80,335,400,355],"characters":20,"reference":"leave_policy#e3","md":"Table 1: Leave types"}
Three rules make sure nothing is lost. Every part of the original document becomes an element, including stamps, logos, watermarks and blacked-out areas. Every element from a page-based file records its page and position, so its exact spot in the original can always be found. The original file is always kept next to the FMJL files.
The authoring form
The authoring form is a normal Markdown file. The tool turns each block into one element. Anything Markdown cannot say goes into a small HTML comment before the block, which every Markdown viewer hides:
<!-- e3 label=leave-types page=0 bbox=80,190,700,330 -->
| Leave type | Days per year |
| --- | --- |
| Sick leave | 12 |
| Paternity leave | 15 |
<!-- e4 type=caption reference=above -->
Table 1: Leave types
Merged cells never need HTML by hand: in a Markdown table a cell holding only ^ joins the cell above it and < joins the cell to its left. Display formulas are written between $$, images as , and a figure with several images as a group.
Twenty element types
| Type | Holds | Type | Holds |
|---|---|---|---|
heading | Titles and subtitles, with a level | annotation | Comments and handwritten notes |
paragraph | Normal text, inline math in $...$ | redaction | Blacked-out hidden parts |
list | A whole bullet or numbered list | noise | Page headers, footers, watermarks, page numbers |
table | Rows and columns, HTML when cells merge | message | One email or chat message |
formula | Display math, with latex | utterance | One speaker's turn in a transcript |
code | Program code, kept exactly | record | One CSV row or one JSON object |
image | Pictures, charts, logos, stamps, signatures, QR codes | citation | A reference or bibliography entry |
caption | The label of a table or image | quote | Quoted text from another source |
footnote | Notes at the bottom of a page | toc | A table of contents |
form_field | Form entries and checkboxes | group | A slide, a sidebar, a figure, a sheet, a thread |
What retrieval gets
The hash of an element is the first sixteen hexadecimal characters of the SHA-256 of its type and its Markdown. When a document is uploaded again, only elements whose hash changed need new embeddings. The parent chain rebuilds the section path above any element. page and bbox point at the exact region of the original. access is filtered before searching, so nobody sees what they may not see. noise, tables of contents and redactions are never embedded.
import fmjl
rows = fmjl.load("report.fmjl")
for chunk in fmjl.chunks(rows, by="section"):
embed(chunk["text"], metadata={"id": chunk["id"], "page": chunk.get("page"), "access": chunk["access"]})
Does it find answers better? On 42 questions about three documents with tables, headings and lists, the answer was in the top five chunks 95% of the time with FMJL sections against 88% for the same text cut into equal pieces (keyword search), and 79% against 69% with embeddings. On a novel, where there is no structure to keep, the two are level. Details are in the README.
Tools
Python package. pip install fmjl gives the fmjl command: convert both ways, check every rule, import PDF and Word files, print retriever chunks, and export to other formats. pip install "fmjl[pdf]" adds the PDF importer; "fmjl[export]" adds HTML and PDF export.
fmjl notes.md
fmjl notes.fmjl
fmjl report.pdf
fmjl report.docx
fmjl chunks report.fmjl --by section
fmjl export report.fmjl --to docx
VS Code extension. Search for FMJL in the Extensions view (publisher rupakkumar). It converts both ways without Python, colors .fmjl files, and underlines every rule violation while you type.
Windows executable. fmjl.exe on the GitHub Releases page does the same as the package for people without Python.
Read the rulebook
The rulebook is the authority: every field, every rule the checker enforces, the canonical form of Markdown, the JSON Schema, the importer rules, and the definition of a chunk. It is itself an FMJL document, stored in both forms in the repository. Tools in any language are welcome; they follow the rulebook, not the Python package.
Source, examples and benchmark on GitHub. Bug reports and suggestions go to the issue tracker.