Skip to content

Commit 5dbaba3

Browse files
authored
Merge pull request #122 from Tcode-Motion/code-health-refactor-web-294185737966890269
🧹 [code health improvement] Refactor `dsl_to_html` in `web.rs`
2 parents 158856d + 6c39b80 commit 5dbaba3

1 file changed

Lines changed: 146 additions & 161 deletions

File tree

‎stdlib/src/web.rs‎

Lines changed: 146 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -95,152 +95,161 @@ use techscript_runtime::{
9595
static SERVER_RUNNING: AtomicBool = AtomicBool::new(false);
9696
static PAGE_CONTENT: Mutex<String> = Mutex::new(String::new());
9797

98+
fn render_children(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
99+
for child in &dsl.children {
100+
html.push_str(&dsl_to_html(
101+
&techscript_runtime::value::RuntimeValue::DslBlock(std::rc::Rc::new(child.clone())),
102+
));
103+
}
104+
}
105+
106+
fn render_website(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
107+
html.push_str("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
108+
for prop in &dsl.properties {
109+
match prop.name.as_str() {
110+
"title" => {
111+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
112+
html.push_str(&format!("<title>{}</title>", t));
113+
}
114+
}
115+
_ => {}
116+
}
117+
}
118+
html.push_str("<style>\
119+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
120+
body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
121+
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; }\
122+
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; }\
123+
nav { display: flex; gap: 1.5rem; }\
124+
nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
125+
nav a:hover { color: #f8fafc; }\
126+
.page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
127+
.hero { text-align: center; padding: 5rem 1rem; position: relative; }\
128+
.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; }\
129+
.hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
130+
.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; }\
131+
.hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
132+
.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; }\
133+
.hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
134+
.content-section { margin-top: 5rem; }\
135+
.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; }\
136+
.grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
137+
.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); }\
138+
.card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
139+
.card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
140+
.card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
141+
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; }\
142+
</style>");
143+
html.push_str("</head><body>");
144+
render_children(html, dsl);
145+
html.push_str("</body></html>");
146+
}
147+
148+
fn render_page(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
149+
html.push_str("<div class=\"page\">");
150+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = dsl.args.first() {
151+
html.push_str(&format!("<h1>{}</h1>", t));
152+
}
153+
for prop in &dsl.properties {
154+
if prop.name == "title" {
155+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
156+
html.push_str(&format!("<h2>{}</h2>", t));
157+
}
158+
}
159+
}
160+
render_children(html, dsl);
161+
html.push_str("</div>");
162+
}
163+
164+
fn render_hero(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
165+
html.push_str("<section class=\"hero\">");
166+
for prop in &dsl.properties {
167+
match prop.name.as_str() {
168+
"title" => {
169+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
170+
html.push_str(&format!("<h1>{}</h1>", t));
171+
}
172+
}
173+
"subtitle" => {
174+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
175+
html.push_str(&format!("<p class=\"subtitle\">{}</p>", t));
176+
}
177+
}
178+
_ => {}
179+
}
180+
}
181+
render_children(html, dsl);
182+
html.push_str("</section>");
183+
}
184+
185+
fn render_section(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
186+
html.push_str("<section class=\"content-section\">");
187+
for prop in &dsl.properties {
188+
match prop.name.as_str() {
189+
"title" => {
190+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
191+
html.push_str(&format!("<h2>{}</h2>", t));
192+
}
193+
}
194+
"id" => {
195+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
196+
html.push_str(&format!("<!-- id: {} -->", t));
197+
}
198+
}
199+
_ => {}
200+
}
201+
}
202+
html.push_str("<div class=\"grid-layout\">");
203+
render_children(html, dsl);
204+
html.push_str("</div>");
205+
html.push_str("</section>");
206+
}
207+
208+
fn render_card(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) {
209+
html.push_str("<div class=\"card\">");
210+
for prop in &dsl.properties {
211+
match prop.name.as_str() {
212+
"title" => {
213+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
214+
html.push_str(&format!("<h3>{}</h3>", t));
215+
}
216+
}
217+
"text" => {
218+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
219+
html.push_str(&format!("<p>{}</p>", t));
220+
}
221+
}
222+
"image" => {
223+
if let Some(techscript_runtime::value::RuntimeValue::Str(t)) = &prop.value {
224+
html.push_str(&format!("<img src=\"{}\" alt=\"card image\">", t));
225+
}
226+
}
227+
_ => {}
228+
}
229+
}
230+
html.push_str("</div>");
231+
}
232+
98233
/// Convert a DslBlockValue tree to HTML string.
99234
fn dsl_to_html(val: &RuntimeValue) -> String {
100235
match val {
101236
RuntimeValue::DslBlock(dsl) => {
102237
let mut html = String::new();
103238
match dsl.kind.as_str() {
104239
"website" => {
105-
html.push_str("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
106-
for prop in &dsl.properties {
107-
match prop.name.as_str() {
108-
"title" => {
109-
if let Some(RuntimeValue::Str(t)) = &prop.value {
110-
html.push_str(&format!("<title>{}</title>", t));
111-
}
112-
}
113-
_ => {}
114-
}
115-
}
116-
html.push_str("<style>\
117-
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
118-
body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
119-
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; }\
120-
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; }\
121-
nav { display: flex; gap: 1.5rem; }\
122-
nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
123-
nav a:hover { color: #f8fafc; }\
124-
.page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
125-
.hero { text-align: center; padding: 5rem 1rem; position: relative; }\
126-
.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; }\
127-
.hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
128-
.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; }\
129-
.hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
130-
.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; }\
131-
.hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
132-
.content-section { margin-top: 5rem; }\
133-
.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; }\
134-
.grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
135-
.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); }\
136-
.card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
137-
.card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
138-
.card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
139-
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; }\
140-
</style>");
141-
html.push_str("</head><body>");
142-
for child in &dsl.children {
143-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
144-
child.clone(),
145-
))));
146-
}
147-
html.push_str("</body></html>");
240+
render_website(&mut html, dsl);
148241
}
149242
"page" => {
150-
html.push_str("<div class=\"page\">");
151-
if let Some(RuntimeValue::Str(t)) = dsl.args.first() {
152-
html.push_str(&format!("<h1>{}</h1>", t));
153-
}
154-
for prop in &dsl.properties {
155-
if prop.name == "title" {
156-
if let Some(RuntimeValue::Str(t)) = &prop.value {
157-
html.push_str(&format!("<h2>{}</h2>", t));
158-
}
159-
}
160-
}
161-
for child in &dsl.children {
162-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
163-
child.clone(),
164-
))));
165-
}
166-
html.push_str("</div>");
243+
render_page(&mut html, dsl);
167244
}
168245
"hero" => {
169-
html.push_str("<section class=\"hero\">");
170-
for prop in &dsl.properties {
171-
match prop.name.as_str() {
172-
"title" => {
173-
if let Some(RuntimeValue::Str(t)) = &prop.value {
174-
html.push_str(&format!("<h1>{}</h1>", t));
175-
}
176-
}
177-
"subtitle" => {
178-
if let Some(RuntimeValue::Str(t)) = &prop.value {
179-
html.push_str(&format!("<p class=\"subtitle\">{}</p>", t));
180-
}
181-
}
182-
_ => {}
183-
}
184-
}
185-
for child in &dsl.children {
186-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
187-
child.clone(),
188-
))));
189-
}
190-
html.push_str("</section>");
246+
render_hero(&mut html, dsl);
191247
}
192248
"section" => {
193-
html.push_str("<section class=\"content-section\">");
194-
for prop in &dsl.properties {
195-
match prop.name.as_str() {
196-
"title" => {
197-
if let Some(RuntimeValue::Str(t)) = &prop.value {
198-
html.push_str(&format!("<h2>{}</h2>", t));
199-
}
200-
}
201-
"id" => {
202-
if let Some(RuntimeValue::Str(t)) = &prop.value {
203-
html.push_str(&format!("<!-- id: {} -->", t));
204-
}
205-
}
206-
_ => {}
207-
}
208-
}
209-
html.push_str("<div class=\"grid-layout\">");
210-
for child in &dsl.children {
211-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
212-
child.clone(),
213-
))));
214-
}
215-
html.push_str("</div>");
216-
html.push_str("</section>");
249+
render_section(&mut html, dsl);
217250
}
218251
"card" => {
219-
html.push_str("<div class=\"card\">");
220-
for prop in &dsl.properties {
221-
match prop.name.as_str() {
222-
"title" => {
223-
if let Some(RuntimeValue::Str(t)) = &prop.value {
224-
html.push_str(&format!("<h3>{}</h3>", t));
225-
}
226-
}
227-
"text" => {
228-
if let Some(RuntimeValue::Str(t)) = &prop.value {
229-
html.push_str(&format!("<p>{}</p>", t));
230-
}
231-
}
232-
"image" => {
233-
if let Some(RuntimeValue::Str(t)) = &prop.value {
234-
html.push_str(&format!(
235-
"<img src=\"{}\" alt=\"card image\">",
236-
t
237-
));
238-
}
239-
}
240-
_ => {}
241-
}
242-
}
243-
html.push_str("</div>");
252+
render_card(&mut html, dsl);
244253
}
245254
"button" => {
246255
let label = dsl
@@ -274,11 +283,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
274283
}
275284
"nav" => {
276285
html.push_str("<nav>");
277-
for child in &dsl.children {
278-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
279-
child.clone(),
280-
))));
281-
}
286+
render_children(&mut html, dsl);
282287
html.push_str("</nav>");
283288
}
284289
"header" => {
@@ -290,11 +295,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
290295
}
291296
}
292297
}
293-
for child in &dsl.children {
294-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
295-
child.clone(),
296-
))));
297-
}
298+
render_children(&mut html, dsl);
298299
html.push_str("</header>");
299300
}
300301
"footer" => {
@@ -306,11 +307,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
306307
}
307308
}
308309
}
309-
for child in &dsl.children {
310-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
311-
child.clone(),
312-
))));
313-
}
310+
render_children(&mut html, dsl);
314311
html.push_str("</footer>");
315312
}
316313
"input" => {
@@ -328,29 +325,17 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
328325
}
329326
"form" => {
330327
html.push_str("<form>");
331-
for child in &dsl.children {
332-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
333-
child.clone(),
334-
))));
335-
}
328+
render_children(&mut html, dsl);
336329
html.push_str("</form>");
337330
}
338331
"main" => {
339332
html.push_str("<main>");
340-
for child in &dsl.children {
341-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
342-
child.clone(),
343-
))));
344-
}
333+
render_children(&mut html, dsl);
345334
html.push_str("</main>");
346335
}
347336
"aside" => {
348337
html.push_str("<aside>");
349-
for child in &dsl.children {
350-
html.push_str(&dsl_to_html(&RuntimeValue::DslBlock(Rc::new(
351-
child.clone(),
352-
))));
353-
}
338+
render_children(&mut html, dsl);
354339
html.push_str("</aside>");
355340
}
356341
"start" => {

0 commit comments

Comments
 (0)