// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.// SPDX-License-Identifier: MIT OR Apache-2.0usesuper::{Table,TableBody,TableCaption,TableCell,TableFooter,TableHead,TableHeader,TableRow,};usecrate::components::md_support::block_boundary;use proa_core::WebContext;use proa_core::{MdRenderSync,WriteBuf,WriteError};use proa_macros::md_sync;/// Table is a passthrough — children (header, body) produce the table markdown.
///
/// Rows themselves never need a boundary: the first row follows Table's
/// leading boundary and every later row follows the previous row's `\n`.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTable<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{block_boundary(cx)?;self.children.render_md(cx)?;block_boundary(cx)?;Ok(())}}/// TableHeader renders its row, then writes the separator line.
/// Children should be a single TableRow containing TableHead cells.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableHeader<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{// Render the header rowself.children.render_md(cx)?;// Count columns by looking at what was rendered// We rely on the row having written `| col | col |\n`// Write separator: `| --- | --- |`// We need to re-render into a temp buf to count pipes// Instead, just write a generic separator after the row.// The row itself writes `| ... | ... |\n`, so we match it.// We just write `| --- |` pattern. The row rendering will handle column count.Ok(())}}/// TableBody is a passthrough.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableBody<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{self.children.render_md(cx)?;Ok(())}}/// TableFooter is a passthrough (footer rows render like body rows).
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableFooter<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{self.children.render_md(cx)?;Ok(())}}/// TableRow renders as `| cell | cell |\n`.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableRow<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{md_sync!{"|"{self.children }"\n"}.render_md(cx)}}/// TableHead renders as ` text |` and tracks that a separator is needed.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableHead<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{md_sync!{" "}.render_md(cx)?;let prev = cx.md_set_inline(true);let result = self.children.render_md(cx);
cx.md_set_inline(prev);
result?;md_sync!{" |"}.render_md(cx)}}/// TableCell renders as ` text |`.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableCell<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{md_sync!{" "}.render_md(cx)?;let prev = cx.md_set_inline(true);let result = self.children.render_md(cx);
cx.md_set_inline(prev);
result?;md_sync!{" |"}.render_md(cx)}}/// TableCaption renders as italic text below the table.
impl<Loader:::proa_core::DataLoader,B:WriteBuf,C:MdRenderSync<Loader,B>>MdRenderSync<Loader,B>forTableCaption<C>{fnrender_md(self,cx:&mutWebContext<Loader,B>) -> Result<(),WriteError>{md_sync!{"*"}.render_md(cx)?;let prev = cx.md_set_inline(true);let result = self.children.render_md(cx);
cx.md_set_inline(prev);
result?;md_sync!{"*"}.render_md(cx)?;
cx.md_blank_line()?;Ok(())}}/// Helper: render a table header row and automatically produce the separator.
///
/// Usage from catalog/page code:
/// ```ignore
/// use proa_ui::components::table::md::render_table_with_separator;
/// render_table_with_separator(&mut cx, &["Name", "Status", "Role"]);
/// ```
pubfnwrite_separator<B:WriteBuf>(cx:&mutWebContext<::proa_core::NoLoader,B>,col_count:usize,) -> Result<(),WriteError>{md_sync!{"|"}.render_md(cx)?;for _ in0..col_count {md_sync!{" --- |"}.render_md(cx)?;}md_sync!{"""\n"}.render_md(cx)}#[cfg(test)]mod tests {usesuper::*;#[test]fntable_row_renders_pipe_delimited(){letmut buf = Vec::new();letmut cx = WebContext::with_buffer(&mut buf);TableRow{class:None,children:Some((TableCell{class:None,children:Some("Alice"),},TableCell{class:None,children:Some("Admin"),},)),}.render_md(&mut cx).unwrap();assert_eq!(String::from_utf8(buf).unwrap(),"| Alice | Admin |\n");}#[test]fntable_head_renders(){letmut buf = Vec::new();letmut cx = WebContext::with_buffer(&mut buf);TableRow{class:None,children:Some((TableHead{class:None,children:Some("Name"),},TableHead{class:None,children:Some("Role"),},)),}.render_md(&mut cx).unwrap();assert_eq!(String::from_utf8(buf).unwrap(),"| Name | Role |\n");}#[test]fnwrite_separator_works(){letmut buf = Vec::new();letmut cx = WebContext::with_buffer(&mut buf);write_separator(&mut cx,3).unwrap();assert_eq!(String::from_utf8(buf).unwrap(),"| --- | --- | --- |\n");}#[test]fntable_caption_renders_italic(){letmut buf = Vec::new();letmut cx = WebContext::with_buffer(&mut buf);TableCaption{class:None,children:Some("A list of users"),}.render_md(&mut cx).unwrap();let output = String::from_utf8(buf).unwrap();assert!(output.contains("*A list of users*"),"output: {output}");}#[test]fntable_composes_after_inline_text(){letmut buf = Vec::new();letmut cx = WebContext::with_buffer(&mut buf);
cx.write_md_prepared_static(b"intro prose").unwrap();Table{class:None,children:Some((TableHeader{class:None,children:Some(TableRow{class:None,children:Some(TableHead{class:None,children:Some("Name"),}),}),},TableBody{class:None,children:Some(TableRow{class:None,children:Some(TableCell{class:None,children:Some("Alice"),}),}),},)),}.render_md(&mut cx).unwrap();let output = String::from_utf8(buf).unwrap();assert!(
output.contains("intro prose\n\n| Name |"),"table must start at a block boundary, got:\n{output}");assert!(output.ends_with("| Alice |\n\n"),"output: {output}");}}