PTTJS — A Text‑Based Format for Complex Tables
PTTJS (Plain Text Table JavaScript) is a table format I created to solve my own pain points—and, judging by the feedback I’ve already received, I’m not the only one who has felt the limitations of existing text‑table formats. A JavaScript library with a parser + serializer and an Obsidian plugin are already available. Goal The main goal of PTTJS is to provide a text format that can faithfully store complex tables—far beyond what CSV or Markdown can handle—while still remaining human‑readable. Motivation I tolerated the shortcomings of other formats for a long time, but eventually the trade‑offs became impossible to ignore. Key drivers were: Feeding richer tables to LLMs. Documents often contain intricate, merged‑cell tables that can’t be flattened to CSV/Markdown, forcing us to reach for heavier multimodal models. Extracting tables via CV into plain text, not into a deeply nested JSON blob that’s unreadable until you re‑render it. Letting an LLM manipulate an entire table natively—rows, columns, formulas—without shoe‑horning everything through Google Sheets or Excel. Opening tables without special software. Most tables end up trapped in XLS(X) or ODT; PTTJS keeps them plain text. Editing complex tables in plain‑text tools like Obsidian. Personal dislike of Google Sheets/Excel (surely I’m not the only one!). the problem is indicated in points 1 and 2, the format is recognized normally and inserted into the text Format Overview The very first line is always an annotation: |PTTJS 1.0|encoding=UTF‑8| Pages A table can span multiple pages: |(@P1|Page Name){ …table data… }| Page markers are optional; omit them and the whole table lives on a single page. Cells Every cell starts with | and ends with >: |H([1|1]1|1|@C1)> H — optional marks a header cell ([X|Y]) — zero‑based cell index (optional; auto‑filled if absent) (X|Y) — cell span (defaults to 1|1; always declared in the top‑left merged cell) (@ID) — cell id for references Row ends with: < { }) must be URL‑encoded: \n → %5Cn | → %7C > → %3E < → %3C { → %7B } → %7D The library provides escapeValue / unescapeValue helpers. Scripts >>>SCRIPT …typings, formulas, styling… Make & Model080XXX02|>2005|>LEXUS RX 350787XXX16|>2015|>GEELY GC7871XXX05|>1997|>TOYOTA IPSUMA602XXX|>1996|>MITSUBISHI PAJERO890XXX02|>1997|>TOYOTA LAND CRUISER PRADO216XXX13|>2007|>DAEWOO NEXIAPlate Number|H([1|0])>Year|H([2|0])>Make & Model080XXX02|([1|1])>2007|([2|1])>LEXUS RX 350Plate Number|H(2|1)>Vehicle Data|H>|H>Year|H>Make & Model080XXX02|>2007|>LEXUS RX 350Plate Number|H([1|0])>Year|H([2|0])>Make & Model080XXX02|([1|1])>2007|([2|1])>LEXUS RX 350Average Year|([1|8])>2003>>SCRIPT (1|1,1|6)=>NUMBER(2,' ') (1|8)=DIV(SUM(1|1,1|6),COUNT(1|1,1|6)) (0|8:8)

PTTJS (Plain Text Table JavaScript) is a table format I created to solve my own pain points—and, judging by the feedback I’ve already received, I’m not the only one who has felt the limitations of existing text‑table formats.
A JavaScript library with a parser + serializer and an Obsidian plugin are already available.
Goal
The main goal of PTTJS is to provide a text format that can faithfully store complex tables—far beyond what CSV or Markdown can handle—while still remaining human‑readable.
Motivation
I tolerated the shortcomings of other formats for a long time, but eventually the trade‑offs became impossible to ignore. Key drivers were:
- Feeding richer tables to LLMs. Documents often contain intricate, merged‑cell tables that can’t be flattened to CSV/Markdown, forcing us to reach for heavier multimodal models.
- Extracting tables via CV into plain text, not into a deeply nested JSON blob that’s unreadable until you re‑render it.
- Letting an LLM manipulate an entire table natively—rows, columns, formulas—without shoe‑horning everything through Google Sheets or Excel.
- Opening tables without special software. Most tables end up trapped in XLS(X) or ODT; PTTJS keeps them plain text.
- Editing complex tables in plain‑text tools like Obsidian.
- Personal dislike of Google Sheets/Excel (surely I’m not the only one!).
the problem is indicated in points 1 and 2, the format is recognized normally and inserted into the text
Format Overview
The very first line is always an annotation:
|PTTJS 1.0|encoding=UTF‑8|
Pages
A table can span multiple pages:
|(@P1|Page Name){
…table data…
}|
Page markers are optional; omit them and the whole table lives on a single page.
Cells
Every cell starts with |
and ends with >
:
|H([1|1]1|1|@C1)>
-
H
— optional marks a header cell -
([X|Y])
— zero‑based cell index (optional; auto‑filled if absent) -
(X|Y)
— cell span (defaults to1|1
; always declared in the top‑left merged cell) -
(@ID)
— cell id for references
Row ends with:
<|
If a row is otherwise empty but you still have rows below it, include at least one empty cell: |><|
.
Forbidden characters inside cell content (\n | > < { }
) must be URL‑encoded:
\n → %5Cn
| → %7C
> → %3E
< → %3C
{ → %7B
} → %7D
The library provides escapeValue
/ unescapeValue
helpers.
Scripts
>>>SCRIPT
…typings, formulas, styling…
<<
- Always placed after all table data.
- Can work across pages.
- Use
<=
for CSS‑like styles. - Use
=>
for cell typings. - Use
=
for formulas. - Target a range with
:
—e.g.0:0|0
(whole first row) or0|0:0
(whole first column).
The cells always contain up-to-date information. When we add a script, it recalculates the table data and adds the up-to-date information to the cells.
Examples
1 – Basic Table
|PTTJS 1.0|
|H>Plate Number|H>Year|H>Make & Model<|
|>080XXX02|>2005|>LEXUS RX 350<|
|>787XXX16|>2015|>GEELY GC7<|
|>871XXX05|>1997|>TOYOTA IPSUM<|
|>A602XXX|>1996|>MITSUBISHI PAJERO<|
|>890XXX02|>1997|>TOYOTA LAND CRUISER PRADO<|
|>216XXX13|>2007|>DAEWOO NEXIA<|
2 – With Explicit Indexes
|PTTJS 1.0|encoding=UTF‑8|
|H([0|0])>Plate Number|H([1|0])>Year|H([2|0])>Make & Model<|
|([0|1])>080XXX02|([1|1])>2007|([2|1])>LEXUS RX 350<|
…
3 – Complex Header (Merged Cells)
|PTTJS 1.0|encoding=UTF‑8|
|H(1|2)>Plate Number|H(2|1)>Vehicle Data|H><|
|H>|H>Year|H>Make & Model<|
|>080XXX02|>2007|>LEXUS RX 350<|
…
visualization from Obsidian
4 – With Scripts
|PTTJS 1.0|encoding=UTF‑8|
|H([0|0])>Plate Number|H([1|0])>Year|H([2|0])>Make & Model<|
|([0|1])>080XXX02|([1|1])>2007|([2|1])>LEXUS RX 350<|
…
|([0|7])><|
|([0|8])>Average Year|([1|8])>2003<|
>>>SCRIPT
(1|1,1|6)=>NUMBER(2,' ')
(1|8)=DIV(SUM(1|1,1|6),COUNT(1|1,1|6))
(0|8:8)<=BORDER(each,2,solid,#000)
<<
Row 9 calculates the average year; numeric formatting is applied; a black 2 px border is added to the "Average Year" label cell.
Roadmap
- Execute script functions directly on the parsed Store object.
- Import/export converters (XLS(X), ODT, CSV, MD ↔ PTTJS).
- Libraries in other languages.
- Lightweight web UI for editing PTTJS.
Contributing
The GitHub repo is public—PRs and ideas are very welcome!
Final Words
Thanks for reading! I hope PTTJS helps you tackle the same table woes I faced. Feel free to reach out, fork the repo, or open an issue—let’s make complex tables easy together.