@@ -2,8 +2,12 @@ package rtml
22
33import (
44 "fmt"
5+ "reflect"
56 "regexp"
7+ "strconv"
68 "strings"
9+
10+ "github.com/rfwlab/rfw/v1/state"
711)
812
913// Dependency represents a renderable component used for includes.
@@ -23,6 +27,11 @@ func Replace(template string, ctx Context) string {
2327 rendered := replacePropPlaceholders (template , ctx )
2428 rendered = replaceIncludePlaceholders (ctx , rendered )
2529 rendered = replaceSlotPlaceholders (rendered , ctx )
30+ rendered = replaceForPlaceholders (rendered , ctx )
31+ rendered = replaceConditionals (rendered , ctx )
32+ rendered = replaceStorePlaceholders (rendered , ctx )
33+ rendered = replaceRtIsAttributes (rendered , ctx )
34+ rendered = replaceIncludePlaceholders (ctx , rendered )
2635 return rendered
2736}
2837
@@ -59,3 +68,329 @@ func replaceIncludePlaceholders(ctx Context, template string) string {
5968 }
6069 return template
6170}
71+
72+ func replaceForPlaceholders (template string , ctx Context ) string {
73+ forRegex := regexp .MustCompile (`@for:(\w+(?:,\w+)?)\s+in\s+(\S+)([\s\S]*?)@endfor` )
74+ return forRegex .ReplaceAllStringFunc (template , func (match string ) string {
75+ parts := forRegex .FindStringSubmatch (match )
76+ if len (parts ) < 4 {
77+ return match
78+ }
79+ aliasesPart := parts [1 ]
80+ expr := parts [2 ]
81+ loopContent := parts [3 ]
82+
83+ aliases := strings .Split (aliasesPart , "," )
84+ for i := range aliases {
85+ aliases [i ] = strings .TrimSpace (aliases [i ])
86+ }
87+
88+ if strings .Contains (expr , ".." ) {
89+ rangeParts := strings .Split (expr , ".." )
90+ if len (rangeParts ) != 2 {
91+ return match
92+ }
93+ start , ok1 := resolveNumber (rangeParts [0 ], ctx )
94+ end , ok2 := resolveNumber (rangeParts [1 ], ctx )
95+ if ! ok1 || ! ok2 {
96+ return match
97+ }
98+ var sb strings.Builder
99+ for i := start ; i <= end ; i ++ {
100+ iter := strings .ReplaceAll (loopContent , fmt .Sprintf ("@prop:%s" , aliases [0 ]), fmt .Sprintf ("%d" , i ))
101+ sb .WriteString (iter )
102+ }
103+ return sb .String ()
104+ }
105+
106+ collection , ok := ctx .Props [expr ]
107+ if ! ok {
108+ if strings .HasPrefix (expr , "store:" ) {
109+ parts := strings .Split (strings .TrimPrefix (expr , "store:" ), "." )
110+ if len (parts ) == 3 {
111+ if store := state .GlobalStoreManager .GetStore (parts [0 ], parts [1 ]); store != nil {
112+ collection = store .Get (parts [2 ])
113+ ok = true
114+ }
115+ }
116+ }
117+ if ! ok {
118+ return match
119+ }
120+ }
121+
122+ val := reflect .ValueOf (collection )
123+ switch val .Kind () {
124+ case reflect .Slice , reflect .Array :
125+ var sb strings.Builder
126+ alias := aliases [0 ]
127+ for i := 0 ; i < val .Len (); i ++ {
128+ item := val .Index (i ).Interface ()
129+ iter := loopContent
130+ iter = replaceAlias (iter , alias , item )
131+ sb .WriteString (iter )
132+ }
133+ return sb .String ()
134+ case reflect .Map :
135+ keys := val .MapKeys ()
136+ aliasKey := aliases [0 ]
137+ aliasVal := aliasKey
138+ if len (aliases ) > 1 {
139+ aliasVal = aliases [1 ]
140+ }
141+ var sb strings.Builder
142+ for _ , k := range keys {
143+ v := val .MapIndex (k ).Interface ()
144+ iter := loopContent
145+ iter = strings .ReplaceAll (iter , fmt .Sprintf ("@prop:%s" , aliasKey ), fmt .Sprintf ("%v" , k .Interface ()))
146+ iter = replaceAlias (iter , aliasVal , v )
147+ sb .WriteString (iter )
148+ }
149+ return sb .String ()
150+ default :
151+ return match
152+ }
153+ })
154+ }
155+
156+ func replaceAlias (template , alias string , val any ) string {
157+ switch v := val .(type ) {
158+ case Dependency :
159+ return strings .ReplaceAll (template , fmt .Sprintf ("@prop:%s" , alias ), v .Render ())
160+ case map [string ]any :
161+ re := regexp .MustCompile (fmt .Sprintf (`@prop:%s\\.(\\w+)` , regexp .QuoteMeta (alias )))
162+ return re .ReplaceAllStringFunc (template , func (m string ) string {
163+ parts := re .FindStringSubmatch (m )
164+ if len (parts ) == 2 {
165+ if fv , ok := v [parts [1 ]]; ok {
166+ return fmt .Sprintf ("%v" , fv )
167+ }
168+ }
169+ return m
170+ })
171+ default :
172+ return strings .ReplaceAll (template , fmt .Sprintf ("@prop:%s" , alias ), fmt .Sprintf ("%v" , v ))
173+ }
174+ }
175+
176+ func resolveNumber (expr string , ctx Context ) (int , bool ) {
177+ expr = strings .TrimSpace (expr )
178+ if v , ok := ctx .Props [expr ]; ok {
179+ switch n := v .(type ) {
180+ case int :
181+ return n , true
182+ case int64 :
183+ return int (n ), true
184+ case float64 :
185+ return int (n ), true
186+ case string :
187+ i , err := strconv .Atoi (n )
188+ if err == nil {
189+ return i , true
190+ }
191+ }
192+ }
193+ if strings .HasPrefix (expr , "store:" ) {
194+ parts := strings .Split (strings .TrimPrefix (expr , "store:" ), "." )
195+ if len (parts ) == 3 {
196+ if store := state .GlobalStoreManager .GetStore (parts [0 ], parts [1 ]); store != nil {
197+ if v := store .Get (parts [2 ]); v != nil {
198+ switch n := v .(type ) {
199+ case int :
200+ return n , true
201+ case int64 :
202+ return int (n ), true
203+ case float64 :
204+ return int (n ), true
205+ case string :
206+ i , err := strconv .Atoi (n )
207+ if err == nil {
208+ return i , true
209+ }
210+ }
211+ }
212+ }
213+ }
214+ }
215+ i , err := strconv .Atoi (expr )
216+ if err == nil {
217+ return i , true
218+ }
219+ return 0 , false
220+ }
221+
222+ // --- Conditional rendering ---
223+
224+ type node interface {
225+ Render (ctx Context ) string
226+ }
227+
228+ type textNode struct { Text string }
229+
230+ func (t * textNode ) Render (ctx Context ) string { return t .Text }
231+
232+ type conditionalBranch struct {
233+ Condition string
234+ Nodes []node
235+ }
236+
237+ type conditionalNode struct { Branches []conditionalBranch }
238+
239+ func (cn * conditionalNode ) Render (ctx Context ) string {
240+ for _ , br := range cn .Branches {
241+ if br .Condition == "" || evaluateCondition (br .Condition , ctx ) {
242+ var sb strings.Builder
243+ for _ , n := range br .Nodes {
244+ sb .WriteString (n .Render (ctx ))
245+ }
246+ return sb .String ()
247+ }
248+ }
249+ return ""
250+ }
251+
252+ func replaceConditionals (template string , ctx Context ) string {
253+ if ! strings .Contains (template , "@if:" ) {
254+ return template
255+ }
256+ nodes , err := parseTemplate (template )
257+ if err != nil {
258+ return template
259+ }
260+ var sb strings.Builder
261+ for _ , n := range nodes {
262+ sb .WriteString (n .Render (ctx ))
263+ }
264+ return sb .String ()
265+ }
266+
267+ func parseTemplate (template string ) ([]node , error ) {
268+ lines := strings .Split (template , "\n " )
269+ idx := 0
270+ return parseBlock (lines , & idx )
271+ }
272+
273+ func parseBlock (lines []string , idx * int ) ([]node , error ) {
274+ var nodes []node
275+ for * idx < len (lines ) {
276+ line := lines [* idx ]
277+ trimmed := strings .TrimSpace (line )
278+ switch {
279+ case strings .HasPrefix (trimmed , "@if:" ):
280+ cond := trimmed
281+ * idx ++
282+ n , err := parseConditional (lines , idx , cond )
283+ if err != nil {
284+ return nil , err
285+ }
286+ nodes = append (nodes , n )
287+ case strings .HasPrefix (trimmed , "@else-if:" ), trimmed == "@else" , trimmed == "@endif" :
288+ return nodes , nil
289+ default :
290+ nodes = append (nodes , & textNode {Text : line + "\n " })
291+ * idx ++
292+ }
293+ }
294+ return nodes , nil
295+ }
296+
297+ func parseConditional (lines []string , idx * int , firstCond string ) (node , error ) {
298+ n := & conditionalNode {}
299+ children , err := parseBlock (lines , idx )
300+ if err != nil {
301+ return nil , err
302+ }
303+ n .Branches = append (n .Branches , conditionalBranch {Condition : firstCond , Nodes : children })
304+
305+ for * idx < len (lines ) {
306+ trimmed := strings .TrimSpace (lines [* idx ])
307+ switch {
308+ case strings .HasPrefix (trimmed , "@else-if:" ):
309+ cond := trimmed
310+ * idx ++
311+ children , err := parseBlock (lines , idx )
312+ if err != nil {
313+ return nil , err
314+ }
315+ n .Branches = append (n .Branches , conditionalBranch {Condition : cond , Nodes : children })
316+ case trimmed == "@else" :
317+ * idx ++
318+ children , err := parseBlock (lines , idx )
319+ if err != nil {
320+ return nil , err
321+ }
322+ n .Branches = append (n .Branches , conditionalBranch {Condition : "" , Nodes : children })
323+ case trimmed == "@endif" :
324+ * idx ++
325+ return n , nil
326+ default :
327+ * idx ++
328+ }
329+ }
330+ return n , nil
331+ }
332+
333+ func evaluateCondition (condition string , ctx Context ) bool {
334+ parts := strings .Split (condition , "==" )
335+ if len (parts ) != 2 {
336+ return false
337+ }
338+ left := strings .TrimSpace (parts [0 ])
339+ right := strings .Trim (parts [1 ], "\" ' " )
340+ left = strings .TrimPrefix (left , "@if:" )
341+ left = strings .TrimPrefix (left , "@else-if:" )
342+ if strings .HasPrefix (left , "prop:" ) {
343+ key := strings .TrimPrefix (left , "prop:" )
344+ if v , ok := ctx .Props [key ]; ok {
345+ return fmt .Sprintf ("%v" , v ) == right
346+ }
347+ } else if strings .HasPrefix (left , "store:" ) {
348+ parts := strings .Split (strings .TrimPrefix (left , "store:" ), "." )
349+ if len (parts ) == 3 {
350+ if store := state .GlobalStoreManager .GetStore (parts [0 ], parts [1 ]); store != nil {
351+ if v := store .Get (parts [2 ]); v != nil {
352+ return fmt .Sprintf ("%v" , v ) == right
353+ }
354+ }
355+ }
356+ }
357+ return false
358+ }
359+
360+ // --- Stores ---
361+
362+ func replaceStorePlaceholders (template string , ctx Context ) string {
363+ re := regexp .MustCompile (`@store:(\w+)\.(\w+)\.(\w+)(:w)?` )
364+ return re .ReplaceAllStringFunc (template , func (match string ) string {
365+ parts := re .FindStringSubmatch (match )
366+ if len (parts ) < 5 {
367+ return match
368+ }
369+ if parts [4 ] == ":w" {
370+ return match
371+ }
372+ if store := state .GlobalStoreManager .GetStore (parts [1 ], parts [2 ]); store != nil {
373+ if v := store .Get (parts [3 ]); v != nil {
374+ return fmt .Sprintf ("%v" , v )
375+ }
376+ }
377+ return match
378+ })
379+ }
380+
381+ // --- rt-is ---
382+
383+ func replaceRtIsAttributes (template string , ctx Context ) string {
384+ re := regexp .MustCompile (`<([a-zA-Z0-9]+)([^>]*)rt-is="([^"]+)"[^>]*/?>` )
385+ return re .ReplaceAllStringFunc (template , func (match string ) string {
386+ parts := re .FindStringSubmatch (match )
387+ if len (parts ) < 4 {
388+ return match
389+ }
390+ name := parts [3 ]
391+ if dep , ok := ctx .Dependencies [name ]; ok {
392+ return dep .Render ()
393+ }
394+ return match
395+ })
396+ }
0 commit comments