Compare commits
11 commits
40da0d8b1d
...
18d2933315
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d2933315 | ||
|
|
e34c5c96c3 | ||
|
|
456adce73b | ||
|
|
ef777936d2 | ||
|
|
2fab89ea46 | ||
|
|
5acf63cb4b | ||
|
|
76cef5a8d0 | ||
|
|
a06e9716c5 | ||
|
|
8ac512362a | ||
|
|
f49ddbf374 | ||
|
|
e041f7681f |
11 changed files with 527 additions and 26 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
dist/
|
||||
30
Makefile
Normal file
30
Makefile
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.PHONY: wasm test clean
|
||||
|
||||
WASM_OUT := dist/go-html.wasm
|
||||
# Raw size limit: 3MB (Go WASM has ~2MB runtime floor)
|
||||
WASM_RAW_LIMIT := 3145728
|
||||
# Gzip transfer size limit: 1MB (what users actually download)
|
||||
WASM_GZ_LIMIT := 1048576
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
wasm: $(WASM_OUT)
|
||||
|
||||
$(WASM_OUT):
|
||||
@mkdir -p dist
|
||||
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o $(WASM_OUT) ./cmd/wasm/
|
||||
@RAW=$$(stat -c%s "$(WASM_OUT)" 2>/dev/null || stat -f%z "$(WASM_OUT)"); \
|
||||
GZ=$$(gzip -c "$(WASM_OUT)" | wc -c); \
|
||||
echo "WASM size: $${RAW} bytes raw, $${GZ} bytes gzip"; \
|
||||
if [ "$$GZ" -gt $(WASM_GZ_LIMIT) ]; then \
|
||||
echo "FAIL: gzip transfer size exceeds 1MB limit ($${GZ} bytes)"; \
|
||||
exit 1; \
|
||||
elif [ "$$RAW" -gt $(WASM_RAW_LIMIT) ]; then \
|
||||
echo "WARNING: raw binary exceeds 3MB ($${RAW} bytes) — check imports"; \
|
||||
else \
|
||||
echo "OK: gzip $${GZ} bytes (limit 1MB), raw $${RAW} bytes (limit 3MB)"; \
|
||||
fi
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
55
cmd/wasm/main.go
Normal file
55
cmd/wasm/main.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
|
||||
html "forge.lthn.ai/core/go-html"
|
||||
)
|
||||
|
||||
func renderToString(_ js.Value, args []js.Value) any {
|
||||
if len(args) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
variant := args[0].String()
|
||||
ctx := html.NewContext()
|
||||
|
||||
if len(args) >= 2 {
|
||||
ctx.Locale = args[1].String()
|
||||
}
|
||||
|
||||
layout := html.NewLayout(variant)
|
||||
|
||||
if len(args) >= 3 && args[2].Type() == js.TypeObject {
|
||||
slots := args[2]
|
||||
for _, slot := range []string{"H", "L", "C", "R", "F"} {
|
||||
content := slots.Get(slot)
|
||||
if content.Type() == js.TypeString && content.String() != "" {
|
||||
switch slot {
|
||||
case "H":
|
||||
layout.H(html.Raw(content.String()))
|
||||
case "L":
|
||||
layout.L(html.Raw(content.String()))
|
||||
case "C":
|
||||
layout.C(html.Raw(content.String()))
|
||||
case "R":
|
||||
layout.R(html.Raw(content.String()))
|
||||
case "F":
|
||||
layout.F(html.Raw(content.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return layout.Render(ctx)
|
||||
}
|
||||
|
||||
func main() {
|
||||
js.Global().Set("gohtml", js.ValueOf(map[string]any{
|
||||
"renderToString": js.FuncOf(renderToString),
|
||||
}))
|
||||
|
||||
select {}
|
||||
}
|
||||
50
cmd/wasm/test.html
Normal file
50
cmd/wasm/test.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>go-html WASM Test</title>
|
||||
<style>
|
||||
body { font-family: monospace; margin: 2em; }
|
||||
pre { background: #f5f5f5; padding: 1em; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>go-html WASM Test</h1>
|
||||
<div>
|
||||
<label>Variant: <input id="variant" value="HLCRF"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label>H: <input id="slot-H" value="Header"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label>C: <input id="slot-C" value="Main content"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label>F: <input id="slot-F" value="Footer"></label>
|
||||
</div>
|
||||
<button id="render-btn">Render</button>
|
||||
<h2>Raw HTML Output</h2>
|
||||
<pre id="raw"></pre>
|
||||
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("go-html.wasm"), go.importObject)
|
||||
.then(result => {
|
||||
go.run(result.instance);
|
||||
document.title += " (loaded)";
|
||||
});
|
||||
|
||||
document.getElementById("render-btn").addEventListener("click", function() {
|
||||
const variant = document.getElementById("variant").value;
|
||||
const slots = {
|
||||
H: document.getElementById("slot-H").value,
|
||||
C: document.getElementById("slot-C").value,
|
||||
F: document.getElementById("slot-F").value,
|
||||
};
|
||||
const result = gohtml.renderToString(variant, "en", slots);
|
||||
document.getElementById("raw").textContent = result;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
i18n "forge.lthn.ai/core/go-i18n"
|
||||
"forge.lthn.ai/core/go-i18n/reversal"
|
||||
)
|
||||
|
||||
func TestIntegration_RenderThenReverse(t *testing.T) {
|
||||
|
|
@ -18,12 +16,7 @@ func TestIntegration_RenderThenReverse(t *testing.T) {
|
|||
C(El("p", Text("Files deleted successfully"))).
|
||||
F(El("small", Text("Completed")))
|
||||
|
||||
rendered := page.Render(ctx)
|
||||
text := stripTags(rendered)
|
||||
|
||||
tok := reversal.NewTokeniser()
|
||||
tokens := tok.Tokenise(text)
|
||||
imp := reversal.NewImprint(tokens)
|
||||
imp := Imprint(page, ctx)
|
||||
|
||||
if imp.UniqueVerbs == 0 {
|
||||
t.Error("reversal found no verbs in rendered page")
|
||||
|
|
@ -33,23 +26,27 @@ func TestIntegration_RenderThenReverse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// stripTags removes HTML tags for plain text extraction.
|
||||
func stripTags(html string) string {
|
||||
var b strings.Builder
|
||||
inTag := false
|
||||
for _, r := range html {
|
||||
if r == '<' {
|
||||
inTag = true
|
||||
b.WriteByte(' ')
|
||||
continue
|
||||
}
|
||||
if r == '>' {
|
||||
inTag = false
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
func TestIntegration_ResponsiveImprint(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").
|
||||
H(El("h1", Text("Building project"))).
|
||||
L(El("nav", Text("Deleted files"))).
|
||||
C(El("p", Text("Files deleted successfully"))).
|
||||
R(El("aside", Text("Completed"))).
|
||||
F(El("small", Text("Completed")))).
|
||||
Variant("mobile", NewLayout("C").
|
||||
C(El("p", Text("Files deleted successfully"))))
|
||||
|
||||
imp := Imprint(r, ctx)
|
||||
|
||||
if imp.TokenCount == 0 {
|
||||
t.Error("responsive imprint produced zero tokens")
|
||||
}
|
||||
if imp.UniqueVerbs == 0 {
|
||||
t.Error("responsive imprint found no verbs")
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
|
|
|||
9
node.go
9
node.go
|
|
@ -71,6 +71,15 @@ func El(tag string, children ...Node) Node {
|
|||
}
|
||||
}
|
||||
|
||||
// Attr sets an attribute on an El node. Returns the node for chaining.
|
||||
// If the node is not an *elNode, returns it unchanged.
|
||||
func Attr(n Node, key, value string) Node {
|
||||
if el, ok := n.(*elNode); ok {
|
||||
el.attrs[key] = value
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *elNode) Render(ctx *Context) string {
|
||||
var b strings.Builder
|
||||
|
||||
|
|
|
|||
28
node_test.go
28
node_test.go
|
|
@ -155,6 +155,34 @@ func TestEachNode_Empty(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestElNode_Attr(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Attr(El("div", Raw("content")), "class", "container")
|
||||
got := node.Render(ctx)
|
||||
want := `<div class="container">content</div>`
|
||||
if got != want {
|
||||
t.Errorf("Attr() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_AttrEscaping(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Attr(El("img"), "alt", `he said "hello"`)
|
||||
got := node.Render(ctx)
|
||||
if !strings.Contains(got, `alt="he said "hello""`) {
|
||||
t.Errorf("Attr should escape attribute values, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestElNode_MultipleAttrs(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Attr(Attr(El("a", Raw("link")), "href", "/home"), "class", "nav")
|
||||
got := node.Render(ctx)
|
||||
if !strings.Contains(got, `class="nav"`) || !strings.Contains(got, `href="/home"`) {
|
||||
t.Errorf("multiple Attr() calls should stack, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchNode(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
cases := map[string]Node{
|
||||
|
|
|
|||
75
pipeline.go
Normal file
75
pipeline.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/go-i18n/reversal"
|
||||
)
|
||||
|
||||
// StripTags removes HTML tags from rendered output, returning plain text.
|
||||
// Tag boundaries are replaced with a single space; result is trimmed.
|
||||
func StripTags(html string) string {
|
||||
var b strings.Builder
|
||||
inTag := false
|
||||
for _, r := range html {
|
||||
if r == '<' {
|
||||
inTag = true
|
||||
b.WriteByte(' ')
|
||||
continue
|
||||
}
|
||||
if r == '>' {
|
||||
inTag = false
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
// Collapse multiple spaces into one.
|
||||
result := b.String()
|
||||
for strings.Contains(result, " ") {
|
||||
result = strings.ReplaceAll(result, " ", " ")
|
||||
}
|
||||
return strings.TrimSpace(result)
|
||||
}
|
||||
|
||||
// Imprint renders a node tree to HTML, strips tags, tokenises the text,
|
||||
// and returns a GrammarImprint — the full render-reverse pipeline.
|
||||
func Imprint(node Node, ctx *Context) reversal.GrammarImprint {
|
||||
if ctx == nil {
|
||||
ctx = NewContext()
|
||||
}
|
||||
rendered := node.Render(ctx)
|
||||
text := StripTags(rendered)
|
||||
tok := reversal.NewTokeniser()
|
||||
tokens := tok.Tokenise(text)
|
||||
return reversal.NewImprint(tokens)
|
||||
}
|
||||
|
||||
// CompareVariants runs the imprint pipeline on each responsive variant independently
|
||||
// and returns pairwise similarity scores. Key format: "name1:name2".
|
||||
func CompareVariants(r *Responsive, ctx *Context) map[string]float64 {
|
||||
if ctx == nil {
|
||||
ctx = NewContext()
|
||||
}
|
||||
|
||||
type named struct {
|
||||
name string
|
||||
imp reversal.GrammarImprint
|
||||
}
|
||||
|
||||
var imprints []named
|
||||
for _, v := range r.variants {
|
||||
imp := Imprint(v.layout, ctx)
|
||||
imprints = append(imprints, named{name: v.name, imp: imp})
|
||||
}
|
||||
|
||||
scores := make(map[string]float64)
|
||||
for i := 0; i < len(imprints); i++ {
|
||||
for j := i + 1; j < len(imprints); j++ {
|
||||
key := imprints[i].name + ":" + imprints[j].name
|
||||
scores[key] = imprints[i].imp.Similar(imprints[j].imp)
|
||||
}
|
||||
}
|
||||
return scores
|
||||
}
|
||||
128
pipeline_test.go
Normal file
128
pipeline_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
i18n "forge.lthn.ai/core/go-i18n"
|
||||
)
|
||||
|
||||
func TestStripTags_Simple(t *testing.T) {
|
||||
got := StripTags(`<div>hello</div>`)
|
||||
want := "hello"
|
||||
if got != want {
|
||||
t.Errorf("StripTags(<div>hello</div>) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Nested(t *testing.T) {
|
||||
got := StripTags(`<header role="banner"><h1>Title</h1></header>`)
|
||||
want := "Title"
|
||||
if got != want {
|
||||
t.Errorf("StripTags(nested) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_MultipleRegions(t *testing.T) {
|
||||
got := StripTags(`<header>Head</header><main>Body</main><footer>Foot</footer>`)
|
||||
want := "Head Body Foot"
|
||||
if got != want {
|
||||
t.Errorf("StripTags(multi) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Empty(t *testing.T) {
|
||||
got := StripTags("")
|
||||
if got != "" {
|
||||
t.Errorf("StripTags(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_NoTags(t *testing.T) {
|
||||
got := StripTags("plain text")
|
||||
if got != "plain text" {
|
||||
t.Errorf("StripTags(plain) = %q, want %q", got, "plain text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTags_Entities(t *testing.T) {
|
||||
got := StripTags(`<script>`)
|
||||
want := "<script>"
|
||||
if got != want {
|
||||
t.Errorf("StripTags should preserve entities, got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImprint_FromNode(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
||||
page := NewLayout("HCF").
|
||||
H(El("h1", Text("Building project"))).
|
||||
C(El("p", Text("Files deleted successfully"))).
|
||||
F(El("small", Text("Completed")))
|
||||
|
||||
imp := Imprint(page, ctx)
|
||||
|
||||
if imp.TokenCount == 0 {
|
||||
t.Error("Imprint should produce non-zero token count")
|
||||
}
|
||||
if imp.UniqueVerbs == 0 {
|
||||
t.Error("Imprint should find verbs in rendered content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImprint_SimilarPages(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
||||
page1 := NewLayout("HCF").
|
||||
H(El("h1", Text("Building project"))).
|
||||
C(El("p", Text("Files deleted successfully")))
|
||||
|
||||
page2 := NewLayout("HCF").
|
||||
H(El("h1", Text("Building system"))).
|
||||
C(El("p", Text("Files removed successfully")))
|
||||
|
||||
different := NewLayout("HCF").
|
||||
C(El("p", Raw("no grammar content here xyz abc")))
|
||||
|
||||
imp1 := Imprint(page1, ctx)
|
||||
imp2 := Imprint(page2, ctx)
|
||||
impDiff := Imprint(different, ctx)
|
||||
|
||||
sim := imp1.Similar(imp2)
|
||||
diffSim := imp1.Similar(impDiff)
|
||||
|
||||
if sim <= diffSim {
|
||||
t.Errorf("similar pages should score higher (%f) than different (%f)", sim, diffSim)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVariants(t *testing.T) {
|
||||
svc, _ := i18n.New()
|
||||
i18n.SetDefault(svc)
|
||||
ctx := NewContext()
|
||||
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").
|
||||
H(El("h1", Text("Building project"))).
|
||||
C(El("p", Text("Files deleted successfully"))).
|
||||
F(El("small", Text("Completed")))).
|
||||
Variant("mobile", NewLayout("HCF").
|
||||
H(El("h1", Text("Building project"))).
|
||||
C(El("p", Text("Files deleted successfully"))).
|
||||
F(El("small", Text("Completed"))))
|
||||
|
||||
scores := CompareVariants(r, ctx)
|
||||
|
||||
key := "desktop:mobile"
|
||||
sim, ok := scores[key]
|
||||
if !ok {
|
||||
t.Fatalf("CompareVariants missing key %q, got keys: %v", key, scores)
|
||||
}
|
||||
if sim < 0.8 {
|
||||
t.Errorf("same content in different variants should score >= 0.8, got %f", sim)
|
||||
}
|
||||
}
|
||||
39
responsive.go
Normal file
39
responsive.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package html
|
||||
|
||||
import "strings"
|
||||
|
||||
// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
|
||||
// Each variant is rendered inside a container with data-variant for CSS targeting.
|
||||
type Responsive struct {
|
||||
variants []responsiveVariant
|
||||
}
|
||||
|
||||
type responsiveVariant struct {
|
||||
name string
|
||||
layout *Layout
|
||||
}
|
||||
|
||||
// NewResponsive creates a new multi-variant responsive compositor.
|
||||
func NewResponsive() *Responsive {
|
||||
return &Responsive{}
|
||||
}
|
||||
|
||||
// Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
|
||||
// Variants render in insertion order.
|
||||
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
|
||||
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
|
||||
return r
|
||||
}
|
||||
|
||||
// Render produces HTML with each variant in a data-variant container.
|
||||
func (r *Responsive) Render(ctx *Context) string {
|
||||
var b strings.Builder
|
||||
for _, v := range r.variants {
|
||||
b.WriteString(`<div data-variant="`)
|
||||
b.WriteString(v.name)
|
||||
b.WriteString(`">`)
|
||||
b.WriteString(v.layout.Render(ctx))
|
||||
b.WriteString(`</div>`)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
89
responsive_test.go
Normal file
89
responsive_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResponsive_SingleVariant(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").
|
||||
H(Raw("header")).L(Raw("nav")).C(Raw("main")).R(Raw("aside")).F(Raw("footer")))
|
||||
got := r.Render(ctx)
|
||||
|
||||
if !strings.Contains(got, `data-variant="desktop"`) {
|
||||
t.Errorf("responsive should contain data-variant, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `data-block="H-0"`) {
|
||||
t.Errorf("responsive should contain layout content, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsive_MultiVariant(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))).
|
||||
Variant("tablet", NewLayout("HCF").H(Raw("h")).C(Raw("c")).F(Raw("f"))).
|
||||
Variant("mobile", NewLayout("C").C(Raw("c")))
|
||||
|
||||
got := r.Render(ctx)
|
||||
|
||||
for _, v := range []string{"desktop", "tablet", "mobile"} {
|
||||
if !strings.Contains(got, `data-variant="`+v+`"`) {
|
||||
t.Errorf("responsive missing variant %q in:\n%s", v, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsive_VariantOrder(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").C(Raw("d"))).
|
||||
Variant("mobile", NewLayout("C").C(Raw("m")))
|
||||
|
||||
got := r.Render(ctx)
|
||||
|
||||
di := strings.Index(got, `data-variant="desktop"`)
|
||||
mi := strings.Index(got, `data-variant="mobile"`)
|
||||
if di < 0 || mi < 0 {
|
||||
t.Fatalf("missing variants in:\n%s", got)
|
||||
}
|
||||
if di >= mi {
|
||||
t.Errorf("desktop should appear before mobile (insertion order), desktop=%d mobile=%d", di, mi)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsive_NestedPaths(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
inner := NewLayout("HCF").H(Raw("ih")).C(Raw("ic")).F(Raw("if"))
|
||||
r := NewResponsive().
|
||||
Variant("desktop", NewLayout("HLCRF").C(inner))
|
||||
|
||||
got := r.Render(ctx)
|
||||
|
||||
if !strings.Contains(got, `data-block="C-0-H-0"`) {
|
||||
t.Errorf("nested layout in responsive variant missing C-0-H-0 in:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `data-block="C-0-C-0"`) {
|
||||
t.Errorf("nested layout in responsive variant missing C-0-C-0 in:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsive_VariantsIndependent(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
r := NewResponsive().
|
||||
Variant("a", NewLayout("HLCRF").C(Raw("content-a"))).
|
||||
Variant("b", NewLayout("HCF").C(Raw("content-b")))
|
||||
|
||||
got := r.Render(ctx)
|
||||
|
||||
count := strings.Count(got, `data-block="C-0"`)
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 independent C-0 blocks, got %d in:\n%s", count, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsive_ImplementsNode(t *testing.T) {
|
||||
var _ Node = NewResponsive()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue