From 850dbdb0b6d17cce8afb3ccfb858d3e635389711 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 20:08:13 +0000 Subject: [PATCH] feat(html): index repeated layout block ids Co-Authored-By: Virgil --- edge_test.go | 7 ++++++- layout.go | 12 ++++++++---- path.go | 13 ++++++++++--- path_test.go | 22 ++++++++++++---------- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/edge_test.go b/edge_test.go index d9ec689..30f2698 100644 --- a/edge_test.go +++ b/edge_test.go @@ -366,7 +366,7 @@ func TestLayout_InvalidVariant_MixedValidInvalid(t *testing.T) { func TestLayout_DuplicateVariantChars(t *testing.T) { ctx := NewContext() - // "CCC" — C appears three times. Should render C slot content three times. + // "CCC" — C appears three times. Each occurrence should get its own block index. layout := NewLayout("CCC").C(Raw("content")) got := layout.Render(ctx) @@ -374,6 +374,11 @@ func TestLayout_DuplicateVariantChars(t *testing.T) { if count != 3 { t.Errorf("CCC variant should render C slot 3 times, got %d occurrences in:\n%s", count, got) } + for _, want := range []string{`data-block="C-0"`, `data-block="C-1"`, `data-block="C-2"`} { + if !strings.Contains(got, want) { + t.Errorf("CCC variant should contain %q in:\n%s", want, got) + } + } } func TestLayout_EmptySlots(t *testing.T) { diff --git a/layout.go b/layout.go index 452f71e..d0fd95c 100644 --- a/layout.go +++ b/layout.go @@ -138,9 +138,10 @@ func (l *Layout) setAttr(key, value string) { l.attrs[key] = value } -// blockID returns the deterministic data-block attribute value for a slot. -func (l *Layout) blockID(slot byte) string { - return l.path + string(slot) + "-0" +// blockID returns the deterministic data-block attribute value for a slot +// occurrence within this layout variant. +func (l *Layout) blockID(slot byte, index int) string { + return l.path + string(slot) + "-" + strconv.Itoa(index) } // layout.go: VariantError reports whether the layout variant string contained any invalid @@ -171,6 +172,7 @@ func (l *Layout) Render(ctx *Context) string { var b strings.Builder + slotCounts := make(map[byte]int) for i := range len(l.variant) { slot := l.variant[i] children := l.slots[slot] @@ -180,7 +182,9 @@ func (l *Layout) Render(ctx *Context) string { continue } - bid := l.blockID(slot) + index := slotCounts[slot] + slotCounts[slot] = index + 1 + bid := l.blockID(slot, index) b.WriteByte('<') b.WriteString(escapeHTML(meta.tag)) diff --git a/path.go b/path.go index f51641e..338ac68 100644 --- a/path.go +++ b/path.go @@ -10,7 +10,7 @@ func ParseBlockID(id string) []byte { } // Split on "-" and require the exact structural pattern: - // slot, 0, slot, 0, ... + // slot, numeric index, slot, numeric index, ... var slots []byte i := 0 for part := range strings.SplitSeq(id, "-") { @@ -22,8 +22,15 @@ func ParseBlockID(id string) []byte { return nil } slots = append(slots, part[0]) - } else if part != "0" { - return nil + } else { + if part == "" { + return nil + } + for j := range len(part) { + if part[j] < '0' || part[j] > '9' { + return nil + } + } } i++ } diff --git a/path_test.go b/path_test.go index 15ef610..e80df1e 100644 --- a/path_test.go +++ b/path_test.go @@ -149,21 +149,22 @@ func TestNestedLayout_NilChild(t *testing.T) { func TestBlockID(t *testing.T) { tests := []struct { - path string - slot byte - want string + path string + slot byte + index int + want string }{ - {"", 'H', "H-0"}, - {"L-0-", 'C', "L-0-C-0"}, - {"C-0-C-0-", 'C', "C-0-C-0-C-0"}, - {"", 'F', "F-0"}, + {"", 'H', 0, "H-0"}, + {"L-0-", 'C', 0, "L-0-C-0"}, + {"C-0-C-0-", 'C', 0, "C-0-C-0-C-0"}, + {"", 'F', 2, "F-2"}, } for _, tt := range tests { l := &Layout{path: tt.path} - got := l.blockID(tt.slot) + got := l.blockID(tt.slot, tt.index) if got != tt.want { - t.Errorf("blockID(%q, %c) = %q, want %q", tt.path, tt.slot, got, tt.want) + t.Errorf("blockID(%q, %c, %d) = %q, want %q", tt.path, tt.slot, tt.index, got, tt.want) } } } @@ -174,10 +175,11 @@ func TestParseBlockID(t *testing.T) { want []byte }{ {"L-0-C-0", []byte{'L', 'C'}}, + {"C-0-C-1", []byte{'C', 'C'}}, {"H-0", []byte{'H'}}, {"C-0-C-0-C-0", []byte{'C', 'C', 'C'}}, {"", nil}, - {"L-1-C-0", nil}, + {"L-1-C-0", []byte{'L', 'C'}}, {"L-0-C", nil}, {"LL-0", nil}, {"X-0", nil},