Libraries
LTEM comes with libraries in five languages: Python, TypeScript, Rust, Go and Lua. Each language has a core, and plugins connect LTEM to the Markdown parsers people already use. All libraries are at version 0.1, implement LTEM 1.0 and pass the conformance tests; the CodeMirror packages have their own version numbers.
A core parses LTEM into the table model, validates a table against the specification's rules, renders HTML with real colspan and rowspan, and writes a table back as canonical LTEM. The Rust, TypeScript and Python cores also convert HTML tables to LTEM. A plugin replaces the host parser's table rule: LTEM handles the table structure, and the host parser still handles the text inside each cell, such as emphasis, links, code and math.
| Language | Package | What it is |
|---|---|---|
| JavaScript / TypeScript | ltem-core | Core and ltem command |
markdown-it-ltem | markdown-it plugin, also for VitePress | |
marked-ltem | marked extension · coming soon | |
remark-ltem | remark and unified plugin · coming soon | |
codemirror-ltem | CodeMirror 6 table editing and live preview · coming soon | |
| Python | ltem-core | Core and ltem command, the reference |
mdit-py-ltem | markdown-it-py plugin · coming soon | |
markdown-ltem | Python-Markdown extension, also for MkDocs · coming soon | |
| Rust | ltem | Core crate · coming soon |
ltem-cli | The ltem command · coming soon | |
pulldown-cmark-ltem | pulldown-cmark adapter · coming soon | |
mdbook-ltem | mdBook preprocessor · coming soon | |
comrak-ltem | comrak adapter · coming soon | |
| Go | ltem | Core package and ltem command |
goldmarkltem | goldmark extension | |
| Lua | ltem.lua | Core in pure Lua 5.4 |
| Pandoc readers and filter | Pandoc | |
| Quarto extension | Quarto |
JavaScript / TypeScript
ltem-core
The TypeScript core, with the ltem command. It has no dependencies; add markdown-it to render inline Markdown in cells.
npm install ltem-coreimport { parseTable, renderHtml, write } from "ltem-core";
const result = parseTable(`\
| Region | Revenue | << |
| ^^ | Q1 | Q2 |
|--------|---------|-----|
| Asia | 100 | 120 |`);
const table = result.table!; // the table model (see ast/ in the LTEM repository)
JSON.stringify(table); // the model as JSON, valid against ast/table.schema.json
renderHtml(table); // <table> with colspan and rowspan
write(table); // canonical LTEM
result.problems; // validation problems, if anymarkdown-it-ltem
A plugin for markdown-it 14 and 15. It replaces markdown-it's table rule; a table without LTEM notation renders as before.
npm install markdown-it markdown-it-ltemimport markdownit from "markdown-it";
import ltem from "markdown-it-ltem";
const md = markdownit().use(ltem);
md.render(text);VitePress renders Markdown with markdown-it. This site is built this way:
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import ltem from 'markdown-it-ltem'
export default defineConfig({
markdown: {
config(md) {
md.use(ltem)
},
},
})marked-ltem
An extension for marked 17 and 18. It needs no DOM, so it also runs in browsers through a bundler.
import { marked } from "marked";
import markedLtem from "marked-ltem";
marked.use(markedLtem());
marked.parse(text);remark-ltem
A remark plugin, with or without remark-gfm. Pass ltemHandlers to remark-rehype, or the merged cells are lost on the way to HTML. Underneath are micromark-extension-ltem, the syntax extension, and mdast-util-ltem, the mdast utilities, which the rest of the unified ecosystem can use directly.
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkLtem, { ltemHandlers } from "remark-ltem";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
const file = await unified()
.use(remarkParse)
.use(remarkGfm) // optional
.use(remarkLtem)
.use(remarkRehype, { handlers: ltemHandlers }) // required for merged cells
.use(rehypeStringify)
.process(text);
String(file);codemirror-ltem
A CodeMirror 6 component that recognizes and checks LTEM tables and lets people edit them as tables: a table widget, merging and splitting cells, and pasting from Excel and the web with merged cells kept. codemirror-ltem-live builds a live preview of the whole Markdown document on it. Cellmark Editor uses both.
import { EditorView, basicSetup } from "codemirror";
import { ltem } from "codemirror-ltem";
new EditorView({
doc: "| Region | Revenue | << |\n| ^^ | Q1 | Q2 |\n|--------|---------|-----|\n| Asia | 100 | 120 |\n",
extensions: [basicSetup, ltem()],
parent: document.body,
});Python
ltem-core
The reference implementation, with the ltem command. It is installed as ltem-core and imported as ltem. It has no dependencies; the markdown extra (pip install "ltem-core[markdown]") renders inline Markdown in cells with markdown-it-py.
pip install ltem-coreimport ltem
result = ltem.parse_table("""\
| Region | Revenue | << |
| ^^ | Q1 | Q2 |
|--------|---------|-----|
| Asia | 100 | 120 |""")
table = result.table # the table model (see ast/ in the LTEM repository)
table.to_dict() # JSON-ready dict
ltem.render_html(table) # <table> with colspan and rowspan
ltem.write(table) # canonical LTEM
result.problems # validation problems, if any
for item in ltem.from_html(html): # HTML tables -> table models
print(ltem.write(item.table), item.warnings)mdit-py-ltem
A plugin for markdown-it-py.
from markdown_it import MarkdownIt
from mdit_py_ltem import ltem_plugin
md = MarkdownIt("commonmark").use(ltem_plugin)markdown-ltem
An extension for Python-Markdown, and so for MkDocs.
import markdown
html = markdown.markdown(text, extensions=["ltem"])In mkdocs.yml:
markdown_extensions:
- ltemRust
ltem
The Rust core. The optional feature commonmark-inline renders the text of cells as CommonMark inlines with pulldown-cmark.
let doc = "| Region | Revenue | << |\n| ^^ | Q1 | Q2 |\n|--------|---------|-----|\n| Asia | 100 | 120 |\n";
for result in ltem::find_tables(doc) {
let table = result.table.expect("find_tables returns tables only");
for problem in &result.problems {
println!("{problem}"); // for example "V13 at (1,0): '<<' has no cell to extend"
}
let html = ltem::render_html(&table, None); // plain inline rendering
let canonical = ltem::write(&table).unwrap(); // canonical LTEM
let json = table.to_json_pretty(); // the table model as JSON
}ltem-cli
The ltem command, built on the Rust core with CommonMark inline rendering.
ltem parse [FILE] print the table model of every table as JSON
ltem render [FILE] print the HTML of every table
ltem validate [FILE] [--json] print validation problems; exit status 1 if there are any
ltem fmt [FILE] rewrite the input with every table in canonical form
ltem from-html [FILE] [--header-rows N] [--plain] convert HTML tables to canonical LTEMpulldown-cmark-ltem
An adapter for pulldown-cmark: LTEM tables come out as HTML blocks, everything else from pulldown-cmark.
use pulldown_cmark::{html, Options};
let parser = pulldown_cmark_ltem::Parser::new_ext(markdown, Options::ENABLE_FOOTNOTES);
let mut out = String::new();
html::push_html(&mut out, parser);mdbook-ltem
A preprocessor for mdBook 0.4 and 0.5. Install it, then add it to book.toml:
[preprocessor.ltem]
after = ["links"]comrak-ltem
An adapter for comrak, the engine behind GitLab and crates.io: LTEM tables in comrak's AST.
use comrak::Options;
let mut options = Options::default();
options.extension.strikethrough = true;
let html = comrak_ltem::markdown_to_html(markdown, &options);Go
ltem
The Go core uses only the standard library. The module github.com/hugman/ltem/ltem-lib/go also holds the ltem command and the goldmark extensions; it needs Go 1.25 or later.
go get github.com/hugman/ltem/ltem-lib/go@latestimport "github.com/hugman/ltem/ltem-lib/go/ltem"
for _, r := range ltem.FindTables(markdown) {
for _, p := range r.Problems {
fmt.Println(p) // for example "V13 at (1,0): '<<' has no cell to extend"
}
html := ltem.RenderHTML(r.Table, nil) // <table> with colspan and rowspan
text, _ := ltem.Write(r.Table) // canonical LTEM
model, _ := json.Marshal(r.Table) // the table model (ast/)
}goldmarkltem
Extensions for goldmark v1 (goldmarkltem) and v2 (goldmarkltem/v2).
import "github.com/hugman/ltem/ltem-lib/go/goldmarkltem"import (
"github.com/yuin/goldmark"
"github.com/hugman/ltem/ltem-lib/go/goldmarkltem"
)
md := goldmark.New(goldmark.WithExtensions(goldmarkltem.Extension))
var buf bytes.Buffer
err := md.Convert(source, &buf)Lua, Pandoc and Quarto
ltem.lua
The core in pure Lua 5.4, without C code. It runs in any Lua 5.4, including Pandoc's.
local ltem = require("ltem")
for _, result in ipairs(ltem.find_tables(text)) do
local t = result.table -- the table model
for _, p in ipairs(result.problems) do
print(p.rule, p.row, p.col, p.message) -- for example: V13 1 0 '<<' has no cell to extend
end
print(ltem.render_html(t)) -- HTML with colspan and rowspan
print(ltem.write(t)) -- canonical LTEM
endPandoc readers and filter
Two Pandoc readers read Pandoc's Markdown or commonmark_x with LTEM tables, into any format Pandoc writes. They need Pandoc 3, tested with 3.9.
pandoc -f ltem-markdown.lua in.md -o out.docxpandoc -f ltem-lib/lua/pandoc/ltem-markdown.lua input.md -o output.html
pandoc -f ltem-lib/lua/pandoc/ltem-commonmark_x.lua input.md -o output.docxTo keep your input format, the filter turns code blocks with the class ltem into tables:
pandoc -L ltem-lib/lua/pandoc/ltem-filter.lua input.md -o output.htmlQuarto extension
Quarto takes the filter as an extension; it cannot use the readers. Copy the folder ltem-lib/lua/_extensions/ltem into your project's _extensions folder and add:
filters:
- ltemWhere LTEM does not render yet
GitHub, GitLab and most chat interfaces render Markdown with their own engines, which do not know LTEM yet. There an LTEM table shows as a plain GFM table, and some of it can look wrong. In renderers without LTEM support shows what to expect and how to write tables that still read well.