Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 146 additions & 161 deletions stdlib/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,152 +92,161 @@ use techscript_runtime::{
static SERVER_RUNNING: AtomicBool = AtomicBool::new(false);
static PAGE_CONTENT: Mutex<String> = Mutex::new(String::new());

fn render_children(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
for child in &dsl.children {
html.push_str(&dsl_to_html(
&techscript_runtime::value::RuntimeValue::DslBlock(std::rc::Rc::new(child.clone())),
));
}
}

fn render_website(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
html.push_str("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<title>{}</title>", t));
}
}
_ => {}
}
}
html.push_str("<style>\
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
header { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 2rem; background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 100; }\
header h1 { font-size: 1.5rem; margin: 0; font-weight: 800; background: linear-gradient(135deg, #a855f7 0%, #3b82f6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
nav { display: flex; gap: 1.5rem; }\
nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
nav a:hover { color: #f8fafc; }\
.page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
.hero { text-align: center; padding: 5rem 1rem; position: relative; }\
.hero h1 { font-size: 4rem; font-weight: 800; margin: 0 0 1.5rem 0; background: linear-gradient(135deg, #c084fc 0%, #60a5fa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
.hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
.hero a.start-button { background: #3b82f6; color: white; padding: 0.8rem 2rem; border-radius: 9999px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.4); margin-right: 1rem; display: inline-block; }\
.hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
.hero button { background: rgba(255,255,255,0.05); color: #f8fafc; border: 1px solid rgba(255,255,255,0.1); padding: 0.8rem 2rem; border-radius: 9999px; font-weight: 600; cursor: pointer; transition: all 0.2s; }\
.hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
.content-section { margin-top: 5rem; }\
.content-section h2 { font-size: 2.5rem; text-align: center; margin-bottom: 3rem; background: linear-gradient(135deg, #f8fafc 0%, #94a3b8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
.grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
.card { background: rgba(30, 41, 59, 0.4); border: 1px solid rgba(255,255,255,0.05); padding: 2rem; border-radius: 16px; transition: all 0.3s; backdrop-filter: blur(8px); }\
.card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
.card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
.card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
footer { text-align: center; padding: 3rem 2rem; background: #0b0f19; border-top: 1px solid rgba(255,255,255,0.05); color: #64748b; font-size: 0.9rem; margin-top: auto; }\
</style>");
html.push_str("</head><body>");
render_children(html, dsl);
html.push_str("</body></html>");
}

fn render_page(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
html.push_str("<div class=\"page\">");
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = dsl.args.first() {
html.push_str(&format!("<h1>{}</h1>", t));
}
for prop in &dsl.properties {
if prop.name == "title" {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h2>{}</h2>", t));
}
}
}
render_children(html, dsl);
html.push_str("</div>");
}

fn render_hero(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
html.push_str("<section class=\"hero\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h1>{}</h1>", t));
}
}
"subtitle" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<p class=\"subtitle\">{}</p>", t));
}
}
_ => {}
}
}
render_children(html, dsl);
html.push_str("</section>");
}

fn render_section(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
html.push_str("<section class=\"content-section\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h2>{}</h2>", t));
}
}
"id" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<!-- id: {} -->", t));
}
}
_ => {}
}
}
html.push_str("<div class=\"grid-layout\">");
render_children(html, dsl);
html.push_str("</div>");
html.push_str("</section>");
}

fn render_card(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
html.push_str("<div class=\"card\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h3>{}</h3>", t));
}
}
"text" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<p>{}</p>", t));
}
}
"image" => {
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<img src=\"{}\" alt=\"card image\">", t));
}
}
_ => {}
}
}
html.push_str("</div>");
}

/// Convert a DslBlockValue tree to HTML string.
fn dsl_to_html(val: &RuntimeValue) -> String {
match val {
RuntimeValue::DslBlock(dsl) => {
let mut html = String::new();
match dsl.kind.as_str() {
"website" => {
html.push_str("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<title>{}</title>", t));
}
}
_ => {}
}
}
html.push_str("<style>\
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
header { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 2rem; background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 100; }\
header h1 { font-size: 1.5rem; margin: 0; font-weight: 800; background: linear-gradient(135deg, #a855f7 0%, #3b82f6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
nav { display: flex; gap: 1.5rem; }\
nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
nav a:hover { color: #f8fafc; }\
.page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
.hero { text-align: center; padding: 5rem 1rem; position: relative; }\
.hero h1 { font-size: 4rem; font-weight: 800; margin: 0 0 1.5rem 0; background: linear-gradient(135deg, #c084fc 0%, #60a5fa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
.hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
.hero a.start-button { background: #3b82f6; color: white; padding: 0.8rem 2rem; border-radius: 9999px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 14px rgba(59, 130, 246, 0.4); margin-right: 1rem; display: inline-block; }\
.hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
.hero button { background: rgba(255,255,255,0.05); color: #f8fafc; border: 1px solid rgba(255,255,255,0.1); padding: 0.8rem 2rem; border-radius: 9999px; font-weight: 600; cursor: pointer; transition: all 0.2s; }\
.hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
.content-section { margin-top: 5rem; }\
.content-section h2 { font-size: 2.5rem; text-align: center; margin-bottom: 3rem; background: linear-gradient(135deg, #f8fafc 0%, #94a3b8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
.grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
.card { background: rgba(30, 41, 59, 0.4); border: 1px solid rgba(255,255,255,0.05); padding: 2rem; border-radius: 16px; transition: all 0.3s; backdrop-filter: blur(8px); }\
.card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
.card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
.card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
footer { text-align: center; padding: 3rem 2rem; background: #0b0f19; border-top: 1px solid rgba(255,255,255,0.05); color: #64748b; font-size: 0.9rem; margin-top: auto; }\
</style>");
html.push_str("</head><body>");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
html.push_str("</body></html>");
render_website(&mut html, dsl);
}
"page" => {
html.push_str("<div class=\"page\">");
if let Some(RuntimeValue::Str(t)) = dsl.args.first() {
html.push_str(&format!("<h1>{}</h1>", t));
}
for prop in &dsl.properties {
if prop.name == "title" {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h2>{}</h2>", t));
}
}
}
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
html.push_str("</div>");
render_page(&mut html, dsl);
}
"hero" => {
html.push_str("<section class=\"hero\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h1>{}</h1>", t));
}
}
"subtitle" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<p class=\"subtitle\">{}</p>", t));
}
}
_ => {}
}
}
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
html.push_str("</section>");
render_hero(&mut html, dsl);
}
"section" => {
html.push_str("<section class=\"content-section\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h2>{}</h2>", t));
}
}
"id" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<!-- id: {} -->", t));
}
}
_ => {}
}
}
html.push_str("<div class=\"grid-layout\">");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
html.push_str("</div>");
html.push_str("</section>");
render_section(&mut html, dsl);
}
"card" => {
html.push_str("<div class=\"card\">");
for prop in &dsl.properties {
match prop.name.as_str() {
"title" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<h3>{}</h3>", t));
}
}
"text" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!("<p>{}</p>", t));
}
}
"image" => {
if let Some(RuntimeValue::Str(t)) = &prop.value {
html.push_str(&format!(
"<img src=\"{}\" alt=\"card image\">",
t
));
}
}
_ => {}
}
}
html.push_str("</div>");
render_card(&mut html, dsl);
}
"button" => {
let label = dsl
Expand Down Expand Up @@ -271,11 +280,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
}
"nav" => {
html.push_str("<nav>");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</nav>");
}
"header" => {
Expand All @@ -287,11 +292,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
}
}
}
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</header>");
}
"footer" => {
Expand All @@ -303,11 +304,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
}
}
}
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</footer>");
}
"input" => {
Expand All @@ -325,29 +322,17 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
}
"form" => {
html.push_str("<form>");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</form>");
}
"main" => {
html.push_str("<main>");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</main>");
}
"aside" => {
html.push_str("<aside>");
for child in &dsl.children {
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
child.clone(),
))));
}
render_children(&mut html, dsl);
html.push_str("</aside>");
}
"start" => {
Expand Down
Loading