Skip to content

Instantly share code, notes, and snippets.

@Zomatree
Created June 15, 2026 12:41
Show Gist options
  • Select an option

  • Save Zomatree/a11b86224097942b7b3f2651bf555420 to your computer and use it in GitHub Desktop.

Select an option

Save Zomatree/a11b86224097942b7b3f2651bf555420 to your computer and use it in GitHub Desktop.
use std::{borrow::Cow, mem};
use chumsky::{Parser as _, prelude::*};
use freya::{icons::lucide::hash, prelude::*};
use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd};
define_theme! {
%[component]
pub MarkdownViewer {
%[fields]
color: Color,
color_link: Color,
background_code: Color,
color_code: Color,
background_blockquote: Color,
border_blockquote: Color,
background_divider: Color,
color_primary: Color,
background_primary: Color,
color_secondary: Color,
background_secondary: Color,
font_size: f32,
}
}
#[derive(PartialEq)]
pub struct MarkdownViewer {
content: Cow<'static, str>,
layout: LayoutData,
key: DiffKey,
pub(crate) theme: Option<MarkdownViewerThemePartial>,
}
impl MarkdownViewer {
pub fn new(content: impl Into<Cow<'static, str>>) -> Self {
Self {
content: content.into(),
layout: LayoutData::default(),
key: DiffKey::None,
theme: None,
}
}
}
impl KeyExt for MarkdownViewer {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl LayoutExt for MarkdownViewer {
fn get_layout(&mut self) -> &mut LayoutData {
&mut self.layout
}
}
impl ContainerExt for MarkdownViewer {}
/// Represents different markdown elements for rendering.
#[allow(dead_code)]
#[derive(Clone)]
enum MarkdownElement {
Heading {
level: HeadingLevel,
content: Vec<Inline>,
},
Paragraph {
content: Vec<Inline>,
},
CodeBlock {
code: String,
#[allow(dead_code)]
language: Option<String>,
},
UnorderedList {
items: Vec<Vec<Inline>>,
},
OrderedList {
start: u64,
items: Vec<Vec<Inline>>,
},
Image {
url: String,
alt: String,
},
Link {
url: String,
title: Option<String>,
text: Vec<Inline>,
},
Blockquote {
content: Vec<Inline>,
},
Table {
headers: Vec<Vec<Inline>>,
rows: Vec<Vec<Vec<Inline>>>,
},
HorizontalRule,
}
/// A piece of a paragraph's content: styled text or an inline link flowing within the text.
#[derive(Clone, Debug)]
enum Inline {
Span(TextSpan),
UserMention {
avatar: String,
name: String,
fill: Option<Fill>,
},
ChannelMention {
name: String,
},
RoleMention {
fill: Option<Fill>,
name: String,
},
Emoji {
id: String,
},
Link {
url: String,
title: Option<String>,
text: Vec<Inline>,
},
}
/// Represents styled text spans within markdown.
#[derive(Clone, Debug)]
struct TextSpan {
text: String,
bold: bool,
italic: bool,
#[allow(dead_code)]
strikethrough: bool,
code: bool,
}
impl TextSpan {
fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
bold: false,
italic: false,
strikethrough: false,
code: false,
}
}
}
/// Parse markdown content into a list of elements.
fn parse_markdown(content: &str) -> Vec<MarkdownElement> {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(content, options);
let mut elements = Vec::new();
let mut current_content: Vec<Inline> = Vec::new();
let mut list_items: Vec<Vec<Inline>> = Vec::new();
let mut current_list_item: Vec<Inline> = Vec::new();
let mut in_heading: Option<HeadingLevel> = None;
let mut in_paragraph = false;
let mut in_code_block = false;
let mut code_block_content = String::new();
let mut code_block_language: Option<String> = None;
let mut ordered_list_start: Option<u64> = None;
let mut in_list_item = false;
let mut in_blockquote = false;
let mut blockquote_spans: Vec<Inline> = Vec::new();
let mut in_table_cell = false;
let mut table_headers: Vec<Vec<Inline>> = Vec::new();
let mut table_rows: Vec<Vec<Vec<Inline>>> = Vec::new();
let mut current_table_row: Vec<Vec<Inline>> = Vec::new();
let mut current_cell_spans: Vec<Inline> = Vec::new();
let mut in_link = false;
let mut link_url: Option<String> = None;
let mut link_title: Option<String> = None;
let mut link_spans: Vec<Inline> = Vec::new();
let mut bold = false;
let mut italic = false;
let mut strikethrough = false;
for event in parser {
match event {
Event::Start(tag) => match tag {
Tag::Heading { level, .. } => {
in_heading = Some(level);
current_content.clear();
}
Tag::Paragraph => {
if in_blockquote {
// Paragraphs inside blockquotes
} else if in_list_item {
// Paragraphs inside list items
} else {
in_paragraph = true;
current_content.clear();
}
}
Tag::CodeBlock(kind) => {
in_code_block = true;
code_block_content.clear();
code_block_language = match kind {
pulldown_cmark::CodeBlockKind::Fenced(lang) => {
let lang_str = lang.to_string();
if lang_str.is_empty() {
None
} else {
Some(lang_str)
}
}
pulldown_cmark::CodeBlockKind::Indented => None,
};
}
Tag::List(start) => {
ordered_list_start = start;
list_items.clear();
}
Tag::Item => {
in_list_item = true;
current_list_item.clear();
}
Tag::Strong => bold = true,
Tag::Emphasis => italic = true,
Tag::Strikethrough => strikethrough = true,
Tag::BlockQuote(_) => {
in_blockquote = true;
blockquote_spans.clear();
}
Tag::Image {
dest_url, title, ..
} => {
elements.push(MarkdownElement::Image {
url: dest_url.to_string(),
alt: title.to_string(),
});
}
Tag::Link {
dest_url, title, ..
} => {
in_link = true;
link_url = Some(dest_url.to_string());
link_title = Some(title.to_string());
link_spans.clear();
}
Tag::Table(_) => {
table_headers.clear();
table_rows.clear();
current_table_row.clear();
}
Tag::TableHead => {}
Tag::TableRow => {
current_table_row.clear();
}
Tag::TableCell => {
in_table_cell = true;
current_cell_spans.clear();
}
_ => {}
},
Event::End(tag_end) => match tag_end {
TagEnd::Heading(_) => {
if let Some(level) = in_heading.take() {
elements.push(MarkdownElement::Heading {
level,
content: mem::take(&mut current_content),
});
}
}
TagEnd::Paragraph => {
if in_blockquote {
blockquote_spans.append(&mut current_content)
} else if in_list_item {
current_list_item.append(&mut current_content)
} else if in_paragraph {
in_paragraph = false;
elements.push(MarkdownElement::Paragraph {
content: mem::take(&mut current_content),
});
}
}
TagEnd::CodeBlock => {
in_code_block = false;
elements.push(MarkdownElement::CodeBlock {
code: mem::take(&mut code_block_content),
language: code_block_language.take(),
});
}
TagEnd::List(_) => {
let items = mem::take(&mut list_items);
if let Some(start) = ordered_list_start.take() {
elements.push(MarkdownElement::OrderedList { start, items });
} else {
elements.push(MarkdownElement::UnorderedList { items });
}
}
TagEnd::Item => {
in_list_item = false;
list_items.push(mem::take(&mut current_list_item));
}
TagEnd::Strong => bold = false,
TagEnd::Emphasis => italic = false,
TagEnd::Strikethrough => strikethrough = false,
TagEnd::BlockQuote(_) => {
in_blockquote = false;
elements.push(MarkdownElement::Blockquote {
content: mem::take(&mut blockquote_spans),
});
}
TagEnd::Table => {
elements.push(MarkdownElement::Table {
headers: mem::take(&mut table_headers),
rows: mem::take(&mut table_rows),
});
}
TagEnd::TableHead => {
// TableHead contains cells directly (no TableRow), so save headers here
table_headers = mem::take(&mut current_table_row);
}
TagEnd::TableRow => {
// TableRow only appears in body rows, not in TableHead
table_rows.push(mem::take(&mut current_table_row));
}
TagEnd::TableCell => {
in_table_cell = false;
current_table_row.push(mem::take(&mut current_cell_spans));
}
TagEnd::Link => {
in_link = false;
if let Some(url) = link_url.take() {
let title = link_title.take();
let text = mem::take(&mut link_spans);
if in_paragraph {
current_content.push(Inline::Link { url, title, text });
} else {
elements.push(MarkdownElement::Link { url, title, text });
}
}
}
_ => {}
},
Event::Text(text) => {
if in_code_block {
code_block_content.push_str(text.trim());
} else if in_table_cell {
let text = if let Some(Inline::Span(last)) = current_cell_spans.last() {
let output = last.text.to_string() + &*text;
current_cell_spans.pop();
output
} else {
text.to_string()
};
current_cell_spans.extend(parse_elements(&text, bold, italic, strikethrough));
} else {
if in_blockquote && !in_paragraph {
let text = if let Some(Inline::Span(last)) = blockquote_spans.last() {
let output = last.text.to_string() + &*text;
blockquote_spans.pop();
output
} else {
text.to_string()
};
blockquote_spans.extend(parse_elements(&text, bold, italic, strikethrough));
} else if in_list_item && !in_paragraph {
let text = if let Some(Inline::Span(last)) = current_list_item.last() {
let output = last.text.to_string() + &*text;
current_list_item.pop();
output
} else {
text.to_string()
};
current_list_item.extend(parse_elements(
&text,
bold,
italic,
strikethrough,
));
} else if in_link {
let text = if let Some(Inline::Span(last)) = link_spans.last() {
let output = last.text.to_string() + &*text;
link_spans.pop();
output
} else {
text.to_string()
};
link_spans.extend(parse_elements(&text, bold, italic, strikethrough));
} else {
let text = if let Some(Inline::Span(last)) = current_content.last() {
let output = last.text.to_string() + &*text;
current_content.pop();
output
} else {
text.to_string()
};
current_content.extend(parse_elements(&text, bold, italic, strikethrough));
}
}
}
Event::Code(code) => {
let span = Inline::Span(TextSpan {
text: code.to_string(),
bold,
italic,
strikethrough,
code: true,
});
if in_table_cell {
current_cell_spans.push(span);
} else if in_blockquote {
blockquote_spans.push(span);
} else if in_list_item {
current_list_item.push(span);
} else if in_link {
link_spans.push(span);
} else {
current_content.push(span);
}
}
Event::SoftBreak | Event::HardBreak => {
let span = Inline::Span(TextSpan::new("\n"));
if in_blockquote {
blockquote_spans.push(span);
} else if in_list_item {
current_list_item.push(span);
} else if in_link {
link_spans.push(span);
} else {
current_content.push(span);
}
}
Event::Rule => {
elements.push(MarkdownElement::HorizontalRule);
}
_ => {}
}
}
elements
}
#[derive(Debug, Clone)]
enum InlineParserElement {
Text(String),
UserMention(String),
ChannelMention(String),
RoleMention(String),
Emoji(String),
Link {
url: String,
title: Option<String>,
text: String,
},
}
fn id_parser<'a>() -> impl chumsky::Parser<'a, &'a str, String> {
one_of("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
.repeated()
.exactly(26)
.collect()
}
fn user_mention_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
just("@")
.ignore_then(id_parser())
.delimited_by(just("<"), just(">"))
.map(InlineParserElement::UserMention)
}
fn channel_mention_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
just("#")
.ignore_then(id_parser())
.delimited_by(just("<"), just(">"))
.map(InlineParserElement::ChannelMention)
}
fn role_mention_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
just("%")
.ignore_then(id_parser())
.delimited_by(just("<"), just(">"))
.map(InlineParserElement::RoleMention)
}
fn emoji_mention_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
id_parser()
.delimited_by(just(":"), just(":"))
.map(InlineParserElement::Emoji)
}
fn link_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
none_of(']')
.repeated()
.collect()
.delimited_by(just("["), just("]"))
.then(
none_of(" )")
.repeated()
.collect::<String>()
// .then(
// just(' ').repeated().at_least(1).ignore_then(
// none_of("\"")
// .repeated()
// .collect::<String>()
// .delimited_by(just("\""), just("\""))
// .or_not(),
// ),
// )
.delimited_by(just('('), just(')')),
)
.map(|(text, (url))| InlineParserElement::Link {
url,
title: None,
text,
})
}
fn custom_element_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
choice((
user_mention_parser(),
channel_mention_parser(),
role_mention_parser(),
emoji_mention_parser(),
link_parser(),
))
}
fn element_parser<'a>() -> impl chumsky::Parser<'a, &'a str, InlineParserElement> {
custom_element_parser().or(any::<&str, _>().map(|c| InlineParserElement::Text(c.to_string())))
}
fn inline_parser<'a>() -> impl chumsky::Parser<'a, &'a str, Vec<InlineParserElement>> {
element_parser().repeated().collect()
}
#[test]
fn test() {
println!(
"{:?}",
inline_parser()
.parse("Hello <@01FD58YK5W7QRV5H3D64KTQYX3> :01J7E64FE9R80ZS9RCA5HRAPJ6:. <#01F92C5ZXBQWQ8KY7J8KY917NM> asdasdasd; jklh.\nasdjklhwe sdfjklhqwer fsdljkhsdfa <%01FD6CBW8QSWHSVJAXNM0K2GET> :01GJTC4RD6XAJXRAAM30KW25VD:")
.into_output()
)
}
fn map_element(
element: InlineParserElement,
bold: bool,
italic: bool,
strikethrough: bool,
) -> Inline {
match element {
InlineParserElement::Text(text) => Inline::Span(TextSpan {
text,
bold,
italic,
strikethrough,
code: false,
}),
InlineParserElement::UserMention(id) => Inline::UserMention {
avatar: "DZdeerdwrU6rvfDOOGdNUptzHZT8Ri0cHCo0Z1sz99".to_string(),
name: "Zomatree".to_string(),
fill: None,
},
InlineParserElement::ChannelMention(id) => Inline::ChannelMention {
name: "General".to_string(),
},
InlineParserElement::RoleMention(id) => Inline::RoleMention {
fill: Some(Fill::Color(Color::RED)),
name: "Admin".to_string(),
},
InlineParserElement::Emoji(id) => Inline::Emoji { id },
InlineParserElement::Link { url, title, text } => Inline::Link {
url,
title,
text: parse_elements(&text, bold, italic, strikethrough),
},
}
}
fn parse_elements(content: &str, bold: bool, italic: bool, strikethrough: bool) -> Vec<Inline> {
inline_parser()
.parse(content)
.into_output()
.unwrap_or_default()
.into_iter()
.map(|element| map_element(element, bold, italic, strikethrough))
.collect()
}
/// Build a styled [Span] from a markdown text span.
fn styled_span(span: &TextSpan, text_color: Color, code_color: Color) -> Span<'static> {
let mut styled = Span::new(span.text.clone());
if span.bold {
styled = styled.font_weight(FontWeight::BOLD);
}
if span.italic {
styled = styled.font_slant(FontSlant::Italic);
}
if span.code {
styled.font_family("monospace").color(code_color)
} else {
styled.color(text_color)
}
}
fn render_user_mention(
avatar: &str,
name: &str,
fill: Option<Fill>,
base_font_size: f32,
text_color: Color,
background_color: Color,
) -> impl IntoElement {
let size = base_font_size * (16. / 14.);
let mut label = label();
label.get_text_style_data().color = Some(fill.unwrap_or(Fill::Color(text_color)));
rect()
.padding((0., 6., 0., 2.))
.horizontal()
.cross_align(Alignment::Center)
.spacing(4.)
.corner_radius(16.)
.background(background_color)
.color(text_color)
.font_weight(FontWeight::SEMI_BOLD)
.child(
ImageViewer::new(
format!("https://cdn.stoatusercontent.com/avatars/{avatar}")
.parse::<Uri>()
.unwrap(),
)
.sampling_mode(SamplingMode::Trilinear)
.aspect_ratio(AspectRatio::Max)
.image_cover(ImageCover::Center)
.corner_radius(size / 2.)
.width(Size::px(size))
.height(Size::px(size)),
)
.child(
label
.line_height(1.5)
.max_lines(1)
.font_size(base_font_size)
.text(name.to_string()),
)
}
fn render_channel_mention(
name: &str,
base_font_size: f32,
text_color: Color,
background_color: Color,
) -> impl IntoElement {
let size = base_font_size * (16. / 14.);
rect()
.padding((0., 6., 0., 2.))
.horizontal()
.cross_align(Alignment::Center)
.spacing(4.)
.corner_radius(16.)
.background(background_color)
.color(text_color)
.font_weight(FontWeight::SEMI_BOLD)
.child(svg(hash()).width(Size::px(size)).height(Size::px(size)))
.child(
label()
.line_height(1.5)
.max_lines(1)
.font_size(base_font_size)
.text(name.to_string()),
)
}
fn render_role_mention(
name: &str,
fill: &Option<Fill>,
base_font_size: f32,
text_color: Color,
background_color: Color,
) -> impl IntoElement {
let size = base_font_size * (16. / 14.);
let fill = fill.clone().unwrap_or(Fill::Color(text_color.clone()));
let mut container = rect();
container.get_text_style_data().color = Some(fill.clone());
let mut circle = rect();
circle.get_style().background = fill;
container
.padding((0., 6., 0., 2.))
.horizontal()
.cross_align(Alignment::Center)
.spacing(4.)
.corner_radius(16.)
.background(background_color)
.font_weight(FontWeight::SEMI_BOLD)
.child(
circle
.corner_radius(size / 2.)
.width(Size::px(size))
.height(Size::px(size)),
)
.child(
label()
.line_height(1.5)
.max_lines(1)
.font_size(base_font_size)
.text(name.to_string()),
)
}
fn render_emoji(id: &str, base_font_size: f32) -> impl IntoElement {
let size = base_font_size * 1.4;
rect().padding((0., 0.7, 0., 1.4)).child(
ImageViewer::new(
format!("https://cdn.stoatusercontent.com/emojis/{id}")
.parse::<Uri>()
.unwrap(),
)
.sampling_mode(SamplingMode::Trilinear)
.aspect_ratio(AspectRatio::Max)
.image_cover(ImageCover::Center)
.width(Size::px(size))
.height(Size::px(size)),
)
}
fn render_link(
content: &[Inline],
url: &str,
title: &Option<String>,
base_font_size: f32,
text_color: Color,
) -> impl IntoElement + KeyExt {
rect()
}
fn render_content(
content: &[Inline],
base_font_size: f32,
text_color: Color,
link_color: Color,
code_color: Color,
primary_foreground: Color,
primary_background: Color,
secondary_foreground: Color,
secondary_background: Color,
) -> Paragraph {
let mut result = paragraph().font_size(base_font_size);
for item in content {
result = match item {
Inline::Span(span) => result.span(styled_span(span, text_color, code_color)),
Inline::Link { text, url, title } => {
result.child(render_link(text, url, title, base_font_size, link_color))
}
Inline::UserMention { avatar, name, fill } => result.child(render_user_mention(
avatar,
name,
fill.clone(),
base_font_size,
primary_foreground,
primary_background,
)),
Inline::ChannelMention { name } => result.child(render_channel_mention(
name,
base_font_size,
secondary_foreground,
secondary_background,
)),
Inline::RoleMention { fill, name } => result.child(render_role_mention(
name,
fill,
base_font_size,
primary_foreground,
primary_background,
)),
Inline::Emoji { id } => result.child(render_emoji(id, base_font_size)),
};
}
result
}
impl Component for MarkdownViewer {
fn render(&self) -> impl IntoElement {
let elements = parse_markdown(&self.content);
let MarkdownViewerTheme {
color,
color_link,
background_code,
color_code,
background_blockquote,
border_blockquote,
background_divider,
font_size,
color_primary,
background_primary,
color_secondary,
background_secondary,
} = crate::get_theme!(
&self.theme,
MarkdownViewerThemePreference,
"custom_markdown_viewer"
);
let mut container = rect().vertical().layout(self.layout.clone()).spacing(12.);
for (idx, element) in elements.into_iter().enumerate() {
let child: Element = match element {
MarkdownElement::Heading { level, content } => {
let multiplier = match level {
HeadingLevel::H1 => 2.,
HeadingLevel::H2 => 1.6,
HeadingLevel::H3 => 1.4,
HeadingLevel::H4 => 1.2,
HeadingLevel::H5 => 1.,
HeadingLevel::H6 => 0.8,
};
render_content(
&content,
multiplier * font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
)
.font_weight(FontWeight::BOLD)
.key(idx)
.into()
}
MarkdownElement::Paragraph { content } => render_content(
&content,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
)
.key(idx)
.into(),
MarkdownElement::CodeBlock { code, .. } => rect()
.key(idx)
.width(Size::fill())
.background(background_code)
.corner_radius(6.)
.padding(Gaps::new_all(12.))
.child(
label()
.text(code)
.font_family("monospace")
.font_size(font_size)
.color(color_code),
)
.into(),
MarkdownElement::UnorderedList { items } => {
let mut list = rect()
.key(idx)
.vertical()
.spacing(4.)
.padding(Gaps::new(0., 0., 0., 20.));
for (item_idx, item_spans) in items.into_iter().enumerate() {
let item_content = rect()
.key(item_idx)
.horizontal()
.cross_align(Alignment::Start)
.spacing(8.)
.child(label().text("•").font_size(font_size).color(color))
.child(render_content(
&item_spans,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
));
list = list.child(item_content);
}
list.into()
}
MarkdownElement::OrderedList { start, items } => {
let mut list = rect()
.key(idx)
.vertical()
.spacing(4.)
.padding(Gaps::new(0., 0., 0., 20.));
for (item_idx, item_spans) in items.into_iter().enumerate() {
let number = start + item_idx as u64;
let item_content = rect()
.key(item_idx)
.horizontal()
.cross_align(Alignment::Start)
.spacing(8.)
.child(
label()
.text(format!("{}.", number))
.font_size(font_size)
.color(color),
)
.child(render_content(
&item_spans,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
));
list = list.child(item_content);
}
list.into()
}
MarkdownElement::Image { alt, .. } => label()
.key(idx)
.text(format!("[Image: {}]", alt))
.color(color)
.into(),
MarkdownElement::Link { text, url, title } => {
render_link(&text, &url, &title, font_size, color_link)
.key(idx)
.into_element()
}
MarkdownElement::Blockquote { content: spans } => rect()
.key(idx)
.width(Size::fill())
.padding((4., 12.))
.corner_radius(8.)
.border(
Border::new()
.width(BorderWidth {
top: 0.,
right: 0.,
left: 4.,
bottom: 0.,
})
.fill(border_blockquote)
.alignment(BorderAlignment::Inner),
)
.background(background_blockquote)
.child(
render_content(
&spans,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
)
.font_slant(FontSlant::Italic),
)
.into(),
MarkdownElement::HorizontalRule => rect()
.key(idx)
.width(Size::fill())
.height(Size::px(1.))
.background(background_divider)
.into(),
MarkdownElement::Table { headers, rows } => {
let mut head = TableHead::new();
let mut header_row = TableRow::new();
let column_count = headers.len();
for (col_idx, header_spans) in headers.into_iter().enumerate() {
header_row = header_row.child(
TableCell::new().key(col_idx).child(
render_content(
&header_spans,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
)
.font_weight(FontWeight::BOLD)
.width(Size::Fill)
.text_align(TextAlign::Start),
),
);
}
head = head.child(header_row);
let mut body = TableBody::new();
for (row_idx, row) in rows.into_iter().enumerate() {
let mut table_row = TableRow::new().key(row_idx);
for (col_idx, cell_spans) in row.into_iter().enumerate() {
table_row = table_row.child(
TableCell::new().key(col_idx).child(
render_content(
&cell_spans,
font_size,
color,
color_link,
color_code,
color_primary,
background_primary,
color_secondary,
background_secondary,
)
.width(Size::Fill)
.text_align(TextAlign::Start),
),
);
}
body = body.child(table_row);
}
Table::new()
.column_widths(vec![Size::flex(1.); column_count])
.key(idx)
.child(head)
.child(body)
.into()
}
};
container = container.child(child);
}
container
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment