Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

template.gno

14.32 Kb · 697 lines
  1package template
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"unicode"
  7
  8	"gno.land/p/nym-moultest123/md/v0"
  9	"gno.land/p/nym-moultest123/typeutil/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13// Renderer provides a simple template engine with a clean API
 14type Renderer struct {
 15	funcs map[string]Func  // Template functions
 16	data  map[string]interface{} // Global data
 17	ctx   *context       // Current evaluation context
 18}
 19
 20// Func is the signature for template functions
 21type Func func(args ...string) string
 22
 23// context tracks variable scope during evaluation
 24type context struct {
 25	parent *context
 26	vars   map[string]interface{} // Local variables
 27	loop   *loopContext          // Range loop state
 28}
 29
 30// loopContext holds state for range loops
 31type loopContext struct {
 32	items []interface{} // Items being iterated
 33	index int          // Current index
 34}
 35
 36// NewRenderer creates a new template renderer with default functions
 37func NewRenderer() *Renderer {
 38	r := &Renderer{
 39		funcs: make(map[string]Func),
 40		data:  make(map[string]interface{}),
 41	}
 42	r.registerDefaults()
 43	return r
 44}
 45
 46// Render processes a template with the given data
 47func (r *Renderer) Render(template string, data map[string]interface{}) string {
 48	if data != nil {
 49		r.data = data
 50	}
 51	r.ctx = nil
 52	return r.render(template)
 53}
 54
 55// render is the core rendering engine
 56func (r *Renderer) render(tmpl string) string {
 57	var out strings.Builder
 58	
 59	for len(tmpl) > 0 {
 60		// Find next placeholder
 61		start := strings.Index(tmpl, "{{")
 62		if start < 0 {
 63			out.WriteString(tmpl)
 64			break
 65		}
 66		
 67		// Write content before placeholder
 68		out.WriteString(tmpl[:start])
 69		
 70		// Parse placeholder
 71		placeholder, end := r.parsePlaceholder(tmpl[start:])
 72		if placeholder == nil {
 73			out.WriteString("{{")
 74			tmpl = tmpl[start+2:]
 75			continue
 76		}
 77		
 78		// Apply left trim
 79		if placeholder.trimLeft {
 80			s := out.String()
 81			out.Reset()
 82			out.WriteString(strings.TrimRightFunc(s, unicode.IsSpace))
 83		}
 84		
 85		// Process placeholder
 86		result, consumed := r.process(placeholder, tmpl[start+end:])
 87		out.WriteString(result)
 88		
 89		// Advance position
 90		tmpl = tmpl[start+end+consumed:]
 91		
 92		// Apply right trim
 93		if placeholder.trimRight {
 94			tmpl = strings.TrimLeftFunc(tmpl, unicode.IsSpace)
 95		}
 96	}
 97	
 98	return out.String()
 99}
100
101// placeholder represents a parsed {{...}} tag
102type placeholder struct {
103	expr      string
104	trimLeft  bool
105	trimRight bool
106}
107
108// parsePlaceholder extracts a {{...}} placeholder
109func (r *Renderer) parsePlaceholder(tmpl string) (*placeholder, int) {
110	if !strings.HasPrefix(tmpl, "{{") {
111		return nil, 0
112	}
113	
114	// Find matching }}
115	end := r.findClosing(tmpl, 2)
116	if end < 0 {
117		return nil, 0
118	}
119	
120	// Extract content
121	content := tmpl[2:end]
122	
123	// Check trim markers
124	p := &placeholder{}
125	if strings.HasPrefix(content, "-") {
126		p.trimLeft = true
127		content = content[1:]
128	}
129	if strings.HasSuffix(content, "-") {
130		p.trimRight = true
131		content = content[:len(content)-1]
132	}
133	
134	p.expr = strings.TrimSpace(content)
135	return p, end + 2
136}
137
138// findClosing finds the closing }} for a {{
139func (r *Renderer) findClosing(tmpl string, start int) int {
140	depth := 1
141	i := start
142	
143	for i < len(tmpl)-1 {
144		if tmpl[i] == '{' && tmpl[i+1] == '{' {
145			depth++
146			i += 2
147		} else if tmpl[i] == '}' && tmpl[i+1] == '}' {
148			depth--
149			if depth == 0 {
150				return i
151			}
152			i += 2
153		} else {
154			i++
155		}
156	}
157	
158	return -1
159}
160
161// process evaluates a placeholder and returns (result, extraBytesConsumed)
162func (r *Renderer) process(p *placeholder, remaining string) (string, int) {
163	parts := r.parseExpr(p.expr)
164	if len(parts) == 0 {
165		return "", 0
166	}
167	
168	cmd := parts[0]
169	args := parts[1:]
170	
171	// Handle variables
172	if strings.HasPrefix(cmd, ".") {
173		if val := r.resolve(cmd); val != nil {
174			return typeutil.ToString(val), 0
175		}
176		return "", 0
177	}
178	
179	// Handle commands
180	switch cmd {
181	case "range":
182		return r.doRange(args, remaining)
183	case "if":
184		return r.doIf(args, remaining)
185	case "end", "else":
186		return "", 0 // Handled by parent
187	default:
188		// Special handling for index function
189		if cmd == "index" && len(args) >= 2 {
190			return r.indexFunc(args...), 0
191		}
192		// Try function
193		if fn, ok := r.funcs[cmd]; ok {
194			resolved := r.resolveArgs(args)
195			return fn(resolved...), 0
196		}
197		return p.expr, 0
198	}
199}
200
201// doRange implements {{range .items}}...{{end}}
202func (r *Renderer) doRange(args []string, tmpl string) (string, int) {
203	if len(args) == 0 {
204		return "", 0
205	}
206	
207	// Get collection
208	val := r.resolve(args[0])
209	if val == nil {
210		return "", 0
211	}
212	
213	// Convert to items
214	items := toSlice(val)
215	if items == nil {
216		return "", 0
217	}
218	
219	// Find block
220	block, consumed := r.findBlock(tmpl, "end")
221	if consumed == 0 {
222		return "", 0
223	}
224	
225	// Execute loop
226	var out strings.Builder
227	oldCtx := r.ctx
228	
229	for i, item := range items {
230		// Create loop context
231		r.ctx = &context{
232			parent: oldCtx,
233			vars:   make(map[string]interface{}),
234			loop: &loopContext{
235				items: items,
236				index: i,
237			},
238		}
239		
240		// Add item properties if map
241		if m, ok := item.(map[string]interface{}); ok {
242			for k, v := range m {
243				r.ctx.vars[k] = v
244			}
245		}
246		
247		out.WriteString(r.render(block))
248	}
249	
250	r.ctx = oldCtx
251	return out.String(), consumed
252}
253
254// doIf implements {{if .cond}}...{{else}}...{{end}}
255func (r *Renderer) doIf(args []string, tmpl string) (string, int) {
256	if len(args) == 0 {
257		return "", 0
258	}
259	
260	// Evaluate condition
261	val := r.resolve(args[0])
262	cond := typeutil.ToBool(val)
263	
264	// Find block
265	block, consumed := r.findBlock(tmpl, "end")
266	if consumed == 0 {
267		return "", 0
268	}
269	
270	// Split on else
271	ifBlock, elseBlock := r.splitElse(block)
272	
273	// Execute branch
274	oldCtx := r.ctx
275	r.ctx = &context{
276		parent: oldCtx,
277		vars:   make(map[string]interface{}),
278	}
279	
280	var result string
281	if cond {
282		result = r.render(ifBlock)
283	} else {
284		result = r.render(elseBlock)
285	}
286	
287	r.ctx = oldCtx
288	return result, consumed
289}
290
291// findBlock finds content up to {{end}}
292func (r *Renderer) findBlock(tmpl string, endMarker string) (string, int) {
293	var out strings.Builder
294	depth := 1
295	i := 0
296	
297	for i < len(tmpl) {
298		// Find next {{
299		next := strings.Index(tmpl[i:], "{{")
300		if next < 0 {
301			break
302		}
303		
304		// Add content before {{
305		out.WriteString(tmpl[i : i+next])
306		
307		// Parse placeholder
308		p, consumed := r.parsePlaceholder(tmpl[i+next:])
309		if p == nil {
310			out.WriteString("{{")
311			i += next + 2
312			continue
313		}
314		
315		// Check command
316		parts := r.parseExpr(p.expr)
317		if len(parts) > 0 {
318			switch parts[0] {
319			case "range", "if":
320				depth++
321			case endMarker:
322				depth--
323				if depth == 0 {
324					return out.String(), i + next + consumed
325				}
326			}
327		}
328		
329		// Add to output
330		out.WriteString(tmpl[i+next : i+next+consumed])
331		i += next + consumed
332	}
333	
334	return "", 0
335}
336
337// splitElse splits block on {{else}}
338func (r *Renderer) splitElse(block string) (string, string) {
339	depth := 0
340	i := 0
341	
342	for i < len(block) {
343		next := strings.Index(block[i:], "{{")
344		if next < 0 {
345			break
346		}
347		
348		p, consumed := r.parsePlaceholder(block[i+next:])
349		if p == nil {
350			i += next + 2
351			continue
352		}
353		
354		parts := r.parseExpr(p.expr)
355		if len(parts) > 0 {
356			switch parts[0] {
357			case "if":
358				depth++
359			case "end":
360				depth--
361			case "else":
362				if depth == 0 {
363					return block[:i+next], block[i+next+consumed:]
364				}
365			}
366		}
367		
368		i += next + consumed
369	}
370	
371	return block, ""
372}
373
374// parseExpr splits expression respecting quotes and parentheses
375func (r *Renderer) parseExpr(expr string) []string {
376	var parts []string
377	var current strings.Builder
378	var inQuote bool
379	var parenDepth int
380	var prevChar rune
381	
382	for _, ch := range expr {
383		switch ch {
384		case '"':
385			if prevChar != '\\' {
386				inQuote = !inQuote
387			}
388			current.WriteRune(ch)
389		case '(':
390			if !inQuote {
391				parenDepth++
392			}
393			current.WriteRune(ch)
394		case ')':
395			if !inQuote {
396				parenDepth--
397			}
398			current.WriteRune(ch)
399		case ' ':
400			if !inQuote && parenDepth == 0 {
401				if current.Len() > 0 {
402					parts = append(parts, current.String())
403					current.Reset()
404				}
405			} else {
406				current.WriteRune(ch)
407			}
408		default:
409			current.WriteRune(ch)
410		}
411		prevChar = ch
412	}
413	
414	if current.Len() > 0 {
415		parts = append(parts, current.String())
416	}
417	
418	return parts
419}
420
421// resolve looks up a variable
422func (r *Renderer) resolve(name string) interface{} {
423	if !strings.HasPrefix(name, ".") {
424		return nil
425	}
426	name = strings.TrimPrefix(name, ".")
427	
428	// Check context stack
429	ctx := r.ctx
430	for ctx != nil {
431		// Handle . in range
432		if name == "" && ctx.loop != nil {
433			if ctx.loop.index < len(ctx.loop.items) {
434				return ctx.loop.items[ctx.loop.index]
435			}
436		}
437		
438		// Check local vars
439		if val, ok := ctx.vars[name]; ok {
440			return val
441		}
442		
443		ctx = ctx.parent
444	}
445	
446	// Check global data
447	return lookup(r.data, name)
448}
449
450// resolveArgs resolves all arguments
451func (r *Renderer) resolveArgs(args []string) []string {
452	result := make([]string, len(args))
453	
454	for i, arg := range args {
455		// Handle nested calls
456		if strings.HasPrefix(arg, "(") && strings.HasSuffix(arg, ")") {
457			inner := arg[1 : len(arg)-1]
458			parts := r.parseExpr(inner)
459			if len(parts) > 0 {
460				// Special handling for index
461				if parts[0] == "index" && len(parts) >= 3 {
462					result[i] = r.indexFunc(parts[1:]...)
463					continue
464				}
465				if fn, ok := r.funcs[parts[0]]; ok {
466					resolved := r.resolveArgs(parts[1:])
467					result[i] = fn(resolved...)
468					continue
469				}
470			}
471		}
472		
473		// Handle variables
474		if strings.HasPrefix(arg, ".") {
475			if val := r.resolve(arg); val != nil {
476				result[i] = typeutil.ToString(val)
477				continue
478			}
479		}
480		
481		// Handle nested templates
482		if strings.Contains(arg, "{{") {
483			// For quoted strings with nested templates, handle specially
484			if len(arg) >= 2 && arg[0] == '"' && arg[len(arg)-1] == '"' {
485				// Remove outer quotes, process template, then result is the value
486				inner := arg[1 : len(arg)-1]
487				result[i] = r.render(inner)
488			} else {
489				result[i] = r.render(arg)
490			}
491			continue
492		}
493		
494		// Plain string
495		result[i] = unquote(arg)
496	}
497	
498	return result
499}
500
501// registerDefaults registers built-in functions
502func (r *Renderer) registerDefaults() {
503	// Markdown helpers
504	r.funcs["H1"] = wrap1(md.H1)
505	r.funcs["H2"] = wrap1(md.H2)
506	r.funcs["H3"] = wrap1(md.H3)
507	r.funcs["H4"] = wrap1(md.H4)
508	r.funcs["H5"] = wrap1(md.H5)
509	r.funcs["H6"] = wrap1(md.H6)
510	r.funcs["Bold"] = wrap1(md.Bold)
511	r.funcs["Italic"] = wrap1(md.Italic)
512	r.funcs["Strikethrough"] = wrap1(md.Strikethrough)
513	r.funcs["InlineCode"] = wrap1(md.InlineCode)
514	r.funcs["BulletItem"] = wrap1(md.BulletItem)
515	r.funcs["CodeBlock"] = wrap1(md.CodeBlock)
516	r.funcs["Blockquote"] = wrap1(md.Blockquote)
517	r.funcs["Paragraph"] = wrap1(md.Paragraph)
518	r.funcs["EscapeText"] = wrap1(md.EscapeText)
519	
520	r.funcs["Link"] = wrap2(md.Link)
521	r.funcs["Image"] = wrap2(md.Image)
522	r.funcs["LanguageCodeBlock"] = wrap2(md.LanguageCodeBlock)
523	r.funcs["Footnote"] = wrap2(md.FootnoteDefinition)
524	r.funcs["CollapsibleSection"] = wrap2(md.CollapsibleSection)
525	
526	r.funcs["InlineImageWithLink"] = wrap3(md.InlineImageWithLink)
527	
528	r.funcs["TodoItem"] = func(args ...string) string {
529		if len(args) >= 2 {
530			return md.TodoItem(args[0], args[1] == "true")
531		}
532		return "[error: TodoItem requires 2 arguments]"
533	}
534	
535	r.funcs["HorizontalRule"] = func(args ...string) string {
536		return md.HorizontalRule()
537	}
538	
539	// Utilities
540	r.funcs["concat"] = func(args ...string) string {
541		return strings.Join(args, "")
542	}
543	
544	r.funcs["printf"] = func(args ...string) string {
545		if len(args) == 0 {
546			return ""
547		}
548		values := make([]interface{}, len(args)-1)
549		for i, v := range args[1:] {
550			values[i] = v
551		}
552		return ufmt.Sprintf(args[0], values...)
553	}
554	
555	r.funcs["trim"] = wrap1(strings.TrimSpace)
556	r.funcs["string"] = func(args ...string) string {
557		if len(args) > 0 {
558			return typeutil.ToString(args[0])
559		}
560		return ""
561	}
562	
563	r.funcs["index"] = r.indexFunc
564}
565
566// indexFunc provides array/map indexing
567func (r *Renderer) indexFunc(args ...string) string {
568	if len(args) < 2 {
569		return "[error: index requires 2 arguments]"
570	}
571	
572	// Handle the first argument - could be variable or literal
573	collection := strings.TrimSpace(args[0])
574	indexStr := strings.TrimSpace(args[1])
575	
576	// Remove quotes from index if present
577	indexStr = unquote(indexStr)
578	
579	// For unit tests - literal values
580	if !strings.HasPrefix(collection, ".") {
581		if idx, err := strconv.Atoi(indexStr); err == nil {
582			return ufmt.Sprintf("[%d]", idx)
583		}
584		return ufmt.Sprintf("[%s]", indexStr)
585	}
586	
587	// Resolve the collection variable
588	val := r.resolve(collection)
589	if val == nil {
590		return "[error: collection not found]"
591	}
592	
593	// Parse index
594	idx, err := strconv.Atoi(indexStr)
595	if err != nil {
596		return "[error: index must be a number]"
597	}
598	
599	// Do the indexing
600	switch v := val.(type) {
601	case []interface{}:
602		if idx >= 0 && idx < len(v) {
603			return typeutil.ToString(v[idx])
604		}
605	case []string:
606		if idx >= 0 && idx < len(v) {
607			return v[idx]
608		}
609	default:
610		if items := toSlice(val); items != nil {
611			if idx >= 0 && idx < len(items) {
612				return typeutil.ToString(items[idx])
613			}
614		}
615	}
616	
617	return "[error: index out of range]"
618}
619
620// Helper functions
621
622// lookup navigates nested maps
623func lookup(data map[string]interface{}, path string) interface{} {
624	parts := strings.Split(path, ".")
625	var current interface{} = data
626	
627	for _, part := range parts {
628		if part == "" {
629			continue
630		}
631		if m, ok := current.(map[string]interface{}); ok {
632			current = m[part]
633		} else {
634			return nil
635		}
636	}
637	
638	return current
639}
640
641// toSlice converts various types to []interface{}
642func toSlice(val interface{}) []interface{} {
643	switch v := val.(type) {
644	case []interface{}:
645		return v
646	case []map[string]interface{}:
647		result := make([]interface{}, len(v))
648		for i, m := range v {
649			result[i] = m
650		}
651		return result
652	default:
653		return typeutil.ToInterfaceSlice(val)
654	}
655}
656
657// unquote removes quotes from strings
658func unquote(s string) string {
659	// Don't trim space - preserve formatting
660	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
661		return s[1 : len(s)-1]
662	}
663	return s
664}
665
666// Function wrappers for cleaner registration
667
668func wrap1(fn func(string) string) Func {
669	return func(args ...string) string {
670		if len(args) > 0 {
671			return fn(args[0])
672		}
673		return "[error: missing argument]"
674	}
675}
676
677func wrap2(fn func(string, string) string) Func {
678	return func(args ...string) string {
679		if len(args) >= 2 {
680			return fn(args[0], args[1])
681		}
682		return "[error: requires 2 arguments]"
683	}
684}
685
686func wrap3(fn func(string, string, string) string) Func {
687	return func(args ...string) string {
688		if len(args) >= 3 {
689			return fn(args[0], args[1], args[2])
690		}
691		return "[error: requires 3 arguments]"
692	}
693}
694
695// Helper is kept for backward compatibility
696type Helper interface {
697	Execute(args []string, data map[string]interface{}) string
698}