feat: render fixed-width text for nex

This commit is contained in:
randogoth 2026-10-04 20:27:29 +03:00
parent 2f38b8b929
commit 9e1cfed284
19 changed files with 1160 additions and 44 deletions

View file

@ -12,6 +12,7 @@
use itsybitsy_core::Error;
use itsybitsy_core::ir::{Block, Doc, Inline};
use itsybitsy_core::render::{RenderCtx, Rendered, Renderer};
use itsybitsy_core::wrap::{display_width, pad};
pub struct Gemtext;
@ -175,7 +176,7 @@ impl Writer {
let widths: Vec<usize> = (0..columns)
.map(|index| {
all.iter()
.map(|row| row.get(index).map_or(0, |c| c.chars().count()))
.map(|row| row.get(index).map_or(0, |cell| display_width(cell)))
.max()
.unwrap_or(0)
})
@ -183,8 +184,9 @@ impl Writer {
self.line("```");
for (index, row) in all.iter().enumerate() {
// Padded by display width, so a wide character does not skew its column.
let cells: Vec<String> =
row.iter().zip(&widths).map(|(cell, width)| format!("{cell:<width$}")).collect();
row.iter().zip(&widths).map(|(cell, width)| pad(cell, *width)).collect();
self.line(&cells.join(" "));
// A rule under the header row, matching the padded column widths.
if index == 0 && !head.is_empty() {
@ -274,20 +276,29 @@ fn quoted(blocks: &[Block]) -> Vec<String> {
}
#[cfg(test)]
mod tests {
mod tests_support {
use itsybitsy_core::config::PageSettings;
use itsybitsy_core::parse;
use super::*;
/// Render a Markdown fragment the way the server would.
fn render(markdown: &str) -> String {
pub fn render(markdown: &str) -> String {
let doc = parse::markdown(markdown);
let settings = PageSettings::default();
let ctx = RenderCtx { url: "/x", title: "T", settings: &settings, width: None };
let out = Gemtext.render(&doc, &ctx).unwrap();
String::from_utf8(out.body).unwrap()
}
}
#[cfg(test)]
mod tests {
use itsybitsy_core::config::PageSettings;
use itsybitsy_core::parse;
use super::tests_support::render;
use super::*;
#[test]
fn headings_flatten_onto_three_levels() {
@ -387,3 +398,22 @@ mod tests {
}
}
}
#[cfg(test)]
mod width_tests {
use itsybitsy_core::wrap::display_width;
use super::tests_support::render;
#[test]
fn table_columns_align_by_display_width_not_character_count() {
// A CJK ideograph is two columns wide; counting characters would leave
// the column a column short for each one.
let out = render("| a | b |\n| --- | --- |\n| 日本 | x |\n");
let lines: Vec<&str> = out.lines().collect();
// Compared in columns, not bytes: a multibyte cell shifts byte offsets
// without shifting where the text actually appears.
let column = |line: &str, ch: char| display_width(&line[..line.find(ch).unwrap()]);
assert_eq!(column(lines[1], 'b'), column(lines[3], 'x'), "{out}");
}
}