PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/template/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / template
Files
3 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/template/v0
gno
0.9

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • template.gnogno
template.gnogno
1package template23import (4	"strconv"5	"strings"6	"unicode"78	"gno.land/p/moul/md/v0"9	"gno.land/p/moul/typeutil/v0"10	"gno.land/p/nt/ufmt/v0"11)1213// Renderer provides a simple template engine with a clean API14type Renderer struct {15	funcs map[string]Func  // Template functions16	data  map[string]interface{} // Global data17	ctx   *context       // Current evaluation context18}1920// Func is the signature for template functions21type Func func(args ...string) string2223// context tracks variable scope during evaluation24type context struct {25	parent *context26	vars   map[string]interface{} // Local variables27	loop   *loopContext          // Range loop state28}2930// loopContext holds state for range loops31type loopContext struct {32	items []interface{} // Items being iterated33	index int          // Current index34}3536// NewRenderer creates a new template renderer with default functions37func NewRenderer() *Renderer {38	r := &Renderer{39		funcs: make(map[string]Func),40		data:  make(map[string]interface{}),41	}42	r.registerDefaults()43	return r44}4546// Render processes a template with the given data47func (r *Renderer) Render(template string, data map[string]interface{}) string {48	if data != nil {49		r.data = data50	}51	r.ctx = nil52	return r.render(template)53}5455// render is the core rendering engine56func (r *Renderer) render(tmpl string) string {57	var out strings.Builder58	59	for len(tmpl) > 0 {60		// Find next placeholder61		start := strings.Index(tmpl, "{{")62		if start < 0 {63			out.WriteString(tmpl)64			break65		}66		67		// Write content before placeholder68		out.WriteString(tmpl[:start])69		70		// Parse placeholder71		placeholder, end := r.parsePlaceholder(tmpl[start:])72		if placeholder == nil {73			out.WriteString("{{")74			tmpl = tmpl[start+2:]75			continue76		}77		78		// Apply left trim79		if placeholder.trimLeft {80			s := out.String()81			out.Reset()82			out.WriteString(strings.TrimRightFunc(s, unicode.IsSpace))83		}84		85		// Process placeholder86		result, consumed := r.process(placeholder, tmpl[start+end:])87		out.WriteString(result)88		89		// Advance position90		tmpl = tmpl[start+end+consumed:]91		92		// Apply right trim93		if placeholder.trimRight {94			tmpl = strings.TrimLeftFunc(tmpl, unicode.IsSpace)95		}96	}97	98	return out.String()99}100101// placeholder represents a parsed {{...}} tag102type placeholder struct {103	expr      string104	trimLeft  bool105	trimRight bool106}107108// parsePlaceholder extracts a {{...}} placeholder109func (r *Renderer) parsePlaceholder(tmpl string) (*placeholder, int) {110	if !strings.HasPrefix(tmpl, "{{") {111		return nil, 0112	}113	114	// Find matching }}115	end := r.findClosing(tmpl, 2)116	if end < 0 {117		return nil, 0118	}119	120	// Extract content121	content := tmpl[2:end]122	123	// Check trim markers124	p := &placeholder{}125	if strings.HasPrefix(content, "-") {126		p.trimLeft = true127		content = content[1:]128	}129	if strings.HasSuffix(content, "-") {130		p.trimRight = true131		content = content[:len(content)-1]132	}133	134	p.expr = strings.TrimSpace(content)135	return p, end + 2136}137138// findClosing finds the closing }} for a {{139func (r *Renderer) findClosing(tmpl string, start int) int {140	depth := 1141	i := start142	143	for i < len(tmpl)-1 {144		if tmpl[i] == '{' && tmpl[i+1] == '{' {145			depth++146			i += 2147		} else if tmpl[i] == '}' && tmpl[i+1] == '}' {148			depth--149			if depth == 0 {150				return i151			}152			i += 2153		} else {154			i++155		}156	}157	158	return -1159}160161// 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 "", 0166	}167	168	cmd := parts[0]169	args := parts[1:]170	171	// Handle variables172	if strings.HasPrefix(cmd, ".") {173		if val := r.resolve(cmd); val != nil {174			return typeutil.ToString(val), 0175		}176		return "", 0177	}178	179	// Handle commands180	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 parent187	default:188		// Special handling for index function189		if cmd == "index" && len(args) >= 2 {190			return r.indexFunc(args...), 0191		}192		// Try function193		if fn, ok := r.funcs[cmd]; ok {194			resolved := r.resolveArgs(args)195			return fn(resolved...), 0196		}197		return p.expr, 0198	}199}200201// doRange implements {{range .items}}...{{end}}202func (r *Renderer) doRange(args []string, tmpl string) (string, int) {203	if len(args) == 0 {204		return "", 0205	}206	207	// Get collection208	val := r.resolve(args[0])209	if val == nil {210		return "", 0211	}212	213	// Convert to items214	items := toSlice(val)215	if items == nil {216		return "", 0217	}218	219	// Find block220	block, consumed := r.findBlock(tmpl, "end")221	if consumed == 0 {222		return "", 0223	}224	225	// Execute loop226	var out strings.Builder227	oldCtx := r.ctx228	229	for i, item := range items {230		// Create loop context231		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 map241		if m, ok := item.(map[string]interface{}); ok {242			for k, v := range m {243				r.ctx.vars[k] = v244			}245		}246		247		out.WriteString(r.render(block))248	}249	250	r.ctx = oldCtx251	return out.String(), consumed252}253254// doIf implements {{if .cond}}...{{else}}...{{end}}255func (r *Renderer) doIf(args []string, tmpl string) (string, int) {256	if len(args) == 0 {257		return "", 0258	}259	260	// Evaluate condition261	val := r.resolve(args[0])262	cond := typeutil.ToBool(val)263	264	// Find block265	block, consumed := r.findBlock(tmpl, "end")266	if consumed == 0 {267		return "", 0268	}269	270	// Split on else271	ifBlock, elseBlock := r.splitElse(block)272	273	// Execute branch274	oldCtx := r.ctx275	r.ctx = &context{276		parent: oldCtx,277		vars:   make(map[string]interface{}),278	}279	280	var result string281	if cond {282		result = r.render(ifBlock)283	} else {284		result = r.render(elseBlock)285	}286	287	r.ctx = oldCtx288	return result, consumed289}290291// findBlock finds content up to {{end}}292func (r *Renderer) findBlock(tmpl string, endMarker string) (string, int) {293	var out strings.Builder294	depth := 1295	i := 0296	297	for i < len(tmpl) {298		// Find next {{299		next := strings.Index(tmpl[i:], "{{")300		if next < 0 {301			break302		}303		304		// Add content before {{305		out.WriteString(tmpl[i : i+next])306		307		// Parse placeholder308		p, consumed := r.parsePlaceholder(tmpl[i+next:])309		if p == nil {310			out.WriteString("{{")311			i += next + 2312			continue313		}314		315		// Check command316		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 + consumed325				}326			}327		}328		329		// Add to output330		out.WriteString(tmpl[i+next : i+next+consumed])331		i += next + consumed332	}333	334	return "", 0335}336337// splitElse splits block on {{else}}338func (r *Renderer) splitElse(block string) (string, string) {339	depth := 0340	i := 0341	342	for i < len(block) {343		next := strings.Index(block[i:], "{{")344		if next < 0 {345			break346		}347		348		p, consumed := r.parsePlaceholder(block[i+next:])349		if p == nil {350			i += next + 2351			continue352		}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 + consumed369	}370	371	return block, ""372}373374// parseExpr splits expression respecting quotes and parentheses375func (r *Renderer) parseExpr(expr string) []string {376	var parts []string377	var current strings.Builder378	var inQuote bool379	var parenDepth int380	var prevChar rune381	382	for _, ch := range expr {383		switch ch {384		case '"':385			if prevChar != '\\' {386				inQuote = !inQuote387			}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 = ch412	}413	414	if current.Len() > 0 {415		parts = append(parts, current.String())416	}417	418	return parts419}420421// resolve looks up a variable422func (r *Renderer) resolve(name string) interface{} {423	if !strings.HasPrefix(name, ".") {424		return nil425	}426	name = strings.TrimPrefix(name, ".")427	428	// Check context stack429	ctx := r.ctx430	for ctx != nil {431		// Handle . in range432		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 vars439		if val, ok := ctx.vars[name]; ok {440			return val441		}442		443		ctx = ctx.parent444	}445	446	// Check global data447	return lookup(r.data, name)448}449450// resolveArgs resolves all arguments451func (r *Renderer) resolveArgs(args []string) []string {452	result := make([]string, len(args))453	454	for i, arg := range args {455		// Handle nested calls456		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 index461				if parts[0] == "index" && len(parts) >= 3 {462					result[i] = r.indexFunc(parts[1:]...)463					continue464				}465				if fn, ok := r.funcs[parts[0]]; ok {466					resolved := r.resolveArgs(parts[1:])467					result[i] = fn(resolved...)468					continue469				}470			}471		}472		473		// Handle variables474		if strings.HasPrefix(arg, ".") {475			if val := r.resolve(arg); val != nil {476				result[i] = typeutil.ToString(val)477				continue478			}479		}480		481		// Handle nested templates482		if strings.Contains(arg, "{{") {483			// For quoted strings with nested templates, handle specially484			if len(arg) >= 2 && arg[0] == '"' && arg[len(arg)-1] == '"' {485				// Remove outer quotes, process template, then result is the value486				inner := arg[1 : len(arg)-1]487				result[i] = r.render(inner)488			} else {489				result[i] = r.render(arg)490			}491			continue492		}493		494		// Plain string495		result[i] = unquote(arg)496	}497	498	return result499}500501// registerDefaults registers built-in functions502func (r *Renderer) registerDefaults() {503	// Markdown helpers504	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	// Utilities540	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] = v551		}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.indexFunc564}565566// indexFunc provides array/map indexing567func (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 literal573	collection := strings.TrimSpace(args[0])574	indexStr := strings.TrimSpace(args[1])575	576	// Remove quotes from index if present577	indexStr = unquote(indexStr)578	579	// For unit tests - literal values580	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 variable588	val := r.resolve(collection)589	if val == nil {590		return "[error: collection not found]"591	}592	593	// Parse index594	idx, err := strconv.Atoi(indexStr)595	if err != nil {596		return "[error: index must be a number]"597	}598	599	// Do the indexing600	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}619620// Helper functions621622// lookup navigates nested maps623func lookup(data map[string]interface{}, path string) interface{} {624	parts := strings.Split(path, ".")625	var current interface{} = data626	627	for _, part := range parts {628		if part == "" {629			continue630		}631		if m, ok := current.(map[string]interface{}); ok {632			current = m[part]633		} else {634			return nil635		}636	}637	638	return current639}640641// toSlice converts various types to []interface{}642func toSlice(val interface{}) []interface{} {643	switch v := val.(type) {644	case []interface{}:645		return v646	case []map[string]interface{}:647		result := make([]interface{}, len(v))648		for i, m := range v {649			result[i] = m650		}651		return result652	default:653		return typeutil.ToInterfaceSlice(val)654	}655}656657// unquote removes quotes from strings658func unquote(s string) string {659	// Don't trim space - preserve formatting660	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {661		return s[1 : len(s)-1]662	}663	return s664}665666// Function wrappers for cleaner registration667668func 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}676677func 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}685686func 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}694695// Helper is kept for backward compatibility696type Helper interface {697	Execute(args []string, data map[string]interface{}) string698}

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.