Skip to content

Commit 43d9848

Browse files
committed
feat: extend rtml methods for SSR rendering
1 parent 3a0c03f commit 43d9848

File tree

2 files changed

+393
-0
lines changed

2 files changed

+393
-0
lines changed

v1/rtml/rtml.go

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package rtml
22

33
import (
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

Comments
 (0)