rastrillo / fiesta Public

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

Plain git — no account needed to clone.

Download

Download this file

1package codeview
2
3import (
4 "strings"
5 "testing"
6 "unicode"
7)
8
9// squash is markup with every whitespace character taken out. Format
10// may add, move and drop whitespace and must touch nothing else, so the
11// two sides of a formatting agree once squashed. Whether the whitespace
12// it moved could show is the browser's question, not this one's, and
13// no drive here asks it yet (see the note above flexAttrs).
14func squash(s string) string {
15 return strings.Map(func(r rune) rune {
16 if unicode.IsSpace(r) {
17 return -1
18 }
19 return r
20 }, s)
21}
22
23var formatCases = []string{
24 `<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="t">Title</label><input rst-input id="t" name="t"></div></form></section>`,
25 `<div rst-list><div rst-lrow><a class="rst-nm" href="/p/1">Release notes<small>2 August</small></a> <span rst-pill>Draft</span></div></div>`,
26 `<span rst-meter><meter rst-meter-bar value="100" min="0" max="100" aria-hidden="true"></meter><span rst-meter-num>700/500</span></span>`,
27 "<ul>\n <li>One</li>\n\n<li>Two</li></ul><p>After</p>",
28 `<details rst-dropdown name="rst-menus"><summary>Filter<span rst-caret aria-hidden="true"><svg class="icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></span></summary><div rst-dropdown-menu><a href="/a">A</a></div></details>`,
29 `<div rst-form-foot><button rst-btn="primary lg" type="submit" aria-busy="true" disabled><span rst-spin aria-hidden="true"></span>Publish</button><a rst-btn="lg" href="/posts">Back</a></div>`,
30 `<form rst-form-actions method="post"><input type="hidden" name="csrf" value="t"><input type="hidden" name="id" value="1"><a rst-btn href="/p">Cancel</a><button rst-btn="danger">Delete</button></form>`,
31}
32
33func TestFormatChangesOnlyWhitespaceAndIsIdempotent(t *testing.T) {
34 for _, in := range formatCases {
35 out := Format(in)
36 if squash(out) != squash(in) {
37 t.Errorf("Format changed more than whitespace:\n in: %s\nout: %s", in, out)
38 }
39 if Format(out) != out {
40 t.Errorf("Format is not idempotent on:\n%s\nsecond pass:\n%s", out, Format(out))
41 }
42 }
43}
44
45// The space between two inline elements, and between text and an
46// inline element, is rendered wherever the container is not one the
47// formatter knows lays its children out as flex or grid, so Format
48// keeps it exactly. An unknown container stays inline.
49func TestFormatKeepsTheSpaceBetweenInlineElements(t *testing.T) {
50 for in, want := range map[string]string{
51 `<div><a href="/a">A</a> <span>B</span></div>`: "<div>\n <a href=\"/a\">A</a> <span>B</span>\n</div>",
52 `<p>Hello <b>world</b> <i>again</i></p>`: "<p>Hello <b>world</b> <i>again</i></p>",
53 `<span rst-a>x</span> <span rst-b>y</span>`: `<span rst-a>x</span> <span rst-b>y</span>`,
54 "<li><a href=\"/x\">X</a>\n<a href=\"/y\">Y</a></li>": "<li>\n <a href=\"/x\">X</a>\n<a href=\"/y\">Y</a>\n</li>",
55 `<p>Total <output name="t">$1</output> due</p>`: `<p>Total <output name="t">$1</output> due</p>`,
56 `<label rst-field-label for="a">Name <span rst-field-required aria-hidden="true">*</span></label>`: `<label rst-field-label for="a">Name <span rst-field-required aria-hidden="true">*</span></label>`,
57 `<div rst-unknown><span>a</span><span>b</span></div>`: "<div rst-unknown>\n <span>a</span><span>b</span>\n</div>",
58 } {
59 if got := Format(in); got != want {
60 t.Errorf("Format(%q)\n got %q\nwant %q", in, got, want)
61 }
62 }
63}
64
65func TestFormatBreaksAtBlockTagsAndIndents(t *testing.T) {
66 got := Format(`<section rst-box><form><div rst-field><label>Title</label><input name="t"></div></form></section>`)
67 want := "<section rst-box>\n <form>\n <div rst-field>\n <label>Title</label><input name=\"t\">\n </div>\n </form>\n</section>"
68 if got != want {
69 t.Errorf("got\n%s\nwant\n%s", got, want)
70 }
71}
72
73// Inside a flex or grid container whitespace between children renders
74// as nothing, so every child gets a line: the busy button's spinner and
75// its label, and the two buttons beside each other. A button holding
76// only its label keeps it inline.
77func TestFormatPutsEachChildOfAFlexContainerOnALine(t *testing.T) {
78 got := Format(formatCases[5])
79 want := "<div rst-form-foot>\n" +
80 " <button rst-btn=\"primary lg\" type=\"submit\" aria-busy=\"true\" disabled>\n" +
81 " <span rst-spin aria-hidden=\"true\"></span>\n" +
82 " Publish\n" +
83 " </button>\n" +
84 " <a rst-btn=\"lg\" href=\"/posts\">Back</a>\n" +
85 "</div>"
86 if got != want {
87 t.Errorf("got\n%s\nwant\n%s", got, want)
88 }
89 got = Format(formatCases[2])
90 want = "<span rst-meter>\n <meter rst-meter-bar value=\"100\" min=\"0\" max=\"100\" aria-hidden=\"true\"></meter>\n <span rst-meter-num>700/500</span>\n</span>"
91 if got != want {
92 t.Errorf("got\n%s\nwant\n%s", got, want)
93 }
94}
95
96// A hidden input renders nothing, so the space beside it shows only if
97// something visible shares its line. At the head of a form, before the
98// first thing drawn, each one gets a line.
99func TestFormatPutsSilentElementsOnLinesOfTheirOwn(t *testing.T) {
100 got := Format(formatCases[6])
101 want := "<form rst-form-actions method=\"post\">\n" +
102 " <input type=\"hidden\" name=\"csrf\" value=\"t\">\n" +
103 " <input type=\"hidden\" name=\"id\" value=\"1\">\n" +
104 " <a rst-btn href=\"/p\">Cancel</a>\n" +
105 " <button rst-btn=\"danger\">Delete</button>\n" +
106 "</form>"
107 if got != want {
108 t.Errorf("got\n%s\nwant\n%s", got, want)
109 }
110 // In a sentence, a hidden input between two words keeps the line it
111 // is on: a break there would be a space between them.
112 in := `<p>one<input type="hidden" name="a">two</p>`
113 if got := Format(in); got != in {
114 t.Errorf("a hidden input inside running text was broken out:\n%s", got)
115 }
116}
117
118// An element with nothing in it stays with nothing in it: :empty is a
119// selector tokens.css uses, and a line break would stop it matching.
120func TestFormatLeavesAnEmptyElementEmpty(t *testing.T) {
121 for _, in := range []string{
122 `<div rst-form-foot><span rst-spin aria-hidden="true"></span></div>`,
123 `<footer rst-shell-foot></footer>`,
124 `<div rst-lrow="head"><span>Order</span><span></span></div>`,
125 } {
126 out := Format(in)
127 for _, empty := range []string{`<span rst-spin aria-hidden="true"></span>`, `<footer rst-shell-foot></footer>`, `<span></span>`} {
128 if strings.Contains(in, empty) && !strings.Contains(out, empty) {
129 t.Errorf("an empty element got content:\n%s", out)
130 }
131 }
132 }
133}
134
135// A <pre> or <textarea> whose content looks like tags,
136// an svg, and an attribute value holding a '>' all pass through byte
137// for byte.
138func TestFormatLeavesRawZonesAndQuotedBrackets(t *testing.T) {
139 for _, raw := range []string{
140 "<pre> <div>\n not a tag </div>\n</pre>",
141 "<textarea name=\"n\"><p>typed</p>\n </textarea>",
142 `<svg viewBox="0 0 2 2"><g><path d="M0 0"/></g></svg>`,
143 } {
144 in := "<div>" + raw + "</div>"
145 out := Format(in)
146 if !strings.Contains(out, raw) {
147 t.Errorf("a raw zone changed:\n in: %s\nout: %s", in, out)
148 }
149 }
150 in := `<div data-tip="a > b"><p title='x <div> y'>t</p></div>`
151 out := Format(in)
152 for _, v := range []string{`data-tip="a > b"`, `title='x <div> y'`} {
153 if !strings.Contains(out, v) {
154 t.Errorf("an attribute value changed: %s", out)
155 }
156 }
157 if strings.Count(out, "\n") != 2 {
158 t.Errorf("a '>' inside a quoted value was taken for the end of a tag:\n%s", out)
159 }
160}
161
162// A select sits in its line like a word: the space after it shows, so
163// the formatter may lay out its option list, where nothing between two
164// options renders, but must not break around the select itself.
165func TestFormatKeepsASelectInItsLine(t *testing.T) {
166 in := `<p>Pick <select><option>A</option></select><span>B</span></p>`
167 if got := Format(in); !strings.Contains(got, "Pick <select>") || !strings.Contains(got, "</select><span>B</span>") {
168 t.Errorf("a select in running text was broken out:\n%s", got)
169 }
170 got := Format(`<div rst-field><select name="s"><optgroup label="E"><option>A</option><option>B</option></optgroup></select></div>`)
171 want := "<div rst-field>\n <select name=\"s\">\n <optgroup label=\"E\">\n <option>A</option>\n <option>B</option>\n </optgroup>\n </select>\n</div>"
172 if got != want {
173 t.Errorf("got\n%s\nwant\n%s", got, want)
174 }
175}
176
177// Under white-space: pre and its kin a line break is a line break and
178// an indent is spaces, whatever the container. An element that says so
179// in its style attribute is left byte for byte, its children included,
180// as a <pre> is.
181func TestFormatLeavesWhitespacePreservingStylesAlone(t *testing.T) {
182 for _, style := range []string{"white-space:pre", "white-space: pre-wrap", "white-space:pre-line", "white-space: break-spaces", "white-space-collapse: preserve"} {
183 button := `<button rst-btn style="` + style + `">Pay <em>now</em></button>`
184 got := Format(`<div rst-form-foot>` + button + `<a rst-btn href="/">Back</a></div>`)
185 if !strings.Contains(got, button) {
186 t.Errorf("%s: the button's content was reformatted:\n%s", style, got)
187 }
188 }
189 // nowrap collapses whitespace like normal, so it changes nothing.
190 got := Format(`<div rst-form-foot><button rst-btn style="white-space: nowrap"><span rst-spin></span>Pay</button></div>`)
191 if !strings.Contains(got, "\n Pay\n") {
192 t.Errorf("white-space: nowrap stopped the formatter:\n%s", got)
193 }
194}
195
196// HTML allows whitespace either side of an attribute's =, and the
197// formatter's reading of a tag has to see the attribute the browser
198// sees: style = "white-space:pre" preserves whitespace exactly as
199// style="white-space:pre" does.
200func TestFormatReadsAnAttributeWithSpacesAroundItsEquals(t *testing.T) {
201 button := "<button rst-btn style = \"white-space:pre\">Pay <em>now</em></button>"
202 got := Format(`<div rst-form-foot>` + button + `<a rst-btn href="/">Back</a></div>`)
203 if !strings.Contains(got, button) {
204 t.Errorf("a style written with spaces around = was not seen:\n%s", got)
205 }
206 attrs := attrsOf("<input type =\n'hidden' name\t= n value=v checked>")
207 for name, want := range map[string]string{"type": "hidden", "name": "n", "value": "v", "checked": ""} {
208 if got, ok := attrs[name]; !ok || got != want {
209 t.Errorf("attrsOf: %s = %q (present %v), want %q", name, got, ok, want)
210 }
211 }
212 if len(attrs) != 4 {
213 t.Errorf("attrsOf read %d attributes, want 4: %v", len(attrs), attrs)
214 }
215}
216