Demo/markdown (#61)

* demo: init demo-markdown

* demo: add include_md macro

* demo: improve include_md macro

* demo: improve include_md macro

* demo: improve include_md macro

* demo: improve code block

* demo: add syntect css

* demo: improve include_md macro

* demo: include_md handle table
This commit is contained in:
luoxiaozero 2023-12-30 14:45:16 +08:00 committed by GitHub
parent 2cd308fadf
commit 3df65a4e26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 437 additions and 4 deletions

View file

@ -51,4 +51,4 @@ ssr = ["leptos/ssr", "leptos_meta/ssr"]
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
[workspace] [workspace]
members = ["demo", "examples/*"] members = ["demo", "demo_markdown", "examples/*"]

View file

@ -20,6 +20,7 @@ icondata = { version = "0.1.0", features = [
"AiSearchOutlined", "AiSearchOutlined",
] } ] }
prisms = { git = "https://github.com/luoxiaozero/prisms", rev = "16d4d34b93fc20578ebf03137d54ecc7eafa4d4b" } prisms = { git = "https://github.com/luoxiaozero/prisms", rev = "16d4d34b93fc20578ebf03137d54ecc7eafa4d4b" }
demo_markdown = { path = "../demo_markdown" }
[features] [features]
default = ["csr"] default = ["csr"]

View file

@ -5,7 +5,7 @@ public_url = "/thaw/"
filehash = false filehash = false
[watch] [watch]
watch = ["../src", "./src"] watch = ["../src", "./src", "../demo_markdown"]
[serve] [serve]
address = "127.0.0.1" address = "127.0.0.1"

View file

@ -72,6 +72,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/switch" view=SwitchPage/> <Route path="/switch" view=SwitchPage/>
<Route path="/tag" view=TagPage/> <Route path="/tag" view=TagPage/>
<Route path="/upload" view=UploadPage/> <Route path="/upload" view=UploadPage/>
<Route path="/upload-md" view=UploadMdPage/>
<Route path="/loading-bar" view=LoadingBarPage/> <Route path="/loading-bar" view=LoadingBarPage/>
<Route path="/breadcrumb" view=BreadcrumbPage/> <Route path="/breadcrumb" view=BreadcrumbPage/>
<Route path="/layout" view=LayoutPage/> <Route path="/layout" view=LayoutPage/>

View file

@ -4,6 +4,8 @@ use thaw::*;
#[slot] #[slot]
pub struct DemoCode { pub struct DemoCode {
#[prop(default = true)]
is_highlight: bool,
children: Children, children: Children,
} }
@ -37,7 +39,16 @@ pub fn Demo(demo_code: DemoCode, children: Children) -> impl IntoView {
}); });
style style
}); });
let content_class = create_memo(move |_| {
theme.with(|theme| {
format!(
"thaw-demo__content color-scheme--{}",
theme.common.color_scheme
)
})
});
let is_highlight = demo_code.is_highlight;
let frag = (demo_code.children)(); let frag = (demo_code.children)();
let mut html = String::new(); let mut html = String::new();
for node in frag.nodes { for node in frag.nodes {
@ -51,15 +62,30 @@ pub fn Demo(demo_code: DemoCode, children: Children) -> impl IntoView {
view! { view! {
<Style id="leptos-thaw-prism-css">{prisms::prism_css!()}</Style> <Style id="leptos-thaw-prism-css">{prisms::prism_css!()}</Style>
<Style id="leptos-thaw-syntect-css">
{include_str!("./syntect-css.css")}
</Style>
<Style id="leptos-thaw-prism-css-fix"> <Style id="leptos-thaw-prism-css-fix">
".token.operator { ".token.operator {
background: hsla(0, 0%, 100%, 0) !important; background: hsla(0, 0%, 100%, 0) !important;
}" }"
</Style> </Style>
<div style=move || style.get()>{children()}</div> <div style=move || style.get()>{children()}</div>
<div style=move || code_style.get()> <div style=move || code_style.get() class=move || content_class.get()>
<Code> <Code>
{
if is_highlight {
view! {
<pre style="margin: 0" inner_html=html></pre> <pre style="margin: 0" inner_html=html></pre>
}
} else {
view! {
<pre style="margin: 0">
{html}
</pre>
}
}
}
</Code> </Code>
</div> </div>
} }

View file

@ -0,0 +1,60 @@
.thaw-demo__content pre {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
}
/** https://github.com/AmjadHD/sublime_one_theme */
/** light */
.color-scheme--light .syntect-storage {
color: hsl(301, 63%, 40%);
}
.color-scheme--light .syntect-keyword.syntect-operator {
color: hsl(335, 95%, 62%);
}
.color-scheme--light .syntect-function,
.color-scheme--light .syntect-macro {
color: hsl(221, 87%, 60%);
}
.color-scheme--light .syntect-support.syntect-type {
color: hsl(198, 99%, 37%);
}
.color-scheme--light .syntect-string {
color: hsl(119, 34%, 47%);
}
.color-scheme--light .syntect-placeholder {
color: hsl(41, 99%, 30%);
}
/** dark */
.color-scheme--dark .syntect-storage {
color: hsl(286, 60%, 67%);
}
.color-scheme--dark .syntect-keyword.syntect-operator {
color: hsl(335, 95%, 62%);
}
.color-scheme--dark .syntect-function,
.color-scheme--dark .syntect-macro {
color: hsl(207, 82%, 66%);
}
.color-scheme--dark .syntect-support.syntect-type {
color: hsl(187, 47%, 55%);
}
.color-scheme--dark .syntect-string {
color: hsl(95, 38%, 62%);
}
.color-scheme--dark .syntect-placeholder {
color: hsl(29, 54%, 61%);
}

View file

@ -0,0 +1,5 @@
use crate::components::{Demo, DemoCode};
use leptos::*;
use thaw::*;
demo_markdown::include_md! {}

View file

@ -20,6 +20,7 @@ mod input;
mod input_number; mod input_number;
mod layout; mod layout;
mod loading_bar; mod loading_bar;
mod markdown;
mod menu; mod menu;
mod message; mod message;
mod mobile; mod mobile;
@ -66,6 +67,7 @@ pub use input::*;
pub use input_number::*; pub use input_number::*;
pub use layout::*; pub use layout::*;
pub use loading_bar::*; pub use loading_bar::*;
pub use markdown::*;
pub use menu::*; pub use menu::*;
pub use message::*; pub use message::*;
pub use mobile::*; pub use mobile::*;

17
demo_markdown/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
publish = false
name = "demo_markdown"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
quote = "1.0.33"
comrak = "0.20.0"
proc-macro2 = "1.0.71"
syn = "2.0.43"
syntect = "5.1.0"

View file

@ -0,0 +1,54 @@
# Upload
```rust demo
let message = use_message();
let custom_request = move |file_list: FileList| {
message.create(
format!("Number of uploaded files: {}", file_list.length()),
MessageVariant::Success,
Default::default(),
);
};
view!{
<Upload>
<Button>
"upload"
</Button>
</Upload>
}
```
### Drag to upload
```rust demo
let message = use_message();
let custom_request = move |file_list: FileList| {
message.create(
format!("Number of uploaded files: {}", file_list.length()),
MessageVariant::Success,
Default::default(),
);
};
view! {
<Upload custom_request>
<UploadDragger>"Click or drag a file to this area to upload"</UploadDragger>
</Upload>
}
```
### Upload Props
| Name | Type | Default | Description |
| -------------- | -------------------------------- | -------------------- | ------------------------------------ |
| accept | `MaybeSignal<String>` | `Default::default()` | The accept type of upload. |
| multiple | `MaybeSignal<bool>` | `false` | Allow multiple files to be selected. |
| custom_request | `Option<Callback<FileList, ()>>` | `Default::default()` | Customize upload request. |
| children | `Children` | | Upload's content. |
### UploadDragger Props
| Name | Type | Default | Description |
| -------- | ---------- | ------- | ------------------------ |
| children | `Children` | | UploadDragger's content. |

55
demo_markdown/src/lib.rs Normal file
View file

@ -0,0 +1,55 @@
mod markdown;
use crate::markdown::parse_markdown;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::ItemFn;
#[proc_macro]
pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let file_list = vec![("UploadMdPage", include_str!("../docs/upload/mod.md"))];
let mut fn_list = vec![];
for (fn_name, file_str) in file_list {
let fn_name = Ident::new(fn_name, Span::call_site());
let (body, demos) = match parse_markdown(file_str) {
Ok(body) => body,
Err(err) => {
return quote!(compile_error!(#err)).into();
}
};
let demos: Vec<ItemFn> = demos
.into_iter()
.enumerate()
.map(|(index, demo)| {
format!(
"#[component] fn Demo{}() -> impl IntoView {{ {} }}",
index + 1,
demo
)
})
.map(|demo| syn::parse_str::<ItemFn>(&demo).unwrap())
.collect();
fn_list.push(quote! {
#[component]
pub fn #fn_name() -> impl IntoView {
#(#demos)*
view! {
<div style="width: 896px; margin: 0 auto;">
#body
</div>
}
}
});
}
quote! {
#(#fn_list)*
}
.into()
}

View file

@ -0,0 +1,78 @@
use comrak::nodes::NodeCodeBlock;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use std::sync::OnceLock;
use syntect::{
html::{ClassStyle, ClassedHTMLGenerator},
parsing::SyntaxSet,
util::LinesWithEndings,
};
pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenStream {
let langs: Vec<&str> = code_block.info.split_ascii_whitespace().collect();
if langs.iter().any(|lang| lang == &"demo") {
demos.push(code_block.literal.clone());
let demo = Ident::new(&format!("Demo{}", demos.len()), Span::call_site());
let mut is_highlight = true;
let literal = langs
.iter()
.find(|lang| lang != &&"demo")
.map(|lang| highlight_to_html(&code_block.literal, lang))
.flatten()
.unwrap_or_else(|| {
is_highlight = false;
code_block.literal.clone()
});
quote! {
<Demo>
<#demo />
<DemoCode slot is_highlight=#is_highlight>
#literal
</DemoCode>
</Demo>
}
} else {
let mut is_highlight = true;
let literal = langs
.first()
.map(|lang| highlight_to_html(&code_block.literal, lang))
.flatten()
.unwrap_or_else(|| {
is_highlight = false;
code_block.literal.clone()
});
quote! {
<Demo>
""
<DemoCode slot is_highlight=#is_highlight>
#literal
</DemoCode>
</Demo>
}
}
}
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
fn highlight_to_html(text: &str, syntax: &str) -> Option<String> {
let syntax_set = SYNTAX_SET.get_or_init(|| SyntaxSet::load_defaults_newlines());
let Some(syntax) = syntax_set.find_syntax_by_token(syntax) else {
return None;
};
let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
syntax,
syntax_set,
ClassStyle::SpacedPrefixed { prefix: "syntect-" },
);
for line in LinesWithEndings::from(text) {
html_generator
.parse_html_for_line_which_includes_newline(line)
.expect(line);
}
Some(html_generator.finalize())
}

View file

@ -0,0 +1,134 @@
mod code_block;
use comrak::{
nodes::{AstNode, NodeValue},
parse_document, Arena,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
pub fn parse_markdown(md_text: &str) -> Result<(TokenStream, Vec<String>), String> {
let mut demos: Vec<String> = vec![];
let arena = Arena::new();
let mut options = comrak::Options::default();
options.extension.table = true;
let root = parse_document(&arena, &md_text, &options);
let body = iter_nodes(root, &mut demos);
Ok((body, demos))
}
fn iter_nodes<'a>(node: &'a AstNode<'a>, demos: &mut Vec<String>) -> TokenStream {
let mut children = vec![];
for c in node.children() {
children.push(iter_nodes(c, demos));
}
match &node.data.borrow().value {
NodeValue::Document => quote!(#(#children)*),
NodeValue::FrontMatter(_) => quote!("FrontMatter todo!!!"),
NodeValue::BlockQuote => quote!("BlockQuote todo!!!"),
NodeValue::List(_) => quote!("List todo!!!"),
NodeValue::Item(_) => quote!("Item todo!!!"),
NodeValue::DescriptionList => quote!("DescriptionList todo!!!"),
NodeValue::DescriptionItem(_) => quote!("DescriptionItem todo!!!"),
NodeValue::DescriptionTerm => quote!("DescriptionTerm todo!!!"),
NodeValue::DescriptionDetails => quote!("DescriptionDetails todo!!!"),
NodeValue::CodeBlock(node_code_block) => code_block::to_tokens(node_code_block, demos),
NodeValue::HtmlBlock(_) => quote!("HtmlBlock todo!!!"),
NodeValue::Paragraph => quote!(
<p>
#(#children)*
</p >
),
NodeValue::Heading(node_h) => {
let h = Ident::new(&format!("h{}", node_h.level), Span::call_site());
quote!(
<#h>
#(#children)*
</#h>
)
}
NodeValue::ThematicBreak => quote!("ThematicBreak todo!!!"),
NodeValue::FootnoteDefinition(_) => quote!("FootnoteDefinition todo!!!"),
NodeValue::Table(_) => {
let header_index = {
let mut header_index = 0;
for (index, c) in node.children().enumerate() {
let row = &c.data.borrow().value;
let is_header = match *row {
NodeValue::TableRow(header) => header,
_ => panic!(),
};
if !is_header {
header_index = index;
break;
}
}
header_index
};
let header_children: Vec<TokenStream> = children.drain(0..header_index).collect();
quote!(
<Table single_column=true>
<thead>
#(#header_children)*
</thead>
<tbody>
#(#children)*
</tbody>
</Table>
)
}
NodeValue::TableRow(_) => {
quote!(
<tr>
#(#children)*
</tr>
)
}
NodeValue::TableCell => {
let row = &node.parent().unwrap().data.borrow().value;
let is_header = match *row {
NodeValue::TableRow(header) => header,
_ => panic!(),
};
if is_header {
quote!(
<th>
#(#children)*
</th>
)
} else {
quote!(
<td>
#(#children)*
</td>
)
}
}
NodeValue::Text(text) => {
let text = text.clone();
quote!(#text)
}
NodeValue::TaskItem(_) => quote!("TaskItem todo!!!"),
NodeValue::SoftBreak => quote!("\n"),
NodeValue::LineBreak => quote!(<br />),
NodeValue::Code(node_code) => {
let code = node_code.literal.clone();
quote!(
<Text code=true>
#code
</Text>
)
}
NodeValue::HtmlInline(_) => quote!("HtmlInline todo!!!"),
NodeValue::Emph => quote!("Emph todo!!!"),
NodeValue::Strong => quote!("Strong todo!!!"),
NodeValue::Strikethrough => quote!("Strikethrough todo!!!"),
NodeValue::Superscript => quote!("Superscript todo!!!"),
NodeValue::Link(_) => quote!("Link todo!!!"),
NodeValue::Image(_) => quote!("Image todo!!!"),
NodeValue::FootnoteReference(_) => quote!("FootnoteReference todo!!!"),
}
}