@@ -92,152 +92,161 @@ use techscript_runtime::{
9292static SERVER_RUNNING : AtomicBool = AtomicBool :: new ( false ) ;
9393static PAGE_CONTENT : Mutex < String > = Mutex :: new ( String :: new ( ) ) ;
9494
95+ fn render_children ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
96+ for child in & dsl. children {
97+ html. push_str ( & dsl_to_html (
98+ & techscript_runtime:: value:: RuntimeValue :: DslBlock ( std:: rc:: Rc :: new ( child. clone ( ) ) ) ,
99+ ) ) ;
100+ }
101+ }
102+
103+ fn render_website ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
104+ html. push_str ( "<!DOCTYPE html><html><head><meta charset=\" UTF-8\" ><meta name=\" viewport\" content=\" width=device-width, initial-scale=1.0\" >" ) ;
105+ for prop in & dsl. properties {
106+ match prop. name . as_str ( ) {
107+ "title" => {
108+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
109+ html. push_str ( & format ! ( "<title>{}</title>" , t) ) ;
110+ }
111+ }
112+ _ => { }
113+ }
114+ }
115+ html. push_str ( "<style>\
116+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
117+ body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
118+ 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; }\
119+ 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; }\
120+ nav { display: flex; gap: 1.5rem; }\
121+ nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
122+ nav a:hover { color: #f8fafc; }\
123+ .page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
124+ .hero { text-align: center; padding: 5rem 1rem; position: relative; }\
125+ .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; }\
126+ .hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
127+ .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; }\
128+ .hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
129+ .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; }\
130+ .hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
131+ .content-section { margin-top: 5rem; }\
132+ .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; }\
133+ .grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
134+ .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); }\
135+ .card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
136+ .card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
137+ .card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
138+ 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; }\
139+ </style>") ;
140+ html. push_str ( "</head><body>" ) ;
141+ render_children ( html, dsl) ;
142+ html. push_str ( "</body></html>" ) ;
143+ }
144+
145+ fn render_page ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
146+ html. push_str ( "<div class=\" page\" >" ) ;
147+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = dsl. args . first ( ) {
148+ html. push_str ( & format ! ( "<h1>{}</h1>" , t) ) ;
149+ }
150+ for prop in & dsl. properties {
151+ if prop. name == "title" {
152+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
153+ html. push_str ( & format ! ( "<h2>{}</h2>" , t) ) ;
154+ }
155+ }
156+ }
157+ render_children ( html, dsl) ;
158+ html. push_str ( "</div>" ) ;
159+ }
160+
161+ fn render_hero ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
162+ html. push_str ( "<section class=\" hero\" >" ) ;
163+ for prop in & dsl. properties {
164+ match prop. name . as_str ( ) {
165+ "title" => {
166+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
167+ html. push_str ( & format ! ( "<h1>{}</h1>" , t) ) ;
168+ }
169+ }
170+ "subtitle" => {
171+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
172+ html. push_str ( & format ! ( "<p class=\" subtitle\" >{}</p>" , t) ) ;
173+ }
174+ }
175+ _ => { }
176+ }
177+ }
178+ render_children ( html, dsl) ;
179+ html. push_str ( "</section>" ) ;
180+ }
181+
182+ fn render_section ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
183+ html. push_str ( "<section class=\" content-section\" >" ) ;
184+ for prop in & dsl. properties {
185+ match prop. name . as_str ( ) {
186+ "title" => {
187+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
188+ html. push_str ( & format ! ( "<h2>{}</h2>" , t) ) ;
189+ }
190+ }
191+ "id" => {
192+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
193+ html. push_str ( & format ! ( "<!-- id: {} -->" , t) ) ;
194+ }
195+ }
196+ _ => { }
197+ }
198+ }
199+ html. push_str ( "<div class=\" grid-layout\" >" ) ;
200+ render_children ( html, dsl) ;
201+ html. push_str ( "</div>" ) ;
202+ html. push_str ( "</section>" ) ;
203+ }
204+
205+ fn render_card ( html : & mut String , dsl : & techscript_runtime:: value:: DslBlockValue ) {
206+ html. push_str ( "<div class=\" card\" >" ) ;
207+ for prop in & dsl. properties {
208+ match prop. name . as_str ( ) {
209+ "title" => {
210+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
211+ html. push_str ( & format ! ( "<h3>{}</h3>" , t) ) ;
212+ }
213+ }
214+ "text" => {
215+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
216+ html. push_str ( & format ! ( "<p>{}</p>" , t) ) ;
217+ }
218+ }
219+ "image" => {
220+ if let Some ( techscript_runtime:: value:: RuntimeValue :: Str ( t) ) = & prop. value {
221+ html. push_str ( & format ! ( "<img src=\" {}\" alt=\" card image\" >" , t) ) ;
222+ }
223+ }
224+ _ => { }
225+ }
226+ }
227+ html. push_str ( "</div>" ) ;
228+ }
229+
95230/// Convert a DslBlockValue tree to HTML string.
96231fn dsl_to_html ( val : & RuntimeValue ) -> String {
97232 match val {
98233 RuntimeValue :: DslBlock ( dsl) => {
99234 let mut html = String :: new ( ) ;
100235 match dsl. kind . as_str ( ) {
101236 "website" => {
102- html. push_str ( "<!DOCTYPE html><html><head><meta charset=\" UTF-8\" ><meta name=\" viewport\" content=\" width=device-width, initial-scale=1.0\" >" ) ;
103- for prop in & dsl. properties {
104- match prop. name . as_str ( ) {
105- "title" => {
106- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
107- html. push_str ( & format ! ( "<title>{}</title>" , t) ) ;
108- }
109- }
110- _ => { }
111- }
112- }
113- html. push_str ( "<style>\
114- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');\
115- body { background: #0f172a; color: #f8fafc; font-family: 'Outfit', sans-serif; margin: 0; padding: 0; min-height: 100vh; display: flex; flex-direction: column; }\
116- 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; }\
117- 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; }\
118- nav { display: flex; gap: 1.5rem; }\
119- nav a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }\
120- nav a:hover { color: #f8fafc; }\
121- .page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem; flex: 1; }\
122- .hero { text-align: center; padding: 5rem 1rem; position: relative; }\
123- .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; }\
124- .hero .subtitle { font-size: 1.5rem; color: #94a3b8; max-width: 800px; margin: 0 auto 3rem auto; line-height: 1.6; }\
125- .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; }\
126- .hero a.start-button:hover { background: #2563eb; transform: translateY(-2px); }\
127- .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; }\
128- .hero button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }\
129- .content-section { margin-top: 5rem; }\
130- .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; }\
131- .grid-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 2rem; }\
132- .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); }\
133- .card:hover { transform: translateY(-5px); border-color: rgba(168, 85, 247, 0.4); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }\
134- .card h3 { font-size: 1.5rem; margin: 0 0 1rem 0; color: #c084fc; }\
135- .card p { color: #94a3b8; line-height: 1.6; margin: 0; }\
136- 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; }\
137- </style>") ;
138- html. push_str ( "</head><body>" ) ;
139- for child in & dsl. children {
140- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
141- child. clone ( ) ,
142- ) ) ) ) ;
143- }
144- html. push_str ( "</body></html>" ) ;
237+ render_website ( & mut html, dsl) ;
145238 }
146239 "page" => {
147- html. push_str ( "<div class=\" page\" >" ) ;
148- if let Some ( RuntimeValue :: Str ( t) ) = dsl. args . first ( ) {
149- html. push_str ( & format ! ( "<h1>{}</h1>" , t) ) ;
150- }
151- for prop in & dsl. properties {
152- if prop. name == "title" {
153- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
154- html. push_str ( & format ! ( "<h2>{}</h2>" , t) ) ;
155- }
156- }
157- }
158- for child in & dsl. children {
159- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
160- child. clone ( ) ,
161- ) ) ) ) ;
162- }
163- html. push_str ( "</div>" ) ;
240+ render_page ( & mut html, dsl) ;
164241 }
165242 "hero" => {
166- html. push_str ( "<section class=\" hero\" >" ) ;
167- for prop in & dsl. properties {
168- match prop. name . as_str ( ) {
169- "title" => {
170- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
171- html. push_str ( & format ! ( "<h1>{}</h1>" , t) ) ;
172- }
173- }
174- "subtitle" => {
175- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
176- html. push_str ( & format ! ( "<p class=\" subtitle\" >{}</p>" , t) ) ;
177- }
178- }
179- _ => { }
180- }
181- }
182- for child in & dsl. children {
183- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
184- child. clone ( ) ,
185- ) ) ) ) ;
186- }
187- html. push_str ( "</section>" ) ;
243+ render_hero ( & mut html, dsl) ;
188244 }
189245 "section" => {
190- html. push_str ( "<section class=\" content-section\" >" ) ;
191- for prop in & dsl. properties {
192- match prop. name . as_str ( ) {
193- "title" => {
194- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
195- html. push_str ( & format ! ( "<h2>{}</h2>" , t) ) ;
196- }
197- }
198- "id" => {
199- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
200- html. push_str ( & format ! ( "<!-- id: {} -->" , t) ) ;
201- }
202- }
203- _ => { }
204- }
205- }
206- html. push_str ( "<div class=\" grid-layout\" >" ) ;
207- for child in & dsl. children {
208- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
209- child. clone ( ) ,
210- ) ) ) ) ;
211- }
212- html. push_str ( "</div>" ) ;
213- html. push_str ( "</section>" ) ;
246+ render_section ( & mut html, dsl) ;
214247 }
215248 "card" => {
216- html. push_str ( "<div class=\" card\" >" ) ;
217- for prop in & dsl. properties {
218- match prop. name . as_str ( ) {
219- "title" => {
220- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
221- html. push_str ( & format ! ( "<h3>{}</h3>" , t) ) ;
222- }
223- }
224- "text" => {
225- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
226- html. push_str ( & format ! ( "<p>{}</p>" , t) ) ;
227- }
228- }
229- "image" => {
230- if let Some ( RuntimeValue :: Str ( t) ) = & prop. value {
231- html. push_str ( & format ! (
232- "<img src=\" {}\" alt=\" card image\" >" ,
233- t
234- ) ) ;
235- }
236- }
237- _ => { }
238- }
239- }
240- html. push_str ( "</div>" ) ;
249+ render_card ( & mut html, dsl) ;
241250 }
242251 "button" => {
243252 let label = dsl
@@ -271,11 +280,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
271280 }
272281 "nav" => {
273282 html. push_str ( "<nav>" ) ;
274- for child in & dsl. children {
275- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
276- child. clone ( ) ,
277- ) ) ) ) ;
278- }
283+ render_children ( & mut html, dsl) ;
279284 html. push_str ( "</nav>" ) ;
280285 }
281286 "header" => {
@@ -287,11 +292,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
287292 }
288293 }
289294 }
290- for child in & dsl. children {
291- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
292- child. clone ( ) ,
293- ) ) ) ) ;
294- }
295+ render_children ( & mut html, dsl) ;
295296 html. push_str ( "</header>" ) ;
296297 }
297298 "footer" => {
@@ -303,11 +304,7 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
303304 }
304305 }
305306 }
306- for child in & dsl. children {
307- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
308- child. clone ( ) ,
309- ) ) ) ) ;
310- }
307+ render_children ( & mut html, dsl) ;
311308 html. push_str ( "</footer>" ) ;
312309 }
313310 "input" => {
@@ -325,29 +322,17 @@ fn dsl_to_html(val: &RuntimeValue) -> String {
325322 }
326323 "form" => {
327324 html. push_str ( "<form>" ) ;
328- for child in & dsl. children {
329- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
330- child. clone ( ) ,
331- ) ) ) ) ;
332- }
325+ render_children ( & mut html, dsl) ;
333326 html. push_str ( "</form>" ) ;
334327 }
335328 "main" => {
336329 html. push_str ( "<main>" ) ;
337- for child in & dsl. children {
338- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
339- child. clone ( ) ,
340- ) ) ) ) ;
341- }
330+ render_children ( & mut html, dsl) ;
342331 html. push_str ( "</main>" ) ;
343332 }
344333 "aside" => {
345334 html. push_str ( "<aside>" ) ;
346- for child in & dsl. children {
347- html. push_str ( & dsl_to_html ( & RuntimeValue :: DslBlock ( Rc :: new (
348- child. clone ( ) ,
349- ) ) ) ) ;
350- }
335+ render_children ( & mut html, dsl) ;
351336 html. push_str ( "</aside>" ) ;
352337 }
353338 "start" => {
0 commit comments