@@ -95,152 +95,161 @@ use techscript_runtime::{
9595static SERVER_RUNNING : AtomicBool = AtomicBool :: new ( false ) ;
9696static 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.
99234fn 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