Grace Hopper Harbour
`)) for _, want := range []string{"Help: orders", "Back to the start"} { if !slices.Contains(runs, want) { t.Errorf("the sweep does not read the spoken attribute %q of a kept element; it reads %q", want, runs) } } for _, kept := range []string{"Grace Hopper", "Harbour"} { if slices.Contains(runs, kept) { t.Errorf("the sweep reads %q, the content of a kept element", kept) } } } // leakSweepFor is the sweep over a full render's traced strings. Its // exemptions are leakExempt, leakUnchanged and leakCollisions, lists // written down, so nothing a catalog says can widen them; the controls // build it again after changing the catalogs all the same, so that // stays true if an exemption is ever drawn from them. func leakSweepFor(t *testing.T, english map[string]string) leakSweep { t.Helper() return newLeakSweep(tracedIDs(t), english) } func asRendered(s string) string { var b strings.Builder if err := renderedText.Execute(&b, s); err != nil { panic(err) } return b.String() } var renderedText = template.Must(template.New("").Parse("{{.}}")) // escapedSentences are traced strings html/template writes escaped: one // with an apostrophe, one with a quote and one with a plus sign, where // the gallery says any, each a whole sentence the sweep looks for. func escapedSentences(t *testing.T) map[string]string { t.Helper() english := englishStrings(t) ids := tracedIDs(t) sorted := make([]string, 0, len(ids)) for id := range ids { sorted = append(sorted, id) } sort.Strings(sorted) out := map[string]string{} for _, mark := range []string{"'", `"`, "+"} { for _, id := range sorted { en := english[id] if strings.Contains(en, mark) && !placeholderName.MatchString(en) && len([]rune(en)) >= leakFloor && en == squash(en) && !strings.HasPrefix(id, "labels.") { out[id] = en break } } } if len(out) < 2 { t.Fatalf("found %d escaped sentences to try, want at least the apostrophe and the quote", len(out)) } return out } // TestTheLeakSweepSeesEscapedPunctuation: a sentence html/template // writes with ', " or + is still reported. Each one, with // its Irish translation put back to the English, is on the Irish pages // only in its escaped form, and the sweep reports it there. func TestTheLeakSweepSeesEscapedPunctuation(t *testing.T) { s := newTestSite(t, fiesta.FS(), "") cs := loadedCatalogs(t) for id, en := range escapedSentences(t) { if asRendered(en) == en { t.Errorf("%s (%q) is written as it is, so it is not the case this covers", id, en) } if got := englishLeaks(pageWords(t, []byte(""+asRendered(en)+"
")), []string{en}); len(got) != 1 { t.Errorf("the rendered form of %q is not reported", en) } orig := cs["ga"][id] cs["ga"][id] = en files, err := Render(fiesta.FS(), Options{}) cs["ga"][id] = orig if err != nil { t.Fatal(err) } var raw strings.Builder reported := false for _, name := range translatedPages(s, files) { if !strings.Contains(name, "/ga/") { continue } raw.Write(files[name]) if len(englishLeaks(pageWords(t, files[name]), []string{en})) == 1 { reported = true } } if strings.Contains(raw.String(), en) { t.Errorf("the raw English of %s is on the Irish pages, so this case does not reach the escaped form", id) } if !reported { t.Errorf("with its Irish translation put back to the English, %s (%q) is not reported on any Irish page", id, en) } } } // literalText is every run of letters a template writes outside its // actions: the text between tags, an SVG's title and desc included, // and the values of the spoken attributes, however they are quoted. // The template is read by the HTML tokenizer with each action replaced // by actionHole, a private-use character no template writes, and a run // is cut at each hole: what an action prints is a lookup or a field. Script and style say nothing; an SVG's drawing data (d, // points, viewBox) is in attributes that are not spoken, so it is // never read. func literalText(src string) []string { z := xhtml.NewTokenizer(strings.NewReader(templateAction.ReplaceAllString(src, actionHole))) var out []string keep := func(chunk string) { for _, piece := range strings.Split(chunk, actionHole) { if c := squash(piece); c != "" && templateLetter.MatchString(c) { out = append(out, c) } } } silent := false for { switch tt := z.Next(); tt { case xhtml.ErrorToken: return out case xhtml.StartTagToken, xhtml.SelfClosingTagToken: name, more := z.TagName() for more { var k, v []byte k, v, more = z.TagAttr() if leakSpoken[string(k)] { keep(string(v)) } } if tt == xhtml.StartTagToken && (string(name) == "script" || string(name) == "style") { silent = true } case xhtml.EndTagToken: if name, _ := z.TagName(); string(name) == "script" || string(name) == "style" { silent = false } case xhtml.TextToken: if !silent { keep(string(z.Text())) } } } } var ( // An action, non-greedy: a say call's arguments carry no braces. templateAction = regexp.MustCompile(`(?s)\{\{.*?\}\}`) templateLetter = regexp.MustCompile(`\p{L}`) ) // actionHole stands where an action was. const actionHole = "\ue000" // templateFixtures are the runs of letters a template may write as // they are, because they are the same in every language: the type // specimen beside each font-size token, as Rastrillo's list has it, // and the two halves of the icon file path, icons/{{say "gallery.brand"}} there
{{end}}`, []string{"Hello", "there"}}, {``, []string{"Settings", "A cog"}}, {`{{.Name}}
`, nil}, } { if got := literalText(c.src); !slices.Equal(got, c.want) { t.Errorf("the extractor reads %q from %s, want %q", got, c.src, c.want) } } }