diff --git a/examples/gno.land/p/moul/dynreplacer/dynreplacer.gno b/examples/gno.land/p/moul/dynreplacer/dynreplacer.gno new file mode 100644 index 00000000000..ea9687b7570 --- /dev/null +++ b/examples/gno.land/p/moul/dynreplacer/dynreplacer.gno @@ -0,0 +1,111 @@ +// Package dynreplacer provides a simple template engine for handling dynamic +// content replacement. It is similar to strings.Replacer but with lazy +// execution of replacements, making it more optimization-friendly in several +// cases. While strings.Replacer requires all replacement values to be computed +// upfront, dynreplacer only executes the callback functions for placeholders +// that actually exist in the template, avoiding unnecessary computations. +// +// The package ensures efficient, non-recursive replacement of placeholders in a +// single pass. This lazy evaluation approach is particularly beneficial when: +// - Some replacement values are expensive to compute +// - Not all placeholders are guaranteed to be present in the template +// - Templates are reused with different content +// +// Example usage: +// +// r := dynreplacer.New( +// dynreplacer.Pair{":name:", func() string { return "World" }}, +// dynreplacer.Pair{":greeting:", func() string { return "Hello" }}, +// ) +// result := r.Replace("Hello :name:!") // Returns "Hello World!" +// +// The replacer caches computed values, so subsequent calls with the same +// placeholder will reuse the cached value instead of executing the callback +// again: +// +// r := dynreplacer.New() +// r.RegisterCallback(":expensive:", func() string { return "computed" }) +// r.Replace("Value1: :expensive:") // Computes the value +// r.Replace("Value2: :expensive:") // Uses cached value +// r.ClearCache() // Force re-computation on next use +package dynreplacer + +import ( + "strings" +) + +// Replacer manages dynamic placeholders, their associated functions, and cached +// values. +type Replacer struct { + callbacks map[string]func() string + cachedValues map[string]string +} + +// Pair represents a placeholder and its callback function +type Pair struct { + Placeholder string + Callback func() string +} + +// New creates a new Replacer instance with optional initial replacements. +// It accepts pairs where each pair consists of a placeholder string and +// its corresponding callback function. +// +// Example: +// +// New( +// Pair{":name:", func() string { return "World" }}, +// Pair{":greeting:", func() string { return "Hello" }}, +// ) +func New(pairs ...Pair) *Replacer { + r := &Replacer{ + callbacks: make(map[string]func() string), + cachedValues: make(map[string]string), + } + + for _, pair := range pairs { + r.RegisterCallback(pair.Placeholder, pair.Callback) + } + + return r +} + +// RegisterCallback associates a placeholder with a function to generate its +// content. +func (r *Replacer) RegisterCallback(placeholder string, callback func() string) { + r.callbacks[placeholder] = callback +} + +// Replace processes the given layout, replacing placeholders with cached or +// newly computed values. +func (r *Replacer) Replace(layout string) string { + replacements := []string{} + + // Check for placeholders and compute/retrieve values + hasReplacements := false + for placeholder, callback := range r.callbacks { + if strings.Contains(layout, placeholder) { + value, exists := r.cachedValues[placeholder] + if !exists { + value = callback() + r.cachedValues[placeholder] = value + } + replacements = append(replacements, placeholder, value) + hasReplacements = true + } + } + + // If no replacements were found, return the original layout + if !hasReplacements { + return layout + } + + // Create a strings.Replacer with all computed replacements + replacer := strings.NewReplacer(replacements...) + return replacer.Replace(layout) +} + +// ClearCache clears all cached values, forcing re-computation on next Replace. +func (r *Replacer) ClearCache() { + r.cachedValues = make(map[string]string) +} diff --git a/examples/gno.land/p/moul/dynreplacer/dynreplacer_test.gno b/examples/gno.land/p/moul/dynreplacer/dynreplacer_test.gno new file mode 100644 index 00000000000..51235a28950 --- /dev/null +++ b/examples/gno.land/p/moul/dynreplacer/dynreplacer_test.gno @@ -0,0 +1,235 @@ +package dynreplacer + +import ( + "strings" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + pairs []Pair + }{ + { + name: "empty constructor", + pairs: []Pair{}, + }, + { + name: "single pair", + pairs: []Pair{ + {":name:", func() string { return "World" }}, + }, + }, + { + name: "multiple pairs", + pairs: []Pair{ + {":greeting:", func() string { return "Hello" }}, + {":name:", func() string { return "World" }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New(tt.pairs...) + uassert.True(t, r.callbacks != nil, "callbacks map should be initialized") + uassert.True(t, r.cachedValues != nil, "cachedValues map should be initialized") + + // Verify all callbacks were registered + for _, pair := range tt.pairs { + _, exists := r.callbacks[pair.Placeholder] + uassert.True(t, exists, "callback should be registered for "+pair.Placeholder) + } + }) + } +} + +func TestReplace(t *testing.T) { + tests := []struct { + name string + layout string + setup func(*Replacer) + expected string + }{ + { + name: "empty layout", + layout: "", + setup: func(r *Replacer) {}, + expected: "", + }, + { + name: "single replacement", + layout: "Hello :name:!", + setup: func(r *Replacer) { + r.RegisterCallback(":name:", func() string { return "World" }) + }, + expected: "Hello World!", + }, + { + name: "multiple replacements", + layout: ":greeting: :name:!", + setup: func(r *Replacer) { + r.RegisterCallback(":greeting:", func() string { return "Hello" }) + r.RegisterCallback(":name:", func() string { return "World" }) + }, + expected: "Hello World!", + }, + { + name: "no recursive replacement", + layout: ":outer:", + setup: func(r *Replacer) { + r.RegisterCallback(":outer:", func() string { return ":inner:" }) + r.RegisterCallback(":inner:", func() string { return "content" }) + }, + expected: ":inner:", + }, + { + name: "unused callbacks", + layout: "Hello :name:!", + setup: func(r *Replacer) { + r.RegisterCallback(":name:", func() string { return "World" }) + r.RegisterCallback(":unused:", func() string { return "Never Called" }) + }, + expected: "Hello World!", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New() + tt.setup(r) + result := r.Replace(tt.layout) + uassert.Equal(t, tt.expected, result) + }) + } +} + +func TestCaching(t *testing.T) { + r := New() + callCount := 0 + r.RegisterCallback(":expensive:", func() string { + callCount++ + return "computed" + }) + + layout := "Value: :expensive:" + + // First call should compute + result1 := r.Replace(layout) + uassert.Equal(t, "Value: computed", result1) + uassert.Equal(t, 1, callCount) + + // Second call should use cache + result2 := r.Replace(layout) + uassert.Equal(t, "Value: computed", result2) + uassert.Equal(t, 1, callCount) + + // After clearing cache, should recompute + r.ClearCache() + result3 := r.Replace(layout) + uassert.Equal(t, "Value: computed", result3) + uassert.Equal(t, 2, callCount) +} + +func TestComplexExample(t *testing.T) { + layout := ` + # Welcome to gno.land + + ## Blog + :latest-blogposts: + + ## Events + :next-events: + + ## Awesome Gno + :awesome-gno: + ` + + r := New( + Pair{":latest-blogposts:", func() string { return "Latest blog posts content here" }}, + Pair{":next-events:", func() string { return "Upcoming events listed here" }}, + Pair{":awesome-gno:", func() string { return ":latest-blogposts: (This should NOT be replaced again)" }}, + ) + + result := r.Replace(layout) + + // Check that original placeholders are replaced + uassert.True(t, !strings.Contains(result, ":latest-blogposts:\n"), "':latest-blogposts:' placeholder should be replaced") + uassert.True(t, !strings.Contains(result, ":next-events:\n"), "':next-events:' placeholder should be replaced") + uassert.True(t, !strings.Contains(result, ":awesome-gno:\n"), "':awesome-gno:' placeholder should be replaced") + + // Check that the replacement content is present + uassert.True(t, strings.Contains(result, "Latest blog posts content here"), "Blog posts content should be present") + uassert.True(t, strings.Contains(result, "Upcoming events listed here"), "Events content should be present") + uassert.True(t, strings.Contains(result, ":latest-blogposts: (This should NOT be replaced again)"), + "Nested placeholder should not be replaced") +} + +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + layout string + setup func(*Replacer) + expected string + }{ + { + name: "empty string placeholder", + layout: "Hello :", + setup: func(r *Replacer) { + r.RegisterCallback("", func() string { return "World" }) + }, + expected: "WorldHWorldeWorldlWorldlWorldoWorld World:World", + }, + { + name: "overlapping placeholders", + layout: "Hello :name::greeting:", + setup: func(r *Replacer) { + r.RegisterCallback(":name:", func() string { return "World" }) + r.RegisterCallback(":greeting:", func() string { return "Hi" }) + r.RegisterCallback(":name::greeting:", func() string { return "Should not match" }) + }, + expected: "Hello WorldHi", + }, + { + name: "replacement order", + layout: ":a::b::c:", + setup: func(r *Replacer) { + r.RegisterCallback(":c:", func() string { return "3" }) + r.RegisterCallback(":b:", func() string { return "2" }) + r.RegisterCallback(":a:", func() string { return "1" }) + }, + expected: "123", + }, + { + name: "special characters in placeholders", + layout: "Hello :$name#123:!", + setup: func(r *Replacer) { + r.RegisterCallback(":$name#123:", func() string { return "World" }) + }, + expected: "Hello World!", + }, + { + name: "multiple occurrences of same placeholder", + layout: ":name: and :name: again", + setup: func(r *Replacer) { + callCount := 0 + r.RegisterCallback(":name:", func() string { + callCount++ + return "World" + }) + }, + expected: "World and World again", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New() + tt.setup(r) + result := r.Replace(tt.layout) + uassert.Equal(t, tt.expected, result) + }) + } +} diff --git a/examples/gno.land/p/moul/dynreplacer/gno.mod b/examples/gno.land/p/moul/dynreplacer/gno.mod new file mode 100644 index 00000000000..9e196721f24 --- /dev/null +++ b/examples/gno.land/p/moul/dynreplacer/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/dynreplacer