* Refactor caching system to use pluggable stores The commit modernizes the caching implementation by introducing a pluggable store interface that allows different cache backends. Key changes: - Add Store interface for custom cache implementations - Create default TTL-based store for backwards compatibility - Add example LRU store for memory-bounded caching - Support cache store configuration via options pattern - Make cache cleanup logic implementation-specific - Add comprehensive tests and documentation The main goals were to: 1. Prevent unbounded memory growth through pluggable stores 2. Enable distributed caching support 3. Maintain backwards compatibility 4. Improve testability and maintainability Signed-off-by: franchb <hello@franchb.com> * Add custom cache stores docs and navigation Signed-off-by: franchb <hello@franchb.com> * Use GetOrCompute for atomic cache access The commit introduces an atomic GetOrCompute method to the cache interface and refactors all cache implementations to use it. This prevents race conditions and duplicate computations when multiple goroutines request the same uncached key simultaneously. The changes eliminate a time-of-check to time-of-use race condition in the original caching implementation, where separate Get/Set operations could lead to duplicate renders under high concurrency. With GetOrCompute, the entire check-compute-store operation happens atomically while holding the lock, ensuring only one goroutine computes a value for any given key. The API change is backwards compatible as the framework handles the GetOrCompute logic internally. Existing applications will automatically benefit from the * rename to WithCacheStore --------- Signed-off-by: franchb <hello@franchb.com> Co-authored-by: maddalax <jm@madev.me>
810 lines
19 KiB
Go
810 lines
19 KiB
Go
package h
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestRendererShouldRenderDocType(t *testing.T) {
|
|
t.Parallel()
|
|
result := Render(Html(
|
|
Div(),
|
|
), WithDocType())
|
|
assert.Equal(t, `<!DOCTYPE html><html><div></div></html>`, result)
|
|
}
|
|
|
|
func TestSimpleRender(t *testing.T) {
|
|
t.Parallel()
|
|
result := Render(
|
|
Div(Attribute("id", "my-div"), Attribute("class", "my-class")),
|
|
)
|
|
assert.Equal(t, `<div id="my-div" class="my-class"></div>`, result)
|
|
}
|
|
|
|
func TestRender(t *testing.T) {
|
|
t.Parallel()
|
|
div := Div(
|
|
Id("my-div"),
|
|
Attribute("data-attr-2", "value"),
|
|
Attributes(&AttributeMap{
|
|
"data-attr-3": "value",
|
|
}),
|
|
HxBeforeRequest(
|
|
SetText("before request"),
|
|
),
|
|
HxAfterRequest(
|
|
SetText("after request"),
|
|
),
|
|
Children(
|
|
Div(Text("hello, world")),
|
|
),
|
|
Text("hello, child"),
|
|
)
|
|
|
|
div.attributes.Set("data-attr-1", "value")
|
|
|
|
expected := `<div data-attr-1="value" id="my-div" data-attr-2="value" data-attr-3="value" hx-on::before-request="(self || this).innerText = 'before request';" hx-on::after-request="(self || this).innerText = 'after request';"><div>hello, world</div>hello, child</div>`
|
|
result := Render(div)
|
|
|
|
assert.Equal(t,
|
|
expected,
|
|
strings.ReplaceAll(result, "var self=this;var e=event;", ""))
|
|
}
|
|
|
|
func TestRenderAttributes_1(t *testing.T) {
|
|
t.Parallel()
|
|
div := Div(
|
|
AttributePairs("class", "bg-red-500"),
|
|
Attributes(&AttributeMap{
|
|
"id": Id("my-div"),
|
|
}),
|
|
Attribute("disabled", "true"),
|
|
)
|
|
assert.Equal(t,
|
|
`<div class="bg-red-500" id="my-div" disabled="true"></div>`,
|
|
Render(div),
|
|
)
|
|
}
|
|
|
|
func TestRenderAttributes_2(t *testing.T) {
|
|
div := Div(
|
|
AttributePairs("class", "bg-red-500", "id", "my-div"),
|
|
Button(
|
|
AttributePairs("class", "bg-blue-500", "id", "my-button"),
|
|
Text("Click me"),
|
|
Attribute("disabled", "true"),
|
|
Attribute("data-attr", "value"),
|
|
),
|
|
)
|
|
|
|
assert.Equal(t,
|
|
`<div class="bg-red-500" id="my-div"><button class="bg-blue-500" id="my-button" disabled="true" data-attr="value">Click me</button></div>`,
|
|
Render(div))
|
|
}
|
|
|
|
func TestRenderEmptyDiv(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t,
|
|
`<div></div>`,
|
|
Render(Div()),
|
|
)
|
|
}
|
|
|
|
func TestRenderVoidElement(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t,
|
|
`<input type="text"/>`,
|
|
Render(Input("text")),
|
|
)
|
|
assert.Equal(t, `<input/>`, Render(Tag("input")))
|
|
}
|
|
|
|
func TestRawContent(t *testing.T) {
|
|
t.Parallel()
|
|
str := "<div>hello, world</div>"
|
|
raw := UnsafeRaw(str)
|
|
assert.Equal(t, str, Render(raw))
|
|
}
|
|
|
|
func TestConditional(t *testing.T) {
|
|
t.Parallel()
|
|
result := Render(
|
|
Div(
|
|
Ternary(true, Text("true"), Text("false")),
|
|
),
|
|
)
|
|
assert.Equal(t, "<div>true</div>", result)
|
|
|
|
result = Render(
|
|
Div(
|
|
If(false, Text("true")),
|
|
),
|
|
)
|
|
assert.Equal(t, "<div></div>", result)
|
|
}
|
|
|
|
func TestTagSelfClosing(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, `<input type="text"/>`, Render(
|
|
Input("text"),
|
|
))
|
|
// assert the tag cannot have children
|
|
assert.Equal(t, `<input type="text"/>`, Render(
|
|
Input("text", Div()),
|
|
))
|
|
|
|
assert.Equal(t, `<div id="test"></div>`, Render(
|
|
Div(Id("test")),
|
|
))
|
|
assert.Equal(t, `<div id="test"><div></div></div>`, Render(
|
|
Div(Id("test"), Div()),
|
|
))
|
|
}
|
|
|
|
func TestCached(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := Cached(time.Hour, func() *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page())
|
|
secondRender := Render(page())
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, firstRender, Render(ComplexPage()))
|
|
}
|
|
|
|
func TestCachedT(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := CachedT(time.Hour, func(a string) *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page("a"))
|
|
secondRender := Render(page("a"))
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, firstRender, Render(ComplexPage()))
|
|
}
|
|
|
|
func TestCachedT2(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := CachedT2(time.Hour, func(a string, b string) *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page("a", "b"))
|
|
secondRender := Render(page("a", "b"))
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, firstRender, Render(ComplexPage()))
|
|
}
|
|
|
|
func TestCachedT3(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := CachedT3(time.Hour, func(a string, b string, c string) *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page("a", "b", "c"))
|
|
secondRender := Render(page("a", "b", "c"))
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, firstRender, Render(ComplexPage()))
|
|
}
|
|
|
|
func TestCachedT4(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := CachedT4(time.Hour, func(a string, b string, c string, d string) *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page("a", "b", "c", "d"))
|
|
secondRender := Render(page("a", "b", "c", "d"))
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, firstRender, Render(ComplexPage()))
|
|
}
|
|
|
|
func TestCachedExpired(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
page := Cached(time.Millisecond*3, func() *Element {
|
|
count++
|
|
return ComplexPage()
|
|
})
|
|
|
|
firstRender := Render(page())
|
|
time.Sleep(time.Millisecond * 5)
|
|
secondRender := Render(page())
|
|
|
|
assert.Equal(t, firstRender, secondRender)
|
|
assert.Equal(t, 2, count)
|
|
}
|
|
|
|
func TestCacheMultiple(t *testing.T) {
|
|
t.Parallel()
|
|
count := 0
|
|
cachedItem := Cached(time.Hour, func() *Element {
|
|
count++
|
|
return Div(Text("hello"))
|
|
})
|
|
|
|
wg := sync.WaitGroup{}
|
|
for i := 0; i < 2; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
Render(Div(
|
|
cachedItem(),
|
|
cachedItem(),
|
|
cachedItem(),
|
|
))
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
assert.Equal(t, 1, count)
|
|
}
|
|
|
|
func TestCacheByKey(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
callCount := 0
|
|
cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) {
|
|
key := "key"
|
|
if callCount == 3 {
|
|
key = "key2"
|
|
}
|
|
if callCount == 4 {
|
|
key = "key"
|
|
}
|
|
callCount++
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
Render(Div(
|
|
cachedItem(),
|
|
cachedItem(),
|
|
cachedItem(),
|
|
cachedItem(),
|
|
cachedItem(),
|
|
))
|
|
|
|
assert.Equal(t, 5, callCount)
|
|
assert.Equal(t, 2, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
Render(Div(
|
|
cachedItem("one"),
|
|
cachedItem("one"),
|
|
cachedItem("two"),
|
|
cachedItem("two"),
|
|
cachedItem("three"),
|
|
))
|
|
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT4(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT4(time.Hour, func(arg1 string, arg2 string, arg3 string, arg4 string) (any, GetElementFunc) {
|
|
return arg1, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
Render(Div(
|
|
cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
|
cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
|
cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
|
cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
|
cachedItem("three", uuid.NewString(), uuid.NewString(), uuid.NewString()),
|
|
))
|
|
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT3(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT3(time.Hour, func(arg1 string, arg2 string, arg3 string) (any, GetElementFunc) {
|
|
return arg1, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
Render(Div(
|
|
cachedItem("one", uuid.NewString(), uuid.NewString()),
|
|
cachedItem("one", uuid.NewString(), uuid.NewString()),
|
|
cachedItem("two", uuid.NewString(), uuid.NewString()),
|
|
cachedItem("two", uuid.NewString(), uuid.NewString()),
|
|
cachedItem("three", uuid.NewString(), uuid.NewString()),
|
|
))
|
|
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT2(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT2(time.Hour, func(arg1 string, arg2 string) (any, GetElementFunc) {
|
|
return arg1, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
Render(Div(
|
|
cachedItem("one", uuid.NewString()),
|
|
cachedItem("one", uuid.NewString()),
|
|
cachedItem("two", uuid.NewString()),
|
|
cachedItem("two", uuid.NewString()),
|
|
cachedItem("three", uuid.NewString()),
|
|
))
|
|
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyConcurrent(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
callCount := 0
|
|
cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) {
|
|
key := "key"
|
|
if callCount == 3 {
|
|
key = "key2"
|
|
}
|
|
if callCount == 4 {
|
|
key = "key"
|
|
}
|
|
callCount++
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
wg := sync.WaitGroup{}
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
Render(Div(
|
|
cachedItem(),
|
|
))
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
assert.Equal(t, 5, callCount)
|
|
assert.Equal(t, 2, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT1_2(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Pf(key)
|
|
}
|
|
})
|
|
|
|
assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, 2, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT1Expired(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT(time.Millisecond, func(key string) (any, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Pf(key)
|
|
}
|
|
})
|
|
|
|
assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
time.Sleep(time.Millisecond * 2)
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestCacheByKeyT1Expired_2(t *testing.T) {
|
|
t.Parallel()
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT(time.Millisecond*5, func(key string) (any, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Pf(key)
|
|
}
|
|
})
|
|
|
|
assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
|
|
time.Sleep(time.Millisecond * 3)
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
time.Sleep(time.Millisecond * 3)
|
|
assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
|
|
assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
|
|
assert.Equal(t, 3, renderCount)
|
|
}
|
|
|
|
func TestClearExpiredCached(t *testing.T) {
|
|
renderCount := 0
|
|
cachedItem := Cached(time.Millisecond*2, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
})
|
|
|
|
// First render
|
|
Render(cachedItem())
|
|
assert.Equal(t, 1, renderCount)
|
|
|
|
// Should use cache immediately
|
|
Render(cachedItem())
|
|
assert.Equal(t, 1, renderCount)
|
|
|
|
// Wait for expiration
|
|
time.Sleep(time.Millisecond * 3)
|
|
|
|
// Should re-render after expiration
|
|
Render(cachedItem())
|
|
assert.Equal(t, 2, renderCount)
|
|
}
|
|
|
|
func TestClearExpiredCacheByKey(t *testing.T) {
|
|
renderCount := 0
|
|
// Create two cached functions with different TTLs
|
|
shortLivedCache := CachedPerKeyT(time.Millisecond*1, func(key int) (int, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("short-lived"))
|
|
}
|
|
})
|
|
|
|
longLivedCache := CachedPerKeyT(time.Hour, func(key int) (int, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("long-lived"))
|
|
}
|
|
})
|
|
|
|
// Render 100 short-lived items
|
|
for i := 0; i < 100; i++ {
|
|
Render(shortLivedCache(i))
|
|
}
|
|
assert.Equal(t, 100, renderCount)
|
|
|
|
// Render a long-lived item
|
|
Render(longLivedCache(999))
|
|
assert.Equal(t, 101, renderCount)
|
|
|
|
// Wait for expiration of the short-lived items
|
|
time.Sleep(time.Millisecond * 3)
|
|
|
|
// Re-render some expired items - should trigger new renders
|
|
for i := 0; i < 10; i++ {
|
|
Render(shortLivedCache(i))
|
|
}
|
|
assert.Equal(t, 111, renderCount) // 101 + 10 re-renders
|
|
|
|
// The long-lived item should still be cached
|
|
Render(longLivedCache(999))
|
|
assert.Equal(t, 111, renderCount) // No additional render
|
|
|
|
// Clear cache manually on both
|
|
shortNode := shortLivedCache(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode)
|
|
shortNode.ClearCache()
|
|
|
|
longNode := longLivedCache(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode)
|
|
longNode.ClearCache()
|
|
|
|
// Everything should re-render now
|
|
Render(shortLivedCache(0))
|
|
assert.Equal(t, 112, renderCount)
|
|
|
|
Render(longLivedCache(999))
|
|
assert.Equal(t, 113, renderCount)
|
|
}
|
|
|
|
func TestBackgroundCleaner(t *testing.T) {
|
|
renderCount := 0
|
|
cachedItem := CachedPerKeyT(time.Millisecond*100, func(key int) (int, GetElementFunc) {
|
|
return key, func() *Element {
|
|
renderCount++
|
|
return Div(Text("hello"))
|
|
}
|
|
})
|
|
|
|
// Render 100 items
|
|
for i := 0; i < 100; i++ {
|
|
Render(cachedItem(i))
|
|
}
|
|
assert.Equal(t, 100, renderCount)
|
|
|
|
// Items should be cached immediately
|
|
for i := 0; i < 10; i++ {
|
|
Render(cachedItem(i))
|
|
}
|
|
assert.Equal(t, 100, renderCount) // No additional renders
|
|
|
|
// Wait for expiration and cleanup
|
|
time.Sleep(time.Second * 3)
|
|
|
|
// Items should be expired and need re-rendering
|
|
for i := 0; i < 10; i++ {
|
|
Render(cachedItem(i))
|
|
}
|
|
assert.Equal(t, 110, renderCount) // 10 re-renders after expiration
|
|
}
|
|
|
|
func TestEscapeHtml(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, "<script>alert(1)</script>", Render(Text("<script>alert(1)</script>")))
|
|
assert.Equal(t, "<p><script>alert(1)</script></p>", Render(Pf("<script>alert(1)</script>")))
|
|
|
|
}
|
|
|
|
func BenchmarkCacheByKey(b *testing.B) {
|
|
b.ReportAllocs()
|
|
page := CachedPerKeyT(time.Second*3, func(userId string) (any, GetElementFunc) {
|
|
return userId, func() *Element {
|
|
return MailTo(userId)
|
|
}
|
|
})
|
|
|
|
for i := 0; i < 5000; i++ {
|
|
userId := uuid.NewString()
|
|
Render(page(userId))
|
|
}
|
|
|
|
Render(page(uuid.NewString()))
|
|
}
|
|
|
|
func BenchmarkMailToStatic(b *testing.B) {
|
|
b.ReportAllocs()
|
|
ctx := RenderContext{
|
|
builder: &strings.Builder{},
|
|
}
|
|
page := MailTo("myemail")
|
|
for i := 0; i < b.N; i++ {
|
|
page.Render(&ctx)
|
|
ctx.builder.Reset()
|
|
}
|
|
}
|
|
|
|
func BenchmarkMailToDynamic(b *testing.B) {
|
|
b.ReportAllocs()
|
|
ctx := RenderContext{
|
|
builder: &strings.Builder{},
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
MailTo(uuid.NewString()).Render(&ctx)
|
|
ctx.builder.Reset()
|
|
}
|
|
}
|
|
|
|
func BenchmarkCachedComplexPage(b *testing.B) {
|
|
b.ReportAllocs()
|
|
ctx := RenderContext{
|
|
builder: &strings.Builder{},
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
CachedComplexPage().Render(&ctx)
|
|
ctx.builder.Reset()
|
|
}
|
|
}
|
|
|
|
func BenchmarkComplexPage(b *testing.B) {
|
|
b.ReportAllocs()
|
|
ctx := RenderContext{
|
|
builder: &strings.Builder{},
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
ComplexPage().Render(&ctx)
|
|
ctx.builder.Reset()
|
|
}
|
|
}
|
|
|
|
var CachedComplexPage = Cached(time.Hour, func() *Element {
|
|
return ComplexPage()
|
|
})
|
|
|
|
func ComplexPage() *Element {
|
|
return Html(
|
|
Head(
|
|
Meta("title", "Complex Page"),
|
|
Meta(
|
|
"charset",
|
|
"UTF-8",
|
|
),
|
|
Meta(
|
|
"viewport",
|
|
"width=device-width, initial-scale=1.0",
|
|
),
|
|
Link(
|
|
"stylesheet",
|
|
"https://example.com/styles.css",
|
|
),
|
|
),
|
|
Body(
|
|
Header(
|
|
Class("bg-gray-800 text-white py-4"),
|
|
Div(
|
|
Class("container mx-auto"),
|
|
H1(Class("text-3xl font-bold"), Text("Welcome to the Complex Page")),
|
|
Nav(
|
|
Ul(
|
|
Class("flex space-x-4"),
|
|
Li(A(Href("#"), Text("Home"))),
|
|
Li(A(Href("#"), Text("About"))),
|
|
Li(A(Href("#"), Text("Services"))),
|
|
Li(A(Href("#"), Text("Contact"))),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Main(
|
|
Class("container mx-auto mt-10"),
|
|
Section(
|
|
Class("grid grid-cols-3 gap-4"),
|
|
Article(
|
|
Class("col-span-2"),
|
|
H2(Class("text-2xl font-semibold mb-4"), Text("Featured Article")),
|
|
Img(Src("https://example.com/featured.jpg"), Alt("Featured Image")),
|
|
P(Class("mt-2 text-lg"), Text("This is a large article to test rendering performance.")),
|
|
),
|
|
Aside(
|
|
Class("bg-gray-100 p-4"),
|
|
H3(Class("text-xl font-bold"), Text("Related Links")),
|
|
Ul(
|
|
Li(A(Href("#"), Text("Related Link 1"))),
|
|
Li(A(Href("#"), Text("Related Link 2"))),
|
|
Li(A(Href("#"), Text("Related Link 3"))),
|
|
),
|
|
),
|
|
),
|
|
Section(
|
|
Class("my-8"),
|
|
H2(Class("text-2xl font-semibold mb-4"), Text("User Registration Form")),
|
|
Form(
|
|
Post("/register", "click"),
|
|
Div(
|
|
Class("grid grid-cols-2 gap-4"),
|
|
Label(For("first_name"), Text("First Name")),
|
|
Input("text", Id("first_name"), Name("first_name"), Class("border p-2 w-full")),
|
|
Label(For("last_name"), Text("Last Name")),
|
|
Input("text", Id("last_name"), Name("last_name"), Class("border p-2 w-full")),
|
|
Label(For("email"), Text("Email")),
|
|
Input("email", Id("email"), Name("email"), Class("border p-2 w-full")),
|
|
Label(For("password"), Text("Password")),
|
|
Input("password", Id("password"), Name("password"), Class("border p-2 w-full")),
|
|
),
|
|
Button(
|
|
Type("submit"),
|
|
Class("bg-blue-500 text-white py-2 px-4 mt-4"),
|
|
Text("Register"),
|
|
),
|
|
),
|
|
),
|
|
Section(
|
|
Class("my-8"),
|
|
H2(Class("text-2xl font-semibold mb-4"), Text("Data Table")),
|
|
Table(
|
|
Class("table-auto w-full border-collapse border"),
|
|
THead(
|
|
Tr(
|
|
Th(Class("border px-4 py-2"), Text("ID")),
|
|
Th(Class("border px-4 py-2"), Text("Name")),
|
|
Th(Class("border px-4 py-2"), Text("Age")),
|
|
Th(Class("border px-4 py-2"), Text("Occupation")),
|
|
),
|
|
),
|
|
TBody(
|
|
Tr(
|
|
Td(Class("border px-4 py-2"), Text("1")),
|
|
Td(Class("border px-4 py-2"), Text("John Doe")),
|
|
Td(Class("border px-4 py-2"), Text("28")),
|
|
Td(Class("border px-4 py-2"), Text("Engineer")),
|
|
),
|
|
Tr(
|
|
Td(Class("border px-4 py-2"), Text("2")),
|
|
Td(Class("border px-4 py-2"), Text("Jane Smith")),
|
|
Td(Class("border px-4 py-2"), Text("34")),
|
|
Td(Class("border px-4 py-2"), Text("Designer")),
|
|
),
|
|
Tr(
|
|
Td(Class("border px-4 py-2"), Text("3")),
|
|
Td(Class("border px-4 py-2"), Text("Alice Johnson")),
|
|
Td(Class("border px-4 py-2"), Text("45")),
|
|
Td(Class("border px-4 py-2"), Text("Manager")),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Footer(
|
|
Class("bg-gray-800 text-white py-4 mt-10"),
|
|
Div(
|
|
Class("container mx-auto text-center"),
|
|
Text("© 2024 Complex Page Inc. All rights reserved."),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
func MailTo(email string) *Element {
|
|
return Div(
|
|
H1(
|
|
Text("Contact Us"),
|
|
),
|
|
Div(
|
|
Style("font-family: 'sans-serif'"),
|
|
Id("test"),
|
|
Attribute("data-contents", `something with "quotes" and a <tag>`),
|
|
Div(
|
|
Text("email:"),
|
|
A(
|
|
Href(email),
|
|
Text("Email me"),
|
|
),
|
|
),
|
|
),
|
|
Hr(
|
|
Attribute("noshade", ""),
|
|
),
|
|
Hr(
|
|
Attribute("optionA", ""),
|
|
Attribute("optionB", ""),
|
|
Attribute("optionC", "other"),
|
|
),
|
|
Hr(
|
|
Attribute("noshade", ""),
|
|
),
|
|
)
|
|
}
|