FMJL Rulebook, Version 1.1
Status: released, version 1.1. License: MIT, copyright Rupak Kumar. Website: https://rupakkumar-76319.github.io/fmjl/
This rulebook is the authority for FMJL. The fmjl tool, the Python package of the same name, is the reference implementation: if the tool and this rulebook disagree, the tool has a bug.
1. What FMJL Is
FMJL stores any document as a list of small parts called elements, such as headings, paragraphs, tables, and images. It is built so that people can read and write documents easily, and machines can search and use them quickly.
FMJL combines four languages:
| Language | Role | Where it appears |
|---|---|---|
| JSON (JSON Lines) | The container of the storage form | Every line of an .fmjl file |
| Markdown | All readable text | The md field, and the whole authoring form |
| LaTeX | Math formulas | The latex field |
| HTML | Tables with merged cells | The html field |
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.
2. One Document, Two Forms
Every FMJL document can be written in two forms that hold exactly the same information:
| Storage form | Authoring form | |
|---|---|---|
| File | name.fmjl |
name.md |
| Looks like | One JSON object per line | Normal Markdown, with small hidden notes |
| Made for | Machines: search, RAG, databases | People: reading, writing, reviewing |
| Opens in | Any JSON Lines tool or language | Any Markdown editor, GitHub, VS Code preview |
The tool converts between the two forms without losing anything: fmjl new turns the authoring form into the storage form, and fmjl md turns it back. Converting there and back always gives the same result.
Recommended use:
- People write and edit the authoring form.
- Machines read the storage form.
- Version control (such as Git) keeps the authoring form, because its short lines make changes easy to see.
3. Storage Layout
While processing, each document is stored as one folder:
leave_policy/
leave_policy.fmjl the storage form
leave_policy.md the authoring form (optional)
images/ cropped images
original/ the untouched original file
For sharing, the same folder is packed into one zip file.
The name FMJL is short for Format, Markdown, JSON Lines, and it is also the file extension, .fmjl. Rules for the storage form:
- Text encoding is UTF-8.
- Each line is exactly one JSON object. Lines end with a newline character, and there are no empty lines.
- Line 1 is the header line. Every following line is an element line.
- Element lines appear in reading order.
Lines in the storage form can be very long, because a whole table is one line. To compare two versions, use the authoring form, or git diff --word-diff on the storage form.
4. The Header Line
The header line describes the whole document. It has "type": "document" and these 12 required fields:
| Field | Value | Meaning |
|---|---|---|
version |
Text | The rulebook version the file follows, for example "1.1" |
doc |
Text | Short document name, using lowercase letters, digits, _ or - |
source |
Text | The original file name |
sha256 |
64 hexadecimal characters | Fingerprint of the original file, used to skip duplicate uploads |
protection |
none, password, certificate, or drm |
How the original file was locked |
signed |
true or false |
Whether the original carries a digital signature |
converter |
Text | The tool and version that created this file |
structure |
true or false |
false means no headings were found, so the file should be converted again with a better tool |
elements |
Whole number, or null |
The number of element lines; null only while the file is still being written |
created |
Date and time (ISO 8601, UTC) | When the file was created |
lang |
Language code | The main language, for example "en", "hi", "as", or "bn" |
access |
List of group names | The default permission, for example ["all"] or ["hr"] |
Optional header fields describe the document itself:
| Field | Value | Meaning |
|---|---|---|
title |
Text | The document's title |
authors |
List of names | Who wrote the document |
date |
YYYY, YYYY-MM, or YYYY-MM-DD |
The document's own date, not the conversion date |
summary |
Text | A short summary of the whole document |
last_id |
Whole number | The highest element number ever given, so numbers are never reused (section 7.1) |
meta |
Object | Custom data (section 7.6) |
Security rule: protection records only how a file was locked. A password or key is never stored anywhere in FMJL.
5. Element Lines
5.1 Required Fields
Every element line has these 6 fields:
| Field | Value | Meaning |
|---|---|---|
id |
Text | <doc>#e<number>, for example leave_policy#e7 (section 7.1) |
hash |
16 hexadecimal characters | Content fingerprint (section 7.2) |
type |
One of the 20 types | What kind of part this is (section 6) |
parent |
Element ID or null |
The heading or group this element belongs to (section 7.4) |
characters |
Whole number | The number of characters in md |
md |
Text | The content, in canonical Markdown (section 8) |
5.2 Optional Fields
| Field | Value | Used when |
|---|---|---|
subtype |
Text | The element is an image, noise, or group |
label |
Lowercase name, for example inputs |
People want to refer to the element by name |
level |
1 to 6 | The element is a heading (required for headings) |
page |
Whole number, from 0 | The element sits on one page |
pages |
List of page numbers | The element spans several pages |
bbox |
[left, top, right, bottom] |
The position on the page is known |
reference |
Element ID, or a list of two or more IDs | The element points to other elements |
continues |
Element ID | The element is the next part of a split element |
confidence |
Number from 0 to 1 | The content came from OCR or an AI model |
lang |
Language code | The element's language differs from the header |
access |
List of group names | The element's permission differs from the header |
html |
Text | A table has merged cells or multi-level headers |
latex |
Text | The element is a formula (required for formulas) |
file |
Path | The element is an image (required for images) |
meta |
Object | Custom data (section 7.6) |
When an optional field is missing, a default applies: no confidence means fully certain (1), and no lang or access means the header's value applies.
6. Element Types
| # | Type | Holds | Rules |
|---|---|---|---|
| 1 | heading |
Titles and subtitles | Needs level |
| 2 | paragraph |
Normal text | Inline math uses $...$ (section 8) |
| 3 | list |
Bullet or numbered points | A whole list is one element |
| 4 | table |
Rows and columns | May add html; long tables may be split (section 11) |
| 5 | formula |
Display math | Needs latex; md is $$ + latex + $$ |
| 6 | code |
Program code | md is a fenced code block that names the language |
| 7 | image |
Pictures, charts, logos, stamps, signatures, QR codes | Needs file; md holds a text description |
| 8 | caption |
The label of a table or image | reference points to that table or image |
| 9 | footnote |
Notes at the bottom of a page | md starts with [^n]: ; reference points to the text that carries [^n] |
| 10 | form_field |
Form entries and checkboxes | md looks like - [x] Married |
| 11 | annotation |
Comments and handwritten notes | Usually has confidence |
| 12 | redaction |
Blacked-out hidden parts | md is [REDACTED] |
| 13 | noise |
Page headers, footers, watermarks, page numbers | Needs subtype |
| 14 | message |
One email or chat message | Sender and date go at the top of md |
| 15 | utterance |
One speaker's turn in a transcript | Speaker and time go at the top of md |
| 16 | record |
One CSV row or one JSON object | md lists the fields as Name: value |
| 17 | citation |
A reference or bibliography entry | In-text markers link here with reference |
| 18 | quote |
Quoted text from another source | md uses > |
| 19 | toc |
A table of contents | Kept for structure |
| 20 | group |
A container, such as a slide, a sidebar, or a figure with several images | Needs subtype; md holds its title |
Subtypes:
- For
image:logo,stamp,signature,chart,diagram,photo,qr_code, orbarcode. Forqr_codeandbarcode,mdalso holds the decoded value. - For
noise:header,footer,watermark, orpage_number. - For
group:slide,sidebar,figure,box,sheet, orthread.
7. Field Rules
7.1 IDs
- An ID is
<doc>#e<number>and is unique in the document. - An ID never changes once given, even when other elements are added or removed.
- A new element gets the next free number, which is
last_idplus 1. Numbers of removed elements are never reused. - Reading order comes from the order of the lines, not from the ID numbers.
7.2 Hash
hash is the first 16 hexadecimal characters of the SHA-256 of the text type, a newline, then md, encoded in UTF-8. When a document is uploaded again, only elements whose hash changed need new embeddings.
Hashes are only compared within one document, so 16 characters (64 bits) are enough. Even a document with 1 million elements has about a 1 in 37 million chance of any two hashes matching.
7.3 Pages and Positions
- Page numbers start at 0. When showing a page number to users, add 1.
- An element has
pageorpages, never both.pagesholds at least two numbers, in order. bboxis[left, top, right, bottom]in whole numbers from 0 to 1000, where 1000 is the full page width or height and[0, 0]is the top-left corner.bboxis allowed only together withpage.- For slides,
pageis the slide number. For spreadsheets, it is the sheet number.
7.4 Parents and Groups
parentpoints to aheadingor agroupthat comes earlier in the file.- By default, an element's parent is the nearest heading above it with a lower level. Inside a group, the default parent is the group, or a heading inside that group.
- A heading's parent, when it is a heading, must have a smaller level.
- Groups can hold any elements, including headings and other groups.
7.5 Links Between Elements
referencelinks an element to others: a caption to its table, a footnote to its text, a chart image to the table holding its data.continueslinks the parts of a split element in order. Each part has the same type as the part before it.- Inside
md, a Markdown link can point to an element in the same document by ID or label:[Table 1](#e7)or[Table 1](#inputs). - A
labelis unique in the document and never changes the ID. - A footnote marker inside
mdis[^n], wherenis the footnote's number in the source. The footnote element'smdbegins with[^n]:followed by its text.
7.6 Custom Data
meta holds custom data on the header or on any element. To avoid clashes, use names that start with your organization, such as "acme.department". One name is reserved: facts, a list of extracted facts, for example [{"name": "fee", "value": "₹5,000"}]. Names that start with fmjl. are reserved for the tools; for example, an importer records the tracked changes it accepted as fmjl.tracked_changes (section 10.1), and the ID of the element where the body of a book starts, after its front matter, as fmjl.body in the header meta (sections 10.1 and 12.1).
8. Canonical Markdown
The same content must always give the same md text, so that the same content always gives the same hash. md follows CommonMark plus GitHub-style tables, strikethrough, and task lists, written in this canonical form:
- Lines end with a newline character only, with no spaces at the end of a line.
- A hard line break is written as a backslash at the end of the line.
- Headings use
#marks followed by one space, for example## Annual Leave. - Bullet list markers are
-. - Tables are written as
| cell | cell |, with separator cells---,:---,---:, or:---:. - A
tablewithhtml: the HTML is the exact structure, andmdis made from it by repeating each merged cell in every cell it covers. HTML written by a tool is in canonical form:<table>and</table>on their own lines, one<tr>per line,thcells in the first row andtdcells below,rowspanbeforecolspan, only&,<and>as entities, and no other tags or attributes. - A
formula:latexis the formula, andmdis$$+latex+$$. - Inline math is written between single dollar signs. A dollar sign followed by a digit is always money, never math, so "$50" is always an amount. A literal dollar sign can also be written as
\$. codeelements keep their text exactly as written.
The fmjl fill command rewrites any md into canonical form.
9. The Authoring Form
The authoring form is a normal Markdown file that anyone can read and write. The tool turns each block of Markdown into one element.
9.1 Front Matter
The file can start with front matter between two --- lines. It holds the header fields, one per line:
---
doc: photosynthesis
title: Photosynthesis Notes
authors: Riya Das, Amit Roy
date: 2026-09-26
lang: en
---
Missing header fields are filled in by the tool.
9.2 Blocks Become Elements
The tool recognizes the type of each block by itself:
| You write | It becomes |
|---|---|
# Title, ## Section |
heading |
| Normal text | paragraph |
- item or 1. item |
list |
| A Markdown table | table |
<table>...</table> |
table with html |
A table cell holding only ^ or < |
table with html: the cell is joined to the cell above (^) or to its left (<) |
$$ ... $$ on its own |
formula |
 on its own |
image |
| A fenced code block | code |
> quoted text |
quote |
A table needs its separator row, | --- | --- |, and each row starts and ends with |. Lines that hold | but no separator row are read as a paragraph, and the tool warns. Joined cells must form a rectangle. To put a literal ^ or < alone in a cell, write \^ or \<. When the tool writes the authoring form, a table whose html is in canonical form is shown with ^ and < again; other HTML is shown as written.
9.3 Notes
A note is an HTML comment on its own line, directly before a block. Markdown viewers hide notes, so the document stays clean to read. A note adds fields to the block after it:
<!-- type=caption reference=inputs -->
Table 1: Inputs of photosynthesis
A note can hold:
- The element's ID, such as
e7, first. type=for types the tool cannot recognize by itself, such ascaption,footnote, ornoise.subtype=,label=,page=,lang=,confidence=,parent=, andcontinues=.- Lists separated by commas:
pages=,bbox=,access=, andreference=. meta=followed by JSON, always at the end of the note.
References, parent=, and continues= can use an ID (e7), a label (inputs), or the words above and below for the block just before or just after this one; so a label cannot be named above or below. HTML comments that do not start like a note stay in the document as normal content.
9.4 Groups
A group starts with <!-- group subtype=... -->. The block right after that note is the group's title: it becomes the group's own md and is not an element of its own, so the group has one ID, the one in the group note. Every block until <!-- /group --> belongs to the group:
<!-- group subtype=figure -->
**Figure 1: Inside a leaf**

<!-- /group -->
9.5 Elements With Several Blocks
When one element holds several blocks, such as an email with a blank line inside it, it starts with a note that has an ID or a label, and ends with a matching end note:
<!-- type=message label=hr-email -->
**From:** HR
Please submit the form by Friday.
<!-- /hr-email -->
9.6 Editing an Existing Document
- Turn the storage form into the authoring form:
fmjl md name.fmjl. - Edit
name.mdin any editor. Each block keeps its ID in a small note, such as<!-- e7 -->. - Turn it back:
fmjl new name.md.
Blocks you did not change keep their IDs and hashes, new blocks get new IDs, and removed IDs are never reused.
10. Input Formats
FMJL accepts all formats, in five groups:
| Group | Formats | How they are converted |
|---|---|---|
| Page-based | PDF, TIFF, PNG, JPG, and other scans | Full pipeline with OCR and layout detection; fills page, bbox, and confidence |
| Structured | DOCX, DOC, PPTX, ODT, RTF, HTML, Markdown, EPUB, LaTeX | Structure is read directly, with no OCR; each slide becomes a group with subtype slide |
| Tables | XLSX, XLS, ODS, CSV, TSV | Each sheet becomes a group with subtype sheet, holding table parts; CSV rows become record elements |
| Code and data | Source code, JSON, JSONL, YAML, XML | Code becomes one code element per function or class; data becomes one record per object |
| Email and transcripts | EML, MSG, MBOX, chat exports, SRT, VTT | One message per message, inside a group with subtype thread; one utterance per speaker turn |
Images saved by FMJL use PNG, WebP, JPG, or SVG. TIFF is accepted as input only.
10.1 Importer Rules
Tools that convert other formats into FMJL follow these rules, so that two importers give the same shape of document:
- Merged cells: a table with merged cells gets
htmlin canonical form (section 8), never a Markdown table with empty cells. - Figures: an image and its caption are two elements, the caption pointing to the image with
reference. Agroupwith subtypefigureis used only when a figure holds several images. - Captions: a caption that starts with "Table" belongs to the nearest table; one that starts with "Figure", "Chart", "Diagram", "Image" or "Photo" belongs to the nearest image. Only when no such neighbour exists may the other kind be linked. In Word files a table's caption usually sits above the table and a figure's caption below the figure.
- Heading levels: a heading is at most one level deeper than the heading before it; deeper ones are raised, so a document never jumps from level 2 to level 4.
- Charts drawn with lines and shapes (vector graphics) are rendered to a PNG image with subtype
diagram, orchartwhen the caption calls it a chart, graph, or plot. Their axis labels belong to the image, not to the text. - Formulas: Word math (OMML) is converted to LaTeX, display math as a
formulaelement and inline math as$...$. A formula that cannot be recovered as LaTeX, which is the case in most PDFs, stays as the text that was read, inside its paragraph. - Pages from structured formats: DOCX and similar files give
pageonly from the page breaks the file remembers, and neverbbox. Those numbers can be stale; only page-based formats give reliable positions. - Tracked changes: an importer takes the accepted text, keeping insertions and dropping deletions. It warns, and records the counts in the header
metaasfmjl.tracked_changes. - Scans and OCR: when font sizes are unreliable, short lines in capital letters or with a section number, such as "2.1 Methods", become headings.
- Footnotes: written as in section 7.5, one
footnoteelement right after the element that cites it. - Running headers and footers: a line in the top or bottom margin that repeats on many pages is
noise, even when its page number changes, is written in Roman numerals, or moves from one side to the other, so "PERSUASION. 11" and "102 PERSUASION." are the same header, and so are "xii • Contents" and "Contents • xiii". Three pages are enough, so a header that names the current chapter is found too. A chapter line (rule 14) is never a running header. A number standing alone near the top or bottom edge, or below all the text of its page, such as a printer's sheet mark at the end of a chapter, isnoisewith subtypepage_number. - Paragraphs: a paragraph is one element even when the text layer splits it into several blocks. Blocks join when they have the body text size, stand in the same column with the usual line spacing, the line above runs to the right margin, and the line below is not indented. In documents that do not indent their paragraphs, the text above must also stop in the middle of a sentence. A paragraph that starts in lower case, in the same column just below a paragraph that stops mid-sentence and runs to the right margin, is the rest of that paragraph, whatever size the text layer gives it. A word or two that the text layer moved out of its line goes back into the line it overlaps most, and so does a small footnote marker such as "[12]". A drop cap, the large first letter of a paragraph, goes back to the start of its paragraph. When the drop cap is only a picture, the first word of the chapter lacks its first letter ("IR WALTER"); if that word occurs nowhere else in the document and one capital letter in front of it makes a word the document uses clearly more often than any other letter would ("SIR"), that letter is put back.13. Page breaks: a paragraph that runs over a page break is two elements, the second with
continuespointing to the first. This holds when the first part stops in the middle of a sentence, and in books with indented paragraphs also when the second part is not indented. - Books: when at least three lines read like "Chapter I", "CHAPTER II.", "Part Two", or "Book 3", those lines become headings, parts at level 1 and chapters below them; "Preface", "Introduction", "Prologue", and "Foreword" lines become headings too. Headings found before the first of these lines, such as words on a title page or a library stamp, become paragraphs, and the first of these lines is recorded as
fmjl.bodyin the headermeta(section 7.6). - Scanned pages: on a page whose text lies over a picture of the whole page, a text layer made by OCR, a space is added between two words that the layer stored without one: between two separate pieces of text, and inside one piece where the gap between two letters clearly stands out from the other gaps in it. An image with text lying on it is part of the scan, not a picture, and gives no element; so does text without any letter or digit ("*" read from an ornament) and a piece of at most three characters in another script than the page. A page with no text whose picture is almost all white, such as an empty endpaper, gives no elements.
- Sidebars: text in a smaller size in a narrow column beside the body text, such as quotations in the margin, is a sidebar. It is read after the body text of its page, so it never cuts a body paragraph in two; its blocks join only with each other, and its parts on following pages are linked with
continues. - Headings: a heading may end with "?" or "!". A chapter number on a line of its own just above a heading becomes part of that heading ("6 Anxiety: Are You a Worrier?").
- Lists: a list item keeps a letter or Roman marker such as "a)" or "IV." in its text; only symbol bullets such as "•" become
-. Letter markers make a list only when at least two lines carry them in order, so a name such as "R. B. Sparkman" stays text, and a single line starting with a dash is an attribution, not a list. Short lines after a line that ends with a colon, none of them reaching the right margin, are the items of a list without bullets. - Hyphens: a word that a hyphen breaks across two lines becomes one word again, without the hyphen: "some-" and "thing" become "something", and "good-" and "looking" become "goodlooking", however long the word. There is one exception: when the document itself writes that word with a hyphen somewhere else and never as one word, the hyphen is part of the word and stays ("self-esteem"). An importer never invents a hyphen the document does not show. A soft hyphen (U+00AD, in Word
<w:softHyphen/>) only marks where a word may break, so it is removed and the word stays whole; a non-breaking hyphen (in Word<w:noBreakHyphen/>) is a real hyphen and is written as "-". - Word text that is easy to lose or to leak: the text of a text box is kept, in the paragraph that holds the box, read once even when the file stores the box twice for older Word versions. Text formatted as hidden is not taken. A symbol from a symbol font, such as the Wingdings tick, becomes the matching Unicode character ("✓"). A list style with a number, such as "List Bullet 2", is one level deeper than "List Bullet", and a numbered list that follows bullets in the same run starts again at 1.
11. Processing Order
- Quick check: confirm the file is valid, not a duplicate (
sha256), which pages are digital or scanned, the language, and the page count. - Convert: extract every part into elements and write the storage form.
- Final check: run the checks listed below.
- Chunk: build chunks as described in section 12.1.
- Embed: create embeddings and store them in the vector database.
If the final check fails, convert again with a stronger tool and check again.
Large elements: a table longer than a converter's limit is split into parts, each repeating the header rows, linked with continues. A paragraph or list longer than one chunk is split at sentence boundaries when chunking.
The final check confirms that:
- Every line passes the JSON Schema in section 14.
elementsin the header is a number and equals the number of element lines.- IDs and labels are unique, and no ID number is higher than
last_id. - Every
parent,reference,continues, and in-document link points to an existing element. - Every
fileexists in theimages/folder. - Every
mdis in canonical form, and everyhashandcharactersvalue matches it.
12. Retrieval
One rule is required: always filter by access before searching, so users never see parts they are not allowed to see.
12.1 Chunks
A chunk is what a retriever embeds and returns. The fmjl chunks command builds chunks in two ways:
- By element, the default: one chunk per element. A
captionorfootnotewhosereferenceis a single element joins that element's chunk instead of forming its own.noise,toc, andredactionelements are never chunked. - By section: one chunk per heading, holding the heading and every element below it up to the next heading. A heading with nothing under it but a deeper heading joins that heading's section. A section longer than a chosen number of characters is split into parts with the ids
id/1,id/2, and so on.
In both ways, the parts of an element linked with continues count as one element: their md is joined, with a space for text types and with a word split by a hyphen at the break joined again, and they are never split into different chunks. When the header meta holds fmjl.body, the elements before it are front matter and are not chunked unless the user asks for them (fmjl chunks --front).
Every chunk carries:
id: the ID of its first element, plus/nfor a split part.elements: the IDs it holds, when there are several.text: the titles of the headings and groups above the chunk joined with>, a blank line, then themdof its elements joined with blank lines. This is the text to embed.mdandpathhold the two parts separately.hash: the element'shash, or for several elements the first 16 hexadecimal characters of the SHA-256 of their hashes joined with newlines. A chunk needs a new embedding only when itshashchanges.doc,type,pageorpages,bbox,file,reference,access,title, andsource, copied from the element and the header, so the retriever can filter, cite, and show the original region.
12.2 Recommended Practices
The rest are recommendations. A system may change them for good reasons:
- Don't embed
noise,toc, orredactionelements. - Embed
md, including for tables that also havehtml. - Add each element's section path, rebuilt from its
parentchain, to its chunk for context. - Return
captionandcitationelements together with the elements they reference, and a chart together with its data table. - When an answer uses content with low
confidence, show the original region usingpageandbbox. - Cite answers with the element
idand the page number plus 1.
13. Reader Rules
- Readers ignore fields they do not know, so newer files stay readable.
- Readers treat unknown types as
paragraph. - A reader for version 1.x reads any file of version 1.x: a later minor version only adds optional fields, types, or subtypes, and readers ignore what they do not know. The
fmjl upgradecommand turns 0.x files into the current version.
14. JSON Schema
This schema checks every line of the storage form automatically (JSON Schema draft 2020-12). A line is valid if it matches either the header or the element definition. Checks that need the whole file are listed in section 11.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "FMJL line, version 1.1",
"oneOf": [
{
"$ref": "#/$defs/header"
},
{
"$ref": "#/$defs/element"
}
],
"$defs": {
"header": {
"type": "object",
"required": [
"type",
"version",
"doc",
"source",
"sha256",
"protection",
"signed",
"converter",
"structure",
"elements",
"created",
"lang",
"access"
],
"properties": {
"type": {
"const": "document"
},
"version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+$"
},
"doc": {
"type": "string",
"pattern": "^[a-z0-9_-]+$"
},
"source": {
"type": "string",
"minLength": 1
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"protection": {
"enum": [
"none",
"password",
"certificate",
"drm"
]
},
"signed": {
"type": "boolean"
},
"converter": {
"type": "string",
"minLength": 1
},
"structure": {
"type": "boolean"
},
"elements": {
"type": [
"integer",
"null"
],
"minimum": 0
},
"created": {
"type": "string",
"format": "date-time"
},
"lang": {
"type": "string",
"pattern": "^[a-z]{2,3}(-[A-Za-z0-9]+)*$"
},
"access": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9_.:-]+$"
}
},
"title": {
"type": "string"
},
"authors": {
"type": "array",
"items": {
"type": "string"
}
},
"date": {
"type": "string",
"pattern": "^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$"
},
"summary": {
"type": "string"
},
"last_id": {
"type": "integer",
"minimum": 0
},
"meta": {
"type": "object"
}
}
},
"element": {
"type": "object",
"required": [
"id",
"hash",
"type",
"parent",
"characters",
"md"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9_-]+#e[1-9][0-9]*$"
},
"hash": {
"type": "string",
"pattern": "^[0-9a-f]{16}$"
},
"type": {
"enum": [
"heading",
"paragraph",
"list",
"table",
"formula",
"code",
"image",
"caption",
"footnote",
"form_field",
"annotation",
"redaction",
"noise",
"message",
"utterance",
"record",
"citation",
"quote",
"toc",
"group"
]
},
"subtype": {
"type": "string"
},
"label": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]*$"
},
"level": {
"type": "integer",
"minimum": 1,
"maximum": 6
},
"parent": {
"type": [
"string",
"null"
],
"pattern": "^[a-z0-9_-]+#e[1-9][0-9]*$"
},
"page": {
"type": "integer",
"minimum": 0
},
"pages": {
"type": "array",
"minItems": 2,
"items": {
"type": "integer",
"minimum": 0
}
},
"bbox": {
"type": "array",
"minItems": 4,
"maxItems": 4,
"items": {
"type": "integer",
"minimum": 0,
"maximum": 1000
}
},
"characters": {
"type": "integer",
"minimum": 0
},
"reference": {
"anyOf": [
{
"type": "string",
"pattern": "^[a-z0-9_-]+#e[1-9][0-9]*$"
},
{
"type": "array",
"minItems": 2,
"items": {
"type": "string",
"pattern": "^[a-z0-9_-]+#e[1-9][0-9]*$"
}
}
]
},
"continues": {
"type": "string",
"pattern": "^[a-z0-9_-]+#e[1-9][0-9]*$"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"lang": {
"type": "string",
"pattern": "^[a-z]{2,3}(-[A-Za-z0-9]+)*$"
},
"access": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9_.:-]+$"
}
},
"html": {
"type": "string"
},
"latex": {
"type": "string"
},
"file": {
"type": "string",
"pattern": "^images/[^/]+\\.(png|webp|jpg|jpeg|svg)$"
},
"meta": {
"type": "object"
},
"md": {
"type": "string"
}
},
"dependentRequired": {
"bbox": [
"page"
]
},
"not": {
"required": [
"page",
"pages"
]
},
"allOf": [
{
"if": {
"required": [
"type"
],
"properties": {
"type": {
"const": "heading"
}
}
},
"then": {
"required": [
"level"
]
}
},
{
"if": {
"required": [
"type"
],
"properties": {
"type": {
"const": "formula"
}
}
},
"then": {
"required": [
"latex"
]
}
},
{
"if": {
"required": [
"type"
],
"properties": {
"type": {
"const": "image"
}
}
},
"then": {
"required": [
"file"
],
"properties": {
"subtype": {
"enum": [
"logo",
"stamp",
"signature",
"chart",
"diagram",
"photo",
"qr_code",
"barcode"
]
}
}
}
},
{
"if": {
"required": [
"type"
],
"properties": {
"type": {
"const": "noise"
}
}
},
"then": {
"required": [
"subtype"
],
"properties": {
"subtype": {
"enum": [
"header",
"footer",
"watermark",
"page_number"
]
}
}
}
},
{
"if": {
"required": [
"type"
],
"properties": {
"type": {
"const": "group"
}
}
},
"then": {
"required": [
"subtype"
],
"properties": {
"subtype": {
"enum": [
"slide",
"sidebar",
"figure",
"box",
"sheet",
"thread"
]
}
}
}
},
{
"if": {
"required": [
"level"
]
},
"then": {
"properties": {
"type": {
"const": "heading"
}
}
}
},
{
"if": {
"required": [
"html"
]
},
"then": {
"properties": {
"type": {
"const": "table"
}
}
}
},
{
"if": {
"required": [
"latex"
]
},
"then": {
"properties": {
"type": {
"const": "formula"
}
}
}
},
{
"if": {
"required": [
"file"
]
},
"then": {
"properties": {
"type": {
"const": "image"
}
}
}
},
{
"if": {
"required": [
"subtype"
]
},
"then": {
"properties": {
"type": {
"enum": [
"image",
"noise",
"group"
]
}
}
}
}
]
}
}
}
15. Examples
A short leave policy PDF in the storage form:
{"type":"document","version":"1.1","doc":"leave_policy","source":"leave_policy.pdf","sha256":"af179a761f988e6a40d5154b3c3ce735ba94d23ebd82d8e24e3d80de4e2dda68","protection":"none","signed":false,"converter":"docling 2.x","structure":true,"elements":6,"created":"2026-09-26T10:30:00Z","lang":"en","access":["all"],"title":"Leave Policy","last_id":6}
{"id":"leave_policy#e1","hash":"b323ab53d73b48e7","type":"heading","level":1,"parent":null,"page":0,"bbox":[80,60,620,95],"characters":14,"md":"# Leave Policy"}
{"id":"leave_policy#e2","hash":"b91ad9f6b39e617a","type":"paragraph","parent":"leave_policy#e1","page":0,"bbox":[80,110,920,170],"characters":105,"md":"This policy applies to all full-time employees from their first day of work. See [Table 1](#leave-types)."}
{"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"}
{"id":"leave_policy#e5","hash":"8f843b2362f0d607","type":"image","subtype":"stamp","parent":"leave_policy#e1","page":0,"bbox":[700,820,900,940],"characters":66,"confidence":0.91,"file":"images/leave_policy_e5.png","md":"Round blue stamp reading \"Brightpath Technologies, HR Department\"."}
{"id":"leave_policy#e6","hash":"731af9f7a14feaf5","type":"noise","subtype":"footer","parent":"leave_policy#e1","page":0,"bbox":[460,960,540,980],"characters":6,"md":"Page 1"}
The same document in the authoring form:
---
fmjl: "1.1"
doc: leave_policy
title: Leave Policy
lang: en
access: ["all"]
source: leave_policy.pdf
protection: none
signed: false
last_id: 6
---
<!-- e1 page=0 bbox=80,60,620,95 -->
# Leave Policy
<!-- e2 page=0 bbox=80,110,920,170 -->
This policy applies to all full-time employees from their first day of work. See [Table 1](#leave-types).
<!-- 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 page=0 bbox=80,335,400,355 reference=e3 -->
Table 1: Leave types
<!-- e5 subtype=stamp page=0 bbox=700,820,900,940 confidence=0.91 -->

<!-- e6 type=noise subtype=footer page=0 bbox=460,960,540,980 -->
Page 1
16. Tools
The reference tool is the Python package fmjl (pip install fmjl, or python -m fmjl from the repository). It has eleven commands:
| Command | What it does |
|---|---|
fmjl new notes.md |
Turns the authoring form into the storage form, notes.fmjl |
fmjl md notes.fmjl |
Turns the storage form into the authoring form, notes.md |
fmjl fill notes.fmjl |
Fills in id, hash, characters, and parent, and makes md canonical |
fmjl check notes.fmjl |
Checks every rule and prints errors with line numbers |
fmjl view notes.fmjl |
Prints the document as clean Markdown, without notes |
fmjl info notes.fmjl |
Prints the title, element counts, and an outline |
fmjl upgrade old.fmjl |
Turns a version 0.1 to 1.0 file into version 1.1 |
fmjl pdf report.pdf |
Turns a PDF into the storage form and an images/ folder; --md also writes the authoring form |
fmjl docx report.docx |
Turns a Word file into the storage form and an images/ folder; --md also writes the authoring form |
fmjl chunks notes.fmjl |
Prints retriever-ready chunks (section 12.1) as JSON Lines |
fmjl export notes.fmjl --to docx |
Writes the document as clean Markdown, HTML, PDF, Word, OpenDocument, or EPUB |
It needs Python and one package, pip install jsonschema; the PDF importer also needs pip install pymupdf. Export writes what a reader sees: noise is left out, parts linked with continues become one paragraph again, merged-cell tables keep their spans, and formulas become equations where the format has them. Ids, hashes, page, bbox, and access exist only in FMJL and are not exported. Export never replaces an existing file, which is often the original the document came from, unless --force is given. PDF export needs pip install pymupdf; Word, OpenDocument, and EPUB need pandoc. From Python, import fmjl gives load, save, chunks, changed_chunks, and check. The VS Code extension rupakkumar.fmjl converts between both forms without Python and checks every rule while typing. Tools in other languages are welcome; they follow this rulebook, not the Python tool.
17. Version History
- Version 0.1: first draft, with the extension
.jsonl. - Version 0.2: the extension
.fmjland thefmjltool. - Version 0.3: the authoring form; canonical Markdown; IDs that never change; labels; the
grouptype;continues;meta; header fieldstitle,authors,date,summary, andlast_id;elementsmay benullwhile writing; JPG and SVG images; retrieval rules become recommendations; the rulebook becomes the authority over the tool. - Version 0.4: the format is named FMJL, short for Format, Markdown, JSON Lines; the placeholder name "Format X" is retired; the license is MIT. No rules changed, so 0.3 files stay valid.
- Version 0.5: merged cells can be written in a Markdown table with
^and<, and tool-written HTML has one canonical form (sections 8 and 9.2);aboveandbelowin notes (9.3); the group title explained (9.4); themdform of footnotes (7.5);fmjl.names reserved inmeta(7.6); importer rules (10.1); chunks defined (12.1); the tool warns about a table typed without its separator row; thepdf,docx, andchunkscommands. No 0.4 file becomes invalid, sofmjl upgradeonly changes the version. - Version 1.0: the first stable version. No rules changed since 0.5. The
fmjlpackage is on PyPI, the extension on the Marketplace,fmjl.exeon the GitHub release, and this rulebook on the website. From 1.0 on, every 1.x reader reads every 1.x file (section 13). - Version 1.1: importer rules for books and scans, written after converting a 345-page scanned novel and checking five more books (10.1, rules 11 to 20): running headers with changing page numbers are noise, a paragraph split into blocks by the text layer is one element, page-break continuations are found from the layout, chapter lines become headings, missing spaces in OCR text layers are restored, real hyphens are kept, drop caps rejoin their paragraph, sidebars are read as their own stream, and Word text boxes, hidden text, symbols and list levels are handled.
fmjl.bodymarks where the front matter ends (7.6). Parts linked withcontinuesform one chunk, and front matter is not chunked by default (12.1). Thepdfanddocxcommands write only the storage form;--mdadds the authoring form. Theexportcommand andfmjl --versionare new. No 1.0 file becomes invalid, sofmjl upgradeonly changes the version.
Moving from 0.2 to 0.3: run fmjl upgrade. Some md texts change once into canonical form, so their hashes change and those elements are embedded again once.
18. Not Decided Yet
- Recovering LaTeX from formulas in PDF files, where only the drawn characters are available.
- Reading order for pages with three or more columns; the importer handles two columns and sidebars (10.1, rule 16).
- OCR for scanned pages: the path exists and needs Tesseract, but its
confidencevalues have not been checked against real scans.