FMJL

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:

LanguageJob in FMJLWhy that one
JSON LinesThe container: one element per line, with its id, hash, page, position and permissionsEvery programming language reads JSON; one line per element means a file can be streamed, searched and cut without a parser
MarkdownAll readable text: headings, paragraphs, lists, simple tables, image descriptionsPeople can read and write it directly; it is the language of GitHub, VS Code and every editor
LaTeXMath formulasThe only language that writes every formula exactly
HTMLTables with merged cells or multi-level headersMarkdown 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.

name.fmjl storage form, for machines name.md authoring form, for people fmjl name.fmjl fmjl name.md
The file extension tells the tool which way to convert.

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.

header line repeat, one per element element line \n every line ends with a newline; there are no empty lines
The whole grammar of a .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 hash type parent characters md required level page bbox reference access latex html optional also subtype, label, pages, continues, confidence, lang, file, meta
Round boxes are required on every element; square boxes appear when they apply.
{"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
optional <!-- id and fields --> Markdown block blank line one element in the authoring form; a group or a multi-block element is closed with an end note

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 ![description](images/file.png), and a figure with several images as a group.

Twenty element types

TypeHoldsTypeHolds
headingTitles and subtitles, with a levelannotationComments and handwritten notes
paragraphNormal text, inline math in $...$redactionBlacked-out hidden parts
listA whole bullet or numbered listnoisePage headers, footers, watermarks, page numbers
tableRows and columns, HTML when cells mergemessageOne email or chat message
formulaDisplay math, with latexutteranceOne speaker's turn in a transcript
codeProgram code, kept exactlyrecordOne CSV row or one JSON object
imagePictures, charts, logos, stamps, signatures, QR codescitationA reference or bibliography entry
captionThe label of a table or imagequoteQuoted text from another source
footnoteNotes at the bottom of a pagetocA table of contents
form_fieldForm entries and checkboxesgroupA 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.