use hikari_components::basic::{
Arrow, ArrowProps, Avatar, AvatarProps, Badge, BadgeProps, Button, ButtonProps, Card,
CardContent, CardContentProps, CardHeader, CardHeaderProps, CardProps, Checkbox, CheckboxProps,
IconButton, IconButtonProps, Image, ImageProps, Input, InputProps, Link, LinkProps, Switch,
SwitchProps,
};
use hikari_components::data::{
Collapse, CollapseProps, Filter, FilterProps, Pagination, PaginationProps, Sort, SortProps,
Table, TableProps, Tree, TreeProps,
};
use hikari_components::display::{
Calendar, CalendarProps, Carousel, CarouselProps, Comment, CommentProps, DragLayer,
DragLayerProps, Empty, EmptyProps, QRCode, QRCodeProps, Skeleton, SkeletonCard,
SkeletonCardProps, SkeletonProps, Tag, TagProps, TagVariant, Timeline, TimelineItem,
TimelineItemProps, TimelineProps, UserGuide, UserGuideProps, ZoomControls, ZoomControlsProps,
};
use hikari_components::feedback::{
Drawer, DrawerProps, Glow, GlowProps, Popover, PopoverProps, Progress, ProgressProps, Spin,
SpinProps, Toast, ToastProps,
};
use hikari_components::layout::divider::{DividerOrientation, DividerType};
use hikari_components::layout::{
Aside, AsideProps, Col, ColProps, Content, ContentProps, Divider, DividerProps, Footer,
FooterProps, Grid, GridProps, Header, HeaderProps, Row, RowProps, Section, SectionProps,
};
use hikari_components::navigation::{
Anchor, AnchorProps, Breadcrumb, BreadcrumbProps, Menu, MenuItem, MenuItemProps, MenuProps,
Sidebar, SidebarProps, Stepper, StepperProps, TabPane, TabPaneProps, Tabs, TabsProps,
};
use tairitsu_vdom::{el, txt, VNode};
use crate::markdown::{Block, Inline};
pub fn render_to_html(blocks: &[Block]) -> String {
render_blocks(blocks).render_to_html()
}
pub fn render_to_html_with_live(
blocks: &[Block],
live_html: &std::collections::HashMap<String, String>,
) -> String {
let inner = render_blocks_with_live(blocks, live_html);
inner.render_to_html()
}
pub fn render_blocks(blocks: &[Block]) -> VNode {
VNode::Fragment(blocks.iter().map(render_block).collect())
}
fn render_blocks_with_live(
blocks: &[Block],
live_html: &std::collections::HashMap<String, String>,
) -> VNode {
VNode::Fragment(
blocks
.iter()
.map(|b| render_block_with_live(b, live_html))
.collect(),
)
}
fn render_block(b: &Block) -> VNode {
render_block_with_live(b, &std::collections::HashMap::new())
}
fn render_block_with_live(
b: &Block,
live_html: &std::collections::HashMap<String, String>,
) -> VNode {
match b {
Block::Heading { level, text } => {
el_node(&format!("h{level}"), render_inlines(text))
}
Block::Paragraph(inlines) => el_node("p", render_inlines(inlines)),
Block::CodeBlock { lang, code } => {
let lang_class = format!("language-{}", lang.as_deref().unwrap_or(""));
el_pre_code(&lang_class, code)
}
Block::LiveComponent { source } => render_live_block(source, live_html.get(source)),
Block::Diagram { kind, source } => render_diagram_block(*kind, source),
Block::List { ordered, items } => {
let tag = if *ordered { "ol" } else { "ul" };
let lis: Vec<VNode> = items
.iter()
.enumerate()
.map(|(i, it)| {
let content = render_inlines(it);
if *ordered {
let badge = Badge(BadgeProps {
count: Some((i + 1) as i32),
..Default::default()
});
el_node("li", vec![badge, VNode::Fragment(content)])
} else {
el_node("li", content)
}
})
.collect();
el_node(tag, lis)
}
Block::Blockquote(inner) => {
el_node("blockquote", vec![render_blocks(inner)])
}
Block::Table { headers, rows } => {
let ths: Vec<VNode> = headers
.iter()
.map(|h| el_node("th", render_inlines(h)))
.collect();
let thead = VNode::Element(Box::new(
el("thead").child(VNode::Element(Box::new(el("tr").children(ths)))),
));
let mut trs = Vec::new();
for row in rows {
let tds: Vec<VNode> = row
.iter()
.map(|c| el_node("td", render_inlines(c)))
.collect();
trs.push(VNode::Element(Box::new(el("tr").children(tds))));
}
let tbody = VNode::Element(Box::new(el("tbody").children(trs)));
el_node("table", vec![thead, tbody])
}
Block::ThematicBreak => Divider(DividerProps {
text: None,
orientation: DividerOrientation::Horizontal,
divider_type: DividerType::Solid,
text_align: "center".to_string(),
rtl: None,
..Default::default()
}),
Block::Center(inner) => VNode::Element(Box::new(
el("div")
.attr("style", "text-align:center")
.children(vec![render_blocks(inner)]),
)),
Block::Div { attrs, children } => {
let style = extract_style_attr(&attrs);
let mut d = el("div");
if let Some(s) = style {
d = d.attr("style", &s);
}
VNode::Element(Box::new(d.children(vec![render_blocks(children)])))
}
Block::Html(raw) => VNode::Element(Box::new(el("div").dangerous_inner_html(raw))),
}
}
fn extract_style_attr(attrs: &str) -> Option<String> {
let needle = "style=\"";
let start = attrs.find(needle)?;
let rest = &attrs[start + needle.len()..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn render_inlines(inlines: &[Inline]) -> Vec<VNode> {
inlines.iter().map(render_inline).collect()
}
fn render_inline(i: &Inline) -> VNode {
match i {
Inline::Text(s) => txt(s),
Inline::Strong(inner) => el_node("strong", render_inlines(inner)),
Inline::Emphasis(inner) => el_node("em", render_inlines(inner)),
Inline::Code(s) => Tag(TagProps {
variant: TagVariant::Default,
closable: false,
on_close: None,
class: "hi-tag-code".to_string(),
style: String::new(),
children: txt(s),
}),
Inline::Link { text, url } => Link(LinkProps {
href: rewrite_link(url),
children: VNode::Fragment(render_inlines(text)),
..Default::default()
}),
Inline::Image { alt, url } => Image(ImageProps {
src: Some(url.clone()),
alt: alt.clone(),
..Default::default()
}),
Inline::InlineHtml(raw) => VNode::Element(Box::new(el("span").dangerous_inner_html(raw))),
}
}
fn render_live_block(source: &str, rendered_html: Option<&String>) -> VNode {
let mut children = Vec::new();
children.push(VNode::Element(Box::new(
el("div").attr("class", "lg-live-tabs").children(vec![
VNode::Element(Box::new(
el("button")
.attr("class", "lg-live-tab active")
.attr("data-tab", "preview")
.child(txt("Preview")),
)),
VNode::Element(Box::new(
el("button")
.attr("class", "lg-live-tab")
.attr("data-tab", "source")
.child(txt("Source")),
)),
]),
)));
let preview_inner = if let Some(html) = rendered_html {
Card(CardProps {
children: VNode::Element(Box::new(
el("div")
.attr("class", "lg-live-preview-inner")
.dangerous_inner_html(html),
)),
..Default::default()
})
} else {
Empty(EmptyProps {
description: "(live preview unavailable)".to_string(),
..Default::default()
})
};
children.push(VNode::Element(Box::new(
el("div")
.attr("class", "lg-live-preview")
.child(preview_inner),
)));
let highlighted_src = syntax_highlight(source, "rust");
let line_count = source.lines().count().max(1);
let line_num_nodes: Vec<VNode> = (1..=line_count)
.map(|i| {
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-number")
.child(txt(&i.to_string())),
))
})
.collect();
children.push(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight")
.attr("style", "display:none")
.attr("data-lg-source", "")
.child(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-content")
.child(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-numbers")
.children(line_num_nodes),
)))
.child(VNode::Element(Box::new(
el("pre")
.attr("class", "hi-code-highlight-code hi-scroll-container")
.child(VNode::Element(Box::new(
el("code")
.attr("class", "language-rust")
.dangerous_inner_html(&highlighted_src),
))),
))),
))),
)));
VNode::Element(Box::new(
el("div").attr("class", "lg-live-block").children(children),
))
}
fn render_diagram_block(kind: crate::markdown::DiagramKind, source: &str) -> VNode {
let lang = kind.source_lang();
let highlighted = syntax_highlight(source, lang);
let line_count = source.lines().count().max(1);
let line_num_nodes: Vec<VNode> = (1..=line_count)
.map(|i| {
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-number")
.child(txt(&i.to_string())),
))
})
.collect();
let copy_btn = VNode::Element(Box::new(
el("button")
.attr("class", "hi-code-highlight-copy")
.attr("type", "button")
.attr("data-copy", source)
.attr("aria-label", "Copy code")
.attr("title", "Copy code")
.child(VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-copy-icon")
.attr("aria-hidden", "true")
.dangerous_inner_html(crate::icons::icon_svg("content-copy", 14)),
)))
.child(VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-check")
.attr("aria-hidden", "true")
.dangerous_inner_html(crate::icons::icon_svg("check", 14)),
))),
));
let toggle = VNode::Element(Box::new(
el("div")
.attr("class", "lg-diagram-toggle")
.attr("role", "tablist")
.children(vec![
VNode::Element(Box::new(
el("button")
.attr("type", "button")
.attr("class", "lg-diagram-toggle-btn active")
.attr("data-pane", "preview")
.child(txt("Preview")),
)),
VNode::Element(Box::new(
el("button")
.attr("type", "button")
.attr("class", "lg-diagram-toggle-btn")
.attr("data-pane", "source")
.child(txt("Source")),
)),
]),
));
VNode::Element(Box::new(
el("div")
.attr("class", "lg-diagram")
.attr("data-diagram-kind", kind.attr())
.children(vec![
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-header lg-diagram-header")
.children(vec![
VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-language")
.child(txt(lang)),
)),
VNode::Element(Box::new(
el("div")
.attr("class", "lg-diagram-actions")
.children(vec![toggle, copy_btn]),
)),
]),
)),
VNode::Element(Box::new(
el("div")
.attr("class", "lg-diagram-preview")
.attr("data-pane", "preview")
.children(vec![
VNode::Element(Box::new(el("div").attr("class", "lg-diagram-canvas"))),
VNode::Element(Box::new(
el("pre")
.attr("class", "lg-diagram-raw")
.attr("style", "display:none")
.child(txt(source)),
)),
]),
)),
VNode::Element(Box::new(
el("div")
.attr("class", "lg-diagram-source")
.attr("data-pane", "source")
.attr("style", "display:none")
.child(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-content")
.children(vec![
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-numbers")
.children(line_num_nodes),
)),
VNode::Element(Box::new(
el("pre")
.attr(
"class",
"hi-code-highlight-code hi-scroll-container",
)
.child(VNode::Element(Box::new(
el("code")
.attr("class", format!("language-{lang}"))
.dangerous_inner_html(&highlighted),
))),
)),
]),
))),
)),
]),
))
}
fn el_node(tag: &str, children: Vec<VNode>) -> VNode {
VNode::Element(Box::new(el(tag).children(children)))
}
fn el_pre_code(lang_class: &str, code: &str) -> VNode {
let lang = lang_class.trim_start_matches("language-");
let highlighted = syntax_highlight(code, lang);
let line_count = code.lines().count().max(1);
let line_num_nodes: Vec<VNode> = (1..=line_count)
.map(|i| {
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-number")
.child(txt(&i.to_string())),
))
})
.collect();
let mut code_el = el("code");
if !lang_class.is_empty() {
code_el = code_el.attr("class", lang_class);
}
code_el = code_el.dangerous_inner_html(&highlighted);
let display_lang = if lang.is_empty() { "text" } else { lang };
let header = el("div")
.attr("class", "hi-code-highlight-header")
.children(vec![
VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-language")
.child(txt(display_lang)),
)),
VNode::Element(Box::new(
el("button")
.attr("class", "hi-code-highlight-copy")
.attr("type", "button")
.attr("data-copy", code)
.attr("aria-label", "Copy code")
.attr("title", "Copy code")
.child(VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-copy-icon")
.attr("aria-hidden", "true")
.dangerous_inner_html(crate::icons::icon_svg("content-copy", 14)),
)))
.child(VNode::Element(Box::new(
el("span")
.attr("class", "hi-code-highlight-check")
.attr("aria-hidden", "true")
.dangerous_inner_html(crate::icons::icon_svg("check", 14)),
))),
)),
]);
VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight")
.child(VNode::Element(Box::new(header)))
.child(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-content")
.child(VNode::Element(Box::new(
el("div")
.attr("class", "hi-code-highlight-line-numbers")
.children(line_num_nodes),
)))
.child(VNode::Element(Box::new(
el("pre")
.attr("class", "hi-code-highlight-code hi-scroll-container")
.child(VNode::Element(Box::new(code_el))),
))),
))),
))
}
fn syntax_highlight(code: &str, lang: &str) -> String {
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
let ps = SyntaxSet::load_defaults_newlines();
let syntax = ps
.find_syntax_by_token(lang)
.or_else(|| ps.find_syntax_by_extension(lang));
match syntax {
Some(syntax) => {
let mut generator =
ClassedHTMLGenerator::new_with_class_style(syntax, &ps, ClassStyle::Spaced);
for line in LinesWithEndings::from(code) {
let _ = generator.parse_html_for_line_which_includes_newline(line);
}
generator.finalize()
}
None => html_escape(code),
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn rewrite_link(url: &str) -> String {
if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("mailto:")
|| url.starts_with('#')
{
return url.to_string();
}
let (path, fragment) = match url.split_once('#') {
Some((p, f)) => (p, Some(f)),
None => (url, None),
};
if path.is_empty() {
return url.to_string();
}
let stripped = path.strip_prefix("./").unwrap_or(path);
let rewritten = if std::path::Path::new(stripped)
.file_name()
.map(|f| f == "README.md" || f == "readme.md")
.unwrap_or(false)
{
let dir = std::path::Path::new(stripped)
.parent()
.map(|p| p.to_path_buf());
match dir {
Some(d) if !d.as_os_str().is_empty() => format!("{}/index.html", d.display()),
_ => "index.html".to_string(),
}
} else {
stripped
.strip_suffix(".md")
.map(|p| format!("{p}.html"))
.unwrap_or_else(|| stripped.to_string())
};
match fragment {
Some(f) => format!("{rewritten}#{f}"),
None => rewritten,
}
}
pub fn render_comment_display(_author: &str, _body: &str) -> VNode {
Comment(CommentProps {
..Default::default()
})
}
pub fn render_skeleton() -> VNode {
Skeleton(SkeletonProps {
..Default::default()
})
}
pub fn render_progress(_value: f64) -> VNode {
Progress(ProgressProps {
..Default::default()
})
}
pub fn render_spin() -> VNode {
Spin(SpinProps {
..Default::default()
})
}
pub fn render_avatar(_name: &str) -> VNode {
Avatar(AvatarProps {
..Default::default()
})
}
pub fn render_switch(_checked: bool) -> VNode {
Switch(SwitchProps {
..Default::default()
})
}
pub fn render_button(label: &str) -> VNode {
Button(ButtonProps {
children: txt(label),
..Default::default()
})
}
pub fn render_icon_button() -> VNode {
IconButton(IconButtonProps {
..Default::default()
})
}
pub fn render_checkbox(_checked: bool) -> VNode {
Checkbox(CheckboxProps {
..Default::default()
})
}
pub fn render_breadcrumb(_items: &[(&str, &str)]) -> VNode {
Breadcrumb(BreadcrumbProps {
..Default::default()
})
}
pub fn render_timeline_item(_title: &str, _time: &str) -> VNode {
TimelineItem(TimelineItemProps {
..Default::default()
})
}
pub fn render_qrcode(_data: &str) -> VNode {
QRCode(QRCodeProps {
..Default::default()
})
}
pub fn render_glow(children: VNode) -> VNode {
Glow(GlowProps {
children,
..Default::default()
})
}
pub fn render_zoom_controls() -> VNode {
ZoomControls(ZoomControlsProps {
..Default::default()
})
}
pub fn render_collapse(_title: &str, children: VNode) -> VNode {
Collapse(CollapseProps {
children,
..Default::default()
})
}
pub fn render_popover(_title: &str, children: VNode) -> VNode {
Popover(PopoverProps {
children,
..Default::default()
})
}
pub fn render_drawer(_title: &str) -> VNode {
Drawer(DrawerProps {
..Default::default()
})
}
pub fn render_toast(_message: &str) -> VNode {
Toast(ToastProps {
..Default::default()
})
}
pub fn render_calendar() -> VNode {
Calendar(CalendarProps {
..Default::default()
})
}
pub fn render_arrow() -> VNode {
Arrow(ArrowProps {
..Default::default()
})
}
pub fn render_pagination(_current: i32, _total: i32) -> VNode {
Pagination(PaginationProps {
..Default::default()
})
}
pub fn render_input(_placeholder: &str) -> VNode {
Input(InputProps {
..Default::default()
})
}
pub fn render_stepper(_current: i32) -> VNode {
Stepper(StepperProps {
..Default::default()
})
}
pub fn render_tabs() -> VNode {
Tabs(TabsProps {
..Default::default()
})
}
pub fn render_tab_pane(_label: &str) -> VNode {
TabPane(TabPaneProps {
..Default::default()
})
}
pub fn render_anchor(_href: &str, _title: &str) -> VNode {
Anchor(AnchorProps {
..Default::default()
})
}
pub fn render_menu_item(_label: &str, _href: &str) -> VNode {
MenuItem(MenuItemProps {
..Default::default()
})
}
pub fn render_sidebar() -> VNode {
Sidebar(SidebarProps {
..Default::default()
})
}
pub fn render_menu() -> VNode {
Menu(MenuProps {
..Default::default()
})
}
pub fn render_aside(children: VNode) -> VNode {
Aside(AsideProps {
children: Some(children),
..Default::default()
})
}
pub fn render_header(children: VNode) -> VNode {
Header(HeaderProps {
children: Some(children),
..Default::default()
})
}
pub fn render_content(children: VNode) -> VNode {
Content(ContentProps {
children: Some(children),
..Default::default()
})
}
pub fn render_footer(children: VNode) -> VNode {
Footer(FooterProps {
children,
..Default::default()
})
}
pub fn render_section(children: VNode) -> VNode {
Section(SectionProps {
children: Some(children),
..Default::default()
})
}
pub fn render_card_header(_title: &str) -> VNode {
CardHeader(CardHeaderProps {
..Default::default()
})
}
pub fn render_card_content(children: VNode) -> VNode {
CardContent(CardContentProps {
children,
..Default::default()
})
}
pub fn render_row(children: VNode) -> VNode {
Row(RowProps {
children: Some(children),
..Default::default()
})
}
pub fn render_col(children: VNode) -> VNode {
Col(ColProps {
children: Some(children),
..Default::default()
})
}
pub fn render_grid(children: VNode) -> VNode {
Grid(GridProps {
children: Some(children),
..Default::default()
})
}
pub fn render_skeleton_card() -> VNode {
SkeletonCard(SkeletonCardProps {
..Default::default()
})
}
pub fn render_carousel() -> VNode {
Carousel(CarouselProps {
..Default::default()
})
}
pub fn render_drag_layer() -> VNode {
DragLayer(DragLayerProps {
..Default::default()
})
}
pub fn render_user_guide() -> VNode {
UserGuide(UserGuideProps {
..Default::default()
})
}
pub fn render_filter() -> VNode {
Filter(FilterProps {
..Default::default()
})
}
pub fn render_sort() -> VNode {
Sort(SortProps {
..Default::default()
})
}
pub fn render_table() -> VNode {
Table(TableProps {
..Default::default()
})
}
pub fn render_tree() -> VNode {
Tree(TreeProps {
..Default::default()
})
}
pub fn render_timeline() -> VNode {
Timeline(TimelineProps {
..Default::default()
})
}