rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package check
2
3import (
4 "reflect"
5 "regexp"
6 "strings"
7 "testing"
8
9 "amadan.net/rastrillo/fiesta"
10 "amadan.net/rastrillo/fiesta/example"
11)
12
13// A list-grid row holds one identity link: the stretched a.rst-nm or
14// a[rst-person] that makes the whole row its tap target. Two in one
15// row would overlay each other, and the later one would hide the
16// earlier link. Read off every example state's markup.
17
18var markupTag = regexp.MustCompile(`<(/?)([a-zA-Z][a-zA-Z0-9-]*)([^>]*?)(/?)>`)
19
20var voidElement = map[string]bool{"area": true, "br": true, "col": true, "hr": true, "img": true, "input": true, "link": true, "meta": true, "source": true, "wbr": true}
21
22var (
23 classValue = regexp.MustCompile(`class="([^"]*)"`)
24 attrNamed = func(name string) *regexp.Regexp {
25 return regexp.MustCompile(`(?:^|\s)` + name + `(?:="([^"]*)")?(?:\s|$)`)
26 }
27 lrowAttr = attrNamed("rst-lrow")
28 personAttr = attrNamed("rst-person")
29)
30
31// hasClass reports whether attrs carries class token c, exactly.
32func hasClass(attrs, c string) bool {
33 if m := classValue.FindStringSubmatch(attrs); m != nil {
34 for _, tok := range strings.Fields(m[1]) {
35 if tok == c {
36 return true
37 }
38 }
39 }
40 return false
41}
42
43// isDataRow is a list-grid row in either spelling, not its head row.
44func isDataRow(attrs string) bool {
45 a := " " + attrs + " "
46 if m := lrowAttr.FindStringSubmatch(a); m != nil {
47 return !strings.Contains(" "+m[1]+" ", " head ")
48 }
49 return hasClass(attrs, "rst-lrow") && !hasClass(attrs, "rst-lrow--head")
50}
51
52// isIdentityLink is a.rst-nm or a person link, in either spelling.
53func isIdentityLink(attrs string) bool {
54 return hasClass(attrs, "rst-nm") || hasClass(attrs, "rst-person") || personAttr.MatchString(" "+attrs+" ")
55}
56
57// identityLinksPerRow returns, for every [rst-lrow] data row in markup,
58// how many of its DIRECT children are identity links (a.rst-nm or
59// a[rst-person]). A small depth-tracking reader is enough: the samples
60// are hand-written, well-formed markup.
61func identityLinksPerRow(markup string) []int {
62 var out []int
63 type frame struct {
64 row bool
65 slot int
66 }
67 var stack []frame
68 for _, m := range markupTag.FindAllStringSubmatch(markup, -1) {
69 closing, name, attrs, self := m[1] == "/", strings.ToLower(m[2]), m[3], m[4] == "/"
70 if closing {
71 if len(stack) > 0 {
72 stack = stack[:len(stack)-1]
73 }
74 continue
75 }
76 parentRow := len(stack) > 0 && stack[len(stack)-1].row
77 if parentRow && name == "a" && isIdentityLink(attrs) {
78 out[stack[len(stack)-1].slot]++
79 }
80 if self || voidElement[name] {
81 continue
82 }
83 f := frame{}
84 if isDataRow(attrs) {
85 f = frame{row: true, slot: len(out)}
86 out = append(out, 0)
87 }
88 stack = append(stack, f)
89 }
90 return out
91}
92
93func TestEveryListGridRowHasOneIdentityLink(t *testing.T) {
94 pieces, err := example.All(fiesta.FS())
95 if err != nil {
96 t.Fatal(err)
97 }
98 rows := 0
99 for _, p := range pieces {
100 for i, n := range identityLinksPerRow(p.Markup) {
101 rows++
102 if n > 1 {
103 t.Errorf("example %s/%s row %d has %d identity links; one per row, or the later overlay hides the earlier link", p.Name, p.State, i, n)
104 }
105 }
106 }
107 if rows == 0 {
108 t.Fatal("found no list-grid data rows in the examples; the reader is broken")
109 }
110 // The reader itself, both ways.
111 two := `<div rst-lrow><a class="rst-nm" href="/a">A</a><a rst-person href="/p"><span rst-person-name>P</span></a></div>`
112 if got := identityLinksPerRow(two); len(got) != 1 || got[0] != 2 {
113 t.Errorf("a row with two identity links reads as %v, want [2]", got)
114 }
115 nested := `<div rst-lrow><span><a class="rst-nm" href="/a">A</a></span><a class="rst-nm" href="/b">B</a></div>`
116 if got := identityLinksPerRow(nested); len(got) != 1 || got[0] != 1 {
117 t.Errorf("a nested link counted as a direct child: %v, want [1]", got)
118 }
119 // markup-spelling: old-spelling begin — the reader must count the
120 // class spelling too, which apps still write, so these cases use it.
121 for _, c := range []struct {
122 markup string
123 want []int
124 }{
125 {`<div class="rst-lrow"><a class="rst-nm" href="/a">A</a><a class="rst-person" href="/p">P</a></div>`, []int{2}},
126 {`<div rst-lrow=""><a class="rst-nm" href="/a">A</a><a rst-person href="/p">P</a></div>`, []int{2}},
127 {`<div rst-lrow="head"><a class="rst-nm" href="/a">A</a></div><div class="rst-lrow rst-lrow--head"><a class="rst-nm" href="/a">A</a></div>`, nil},
128 {`<div rst-lrow><a class="rst-nm-extra" href="/a">A</a></div>`, []int{0}},
129 // markup-spelling: old-spelling end
130 } {
131 if got := identityLinksPerRow(c.markup); !reflect.DeepEqual(got, c.want) {
132 t.Errorf("identityLinksPerRow(%s) = %v, want %v", c.markup, got, c.want)
133 }
134 }
135}
136