commit 7d471d22b241d238c547cce1302fda33d48d9672 from: Murilo Ijanc date: Mon Apr 20 20:53:39 2026 UTC render gfm tables commit - 6a0c46aa50e96249f1a1c05ddf19f9c9a6a72e9c commit + 7d471d22b241d238c547cce1302fda33d48d9672 blob - bbff52568021b91084c34eafb4f02da858d4523c blob + 0c306714cffb3c68edb14132f779544946d0e6f7 --- README.md +++ README.md @@ -2,7 +2,8 @@ tmdr - Tiny Markdown Reader ================================== tmdr is a tiny markdown terminal reader written in Rust. It renders headings, paragraphs, lists, code blocks, bold, -italic, inline code, and links (OSC 8) directly in the terminal. +italic, inline code, links (OSC 8), and GFM tables directly +in the terminal. ![tmdr screenshot](tmdr.png) blob - eba3fba58f9ac8c4515984a5efa0c53647ce1349 blob + bb405810110ca95d1db93d6e1e4f77493b4de79b --- example.md +++ example.md @@ -48,6 +48,14 @@ main(void) Visit [OpenBSD](https://www.openbsd.org) for more info. A [link with **bold**](https://example.com) inside a paragraph. +## Tables + +| Left | Center | Right | +|:-------|:------:|------:| +| a | b | c | +| longer | mid | 42 | +| `code` | **bo** | *it* | + ## Headers ### Level 3 blob - 33df2adde1a59722af69e1600471f021f9288f27 blob + 54d9c2a7d8c1734972a9481bac0d2a721d3c21db --- tmdr.rs +++ tmdr.rs @@ -20,14 +20,18 @@ use std::{ io::{self, Read, Write}, }; -use markdown::{ParseOptions, mdast::Node, to_mdast}; +use markdown::{ + ParseOptions, + mdast::{AlignKind, Node, Table}, + to_mdast, +}; fn main() { let args: Vec = env::args().collect(); let (cols, file) = parse_args(&args); let input = read_input(file); - let tree = match to_mdast(&input, &ParseOptions::default()) { + let tree = match to_mdast(&input, &ParseOptions::gfm()) { Ok(t) => t, Err(e) => { eprintln!("tmdr: {}", e); @@ -149,10 +153,75 @@ fn render_block(w: &mut impl Write, node: &Node, cols: render_block(w, child, cols.saturating_sub(2)); } } + Node::Table(t) => render_table(w, t), _ => {} } } +fn render_table(w: &mut impl Write, table: &Table) { + let mut rows: Vec> = Vec::new(); + for child in &table.children { + if let Node::TableRow(r) = child { + let mut row = Vec::new(); + for cell in &r.children { + if let Node::TableCell(c) = cell { + row.push(inline_text(&c.children)); + } + } + rows.push(row); + } + } + if rows.is_empty() { + return; + } + + let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0); + let mut widths = vec![0usize; ncols]; + for row in &rows { + for (i, cell) in row.iter().enumerate() { + widths[i] = widths[i].max(visible_len(cell)); + } + } + + for (ri, row) in rows.iter().enumerate() { + let _ = write!(w, "\u{2502}"); + for (ci, width) in widths.iter().enumerate() { + let empty = String::new(); + let cell = row.get(ci).unwrap_or(&empty); + let align = table.align.get(ci).copied().unwrap_or(AlignKind::None); + let vlen = visible_len(cell); + let extra = width.saturating_sub(vlen); + let (lp, rp) = match align { + AlignKind::Right => (extra, 0), + AlignKind::Center => (extra / 2, extra - extra / 2), + _ => (0, extra), + }; + let _ = write!( + w, + " {}{}{} \u{2502}", + " ".repeat(lp), + cell, + " ".repeat(rp) + ); + } + let _ = writeln!(w); + + if ri == 0 { + let _ = write!(w, "\u{251c}"); + for (ci, width) in widths.iter().enumerate() { + let _ = write!(w, "{}", "\u{2500}".repeat(width + 2)); + let joint = if ci + 1 < widths.len() { + '\u{253c}' + } else { + '\u{2524}' + }; + let _ = write!(w, "{}", joint); + } + let _ = writeln!(w); + } + } +} + fn inline_text(nodes: &[Node]) -> String { let mut out = String::new(); for node in nodes {