diff --git a/.cover b/.cover deleted file mode 100644 index f7c50cf..0000000 --- a/.cover +++ /dev/null @@ -1,223 +0,0 @@ -mode: set -code.squareroundforest.org/arpio/html/eq.go:3.31,4.26 1 1 -code.squareroundforest.org/arpio/html/eq.go:4.26,6.3 1 0 -code.squareroundforest.org/arpio/html/eq.go:8.2,9.24 2 1 -code.squareroundforest.org/arpio/html/eq.go:9.24,11.3 1 0 -code.squareroundforest.org/arpio/html/eq.go:13.2,13.23 1 1 -code.squareroundforest.org/arpio/html/eq.go:13.23,16.22 3 0 -code.squareroundforest.org/arpio/html/eq.go:16.22,18.4 1 0 -code.squareroundforest.org/arpio/html/eq.go:21.2,22.24 2 1 -code.squareroundforest.org/arpio/html/eq.go:22.24,24.3 1 0 -code.squareroundforest.org/arpio/html/eq.go:26.2,26.20 1 1 -code.squareroundforest.org/arpio/html/eq.go:26.20,29.17 3 0 -code.squareroundforest.org/arpio/html/eq.go:29.17,31.4 1 0 -code.squareroundforest.org/arpio/html/eq.go:33.3,33.27 1 0 -code.squareroundforest.org/arpio/html/eq.go:33.27,35.4 1 0 -code.squareroundforest.org/arpio/html/eq.go:37.3,37.11 1 0 -code.squareroundforest.org/arpio/html/eq.go:37.11,38.12 1 0 -code.squareroundforest.org/arpio/html/eq.go:41.3,41.21 1 0 -code.squareroundforest.org/arpio/html/eq.go:41.21,43.4 1 0 -code.squareroundforest.org/arpio/html/eq.go:46.2,46.13 1 1 -code.squareroundforest.org/arpio/html/eq.go:49.28,50.16 1 1 -code.squareroundforest.org/arpio/html/eq.go:50.16,52.3 1 1 -code.squareroundforest.org/arpio/html/eq.go:54.2,54.22 1 1 -code.squareroundforest.org/arpio/html/eq.go:54.22,56.3 1 0 -code.squareroundforest.org/arpio/html/eq.go:58.2,58.21 1 1 -code.squareroundforest.org/arpio/html/lib.go:23.73,27.2 3 1 -code.squareroundforest.org/arpio/html/lib.go:29.32,33.2 3 1 -code.squareroundforest.org/arpio/html/lib.go:35.42,39.35 4 1 -code.squareroundforest.org/arpio/html/lib.go:39.35,41.3 1 1 -code.squareroundforest.org/arpio/html/lib.go:43.2,43.10 1 1 -code.squareroundforest.org/arpio/html/lib.go:46.56,50.2 3 1 -code.squareroundforest.org/arpio/html/lib.go:52.55,58.2 5 0 -code.squareroundforest.org/arpio/html/lib.go:60.35,66.2 5 1 -code.squareroundforest.org/arpio/html/lib.go:70.32,71.19 1 1 -code.squareroundforest.org/arpio/html/lib.go:71.19,73.3 1 0 -code.squareroundforest.org/arpio/html/lib.go:75.2,76.33 2 1 -code.squareroundforest.org/arpio/html/lib.go:76.33,78.3 1 1 -code.squareroundforest.org/arpio/html/lib.go:80.2,80.11 1 1 -code.squareroundforest.org/arpio/html/lib.go:84.48,85.33 1 1 -code.squareroundforest.org/arpio/html/lib.go:85.33,87.3 1 1 -code.squareroundforest.org/arpio/html/lib.go:89.2,89.40 1 1 -code.squareroundforest.org/arpio/html/lib.go:89.40,91.3 1 1 -code.squareroundforest.org/arpio/html/lib.go:95.29,97.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:100.42,102.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:105.47,108.2 2 1 -code.squareroundforest.org/arpio/html/lib.go:111.62,113.2 1 0 -code.squareroundforest.org/arpio/html/lib.go:117.54,119.2 1 0 -code.squareroundforest.org/arpio/html/lib.go:122.30,124.2 1 0 -code.squareroundforest.org/arpio/html/lib.go:127.48,129.2 1 0 -code.squareroundforest.org/arpio/html/lib.go:132.48,134.19 2 0 -code.squareroundforest.org/arpio/html/lib.go:134.19,136.3 1 0 -code.squareroundforest.org/arpio/html/lib.go:138.2,138.40 1 0 -code.squareroundforest.org/arpio/html/lib.go:142.51,147.24 4 0 -code.squareroundforest.org/arpio/html/lib.go:147.24,148.18 1 0 -code.squareroundforest.org/arpio/html/lib.go:148.18,150.4 1 0 -code.squareroundforest.org/arpio/html/lib.go:153.2,153.44 1 0 -code.squareroundforest.org/arpio/html/lib.go:157.32,159.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:164.78,166.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:169.45,171.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:176.34,178.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:181.39,183.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:187.32,189.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:193.30,195.2 1 1 -code.squareroundforest.org/arpio/html/lib.go:199.28,201.19 2 1 -code.squareroundforest.org/arpio/html/lib.go:201.19,203.3 1 1 -code.squareroundforest.org/arpio/html/lib.go:205.2,205.18 1 1 -code.squareroundforest.org/arpio/html/query.go:29.66,36.23 2 1 -code.squareroundforest.org/arpio/html/query.go:36.23,37.36 1 1 -code.squareroundforest.org/arpio/html/query.go:37.36,39.12 2 1 -code.squareroundforest.org/arpio/html/query.go:42.3,42.38 1 1 -code.squareroundforest.org/arpio/html/query.go:42.38,44.12 2 1 -code.squareroundforest.org/arpio/html/query.go:47.3,47.22 1 1 -code.squareroundforest.org/arpio/html/query.go:50.2,50.18 1 1 -code.squareroundforest.org/arpio/html/query.go:53.42,55.17 2 1 -code.squareroundforest.org/arpio/html/query.go:55.17,57.3 1 1 -code.squareroundforest.org/arpio/html/query.go:59.2,60.23 2 1 -code.squareroundforest.org/arpio/html/query.go:60.23,61.31 1 1 -code.squareroundforest.org/arpio/html/query.go:61.31,63.4 1 1 -code.squareroundforest.org/arpio/html/query.go:66.2,66.11 1 1 -code.squareroundforest.org/arpio/html/query.go:69.57,71.35 2 1 -code.squareroundforest.org/arpio/html/query.go:71.35,73.9 2 1 -code.squareroundforest.org/arpio/html/query.go:73.9,75.4 1 1 -code.squareroundforest.org/arpio/html/query.go:78.2,78.18 1 1 -code.squareroundforest.org/arpio/html/query.go:81.52,82.24 1 1 -code.squareroundforest.org/arpio/html/query.go:82.24,84.3 1 1 -code.squareroundforest.org/arpio/html/query.go:86.2,88.41 3 1 -code.squareroundforest.org/arpio/html/query.go:88.41,91.3 2 1 -code.squareroundforest.org/arpio/html/query.go:93.2,93.47 1 1 -code.squareroundforest.org/arpio/html/query.go:93.47,96.3 2 1 -code.squareroundforest.org/arpio/html/query.go:98.2,98.46 1 1 -code.squareroundforest.org/arpio/html/query.go:98.46,101.3 2 1 -code.squareroundforest.org/arpio/html/query.go:103.2,103.45 1 1 -code.squareroundforest.org/arpio/html/query.go:103.45,106.3 2 1 -code.squareroundforest.org/arpio/html/query.go:108.2,108.49 1 1 -code.squareroundforest.org/arpio/html/query.go:108.49,111.3 2 1 -code.squareroundforest.org/arpio/html/query.go:113.2,113.41 1 1 -code.squareroundforest.org/arpio/html/query.go:113.41,116.3 2 1 -code.squareroundforest.org/arpio/html/query.go:118.2,118.40 1 1 -code.squareroundforest.org/arpio/html/query.go:118.40,119.50 1 1 -code.squareroundforest.org/arpio/html/query.go:119.50,122.4 2 1 -code.squareroundforest.org/arpio/html/query.go:124.3,125.14 2 1 -code.squareroundforest.org/arpio/html/query.go:128.2,128.14 1 1 -code.squareroundforest.org/arpio/html/render.go:26.58,28.26 2 1 -code.squareroundforest.org/arpio/html/render.go:28.26,33.3 4 1 -code.squareroundforest.org/arpio/html/render.go:35.2,35.11 1 1 -code.squareroundforest.org/arpio/html/render.go:38.43,41.19 3 1 -code.squareroundforest.org/arpio/html/render.go:41.19,42.15 1 1 -code.squareroundforest.org/arpio/html/render.go:43.12,44.40 1 1 -code.squareroundforest.org/arpio/html/render.go:45.12,46.39 1 1 -code.squareroundforest.org/arpio/html/render.go:47.11,48.25 1 1 -code.squareroundforest.org/arpio/html/render.go:52.2,52.19 1 1 -code.squareroundforest.org/arpio/html/render.go:55.34,62.19 3 1 -code.squareroundforest.org/arpio/html/render.go:62.19,63.15 1 1 -code.squareroundforest.org/arpio/html/render.go:64.12,65.38 1 1 -code.squareroundforest.org/arpio/html/render.go:66.12,67.38 1 1 -code.squareroundforest.org/arpio/html/render.go:68.12,69.39 1 1 -code.squareroundforest.org/arpio/html/render.go:70.18,71.25 1 1 -code.squareroundforest.org/arpio/html/render.go:71.25,73.5 1 1 -code.squareroundforest.org/arpio/html/render.go:73.10,73.21 1 1 -code.squareroundforest.org/arpio/html/render.go:73.21,75.5 1 1 -code.squareroundforest.org/arpio/html/render.go:75.10,77.5 1 1 -code.squareroundforest.org/arpio/html/render.go:78.11,79.25 1 1 -code.squareroundforest.org/arpio/html/render.go:82.3,84.14 3 1 -code.squareroundforest.org/arpio/html/render.go:87.2,87.19 1 1 -code.squareroundforest.org/arpio/html/render.go:90.55,91.18 1 1 -code.squareroundforest.org/arpio/html/render.go:91.18,93.3 1 1 -code.squareroundforest.org/arpio/html/render.go:95.2,95.37 1 1 -code.squareroundforest.org/arpio/html/render.go:95.37,96.19 1 1 -code.squareroundforest.org/arpio/html/render.go:96.19,98.4 1 1 -code.squareroundforest.org/arpio/html/render.go:100.3,101.19 2 1 -code.squareroundforest.org/arpio/html/render.go:101.19,103.4 1 1 -code.squareroundforest.org/arpio/html/render.go:106.2,108.27 3 1 -code.squareroundforest.org/arpio/html/render.go:108.27,110.3 1 1 -code.squareroundforest.org/arpio/html/render.go:112.2,113.23 2 1 -code.squareroundforest.org/arpio/html/render.go:113.23,114.31 1 1 -code.squareroundforest.org/arpio/html/render.go:114.31,116.4 1 1 -code.squareroundforest.org/arpio/html/render.go:119.2,120.48 2 1 -code.squareroundforest.org/arpio/html/render.go:120.48,122.3 1 1 -code.squareroundforest.org/arpio/html/render.go:124.2,124.13 1 1 -code.squareroundforest.org/arpio/html/render.go:124.13,126.3 1 0 -code.squareroundforest.org/arpio/html/render.go:128.2,129.20 2 1 -code.squareroundforest.org/arpio/html/render.go:129.20,131.3 1 1 -code.squareroundforest.org/arpio/html/render.go:133.2,133.23 1 1 -code.squareroundforest.org/arpio/html/render.go:133.23,134.34 1 1 -code.squareroundforest.org/arpio/html/render.go:134.34,138.77 4 1 -code.squareroundforest.org/arpio/html/render.go:138.77,140.15 2 1 -code.squareroundforest.org/arpio/html/render.go:140.15,142.6 1 1 -code.squareroundforest.org/arpio/html/render.go:144.5,145.59 2 1 -code.squareroundforest.org/arpio/html/render.go:145.59,148.6 2 1 -code.squareroundforest.org/arpio/html/render.go:150.5,150.40 1 0 -code.squareroundforest.org/arpio/html/render.go:153.4,153.46 1 1 -code.squareroundforest.org/arpio/html/render.go:153.46,155.5 1 0 -code.squareroundforest.org/arpio/html/render.go:157.4,160.36 4 1 -code.squareroundforest.org/arpio/html/render.go:160.36,162.5 1 1 -code.squareroundforest.org/arpio/html/render.go:164.4,165.12 2 1 -code.squareroundforest.org/arpio/html/render.go:168.3,169.14 2 1 -code.squareroundforest.org/arpio/html/render.go:169.14,170.12 1 0 -code.squareroundforest.org/arpio/html/render.go:173.3,173.33 1 1 -code.squareroundforest.org/arpio/html/render.go:173.33,175.4 1 1 -code.squareroundforest.org/arpio/html/render.go:177.3,177.21 1 1 -code.squareroundforest.org/arpio/html/render.go:177.21,179.12 2 1 -code.squareroundforest.org/arpio/html/render.go:182.3,182.30 1 1 -code.squareroundforest.org/arpio/html/render.go:185.2,185.46 1 1 -code.squareroundforest.org/arpio/html/render.go:185.46,187.13 2 1 -code.squareroundforest.org/arpio/html/render.go:187.13,189.4 1 1 -code.squareroundforest.org/arpio/html/render.go:191.3,192.31 2 1 -code.squareroundforest.org/arpio/html/render.go:192.31,194.4 1 1 -code.squareroundforest.org/arpio/html/render.go:196.3,197.57 2 1 -code.squareroundforest.org/arpio/html/render.go:197.57,200.4 2 1 -code.squareroundforest.org/arpio/html/render.go:202.3,202.17 1 1 -code.squareroundforest.org/arpio/html/render.go:202.17,204.4 1 1 -code.squareroundforest.org/arpio/html/render.go:207.2,208.34 2 1 -code.squareroundforest.org/arpio/html/render.go:208.34,210.3 1 1 -code.squareroundforest.org/arpio/html/validate.go:14.37,15.31 1 1 -code.squareroundforest.org/arpio/html/validate.go:15.31,17.3 1 1 -code.squareroundforest.org/arpio/html/validate.go:19.2,19.12 1 1 -code.squareroundforest.org/arpio/html/validate.go:22.41,24.2 1 1 -code.squareroundforest.org/arpio/html/validate.go:26.47,28.2 1 1 -code.squareroundforest.org/arpio/html/validate.go:30.50,31.46 1 1 -code.squareroundforest.org/arpio/html/validate.go:31.46,33.3 1 1 -code.squareroundforest.org/arpio/html/validate.go:35.2,37.23 3 1 -code.squareroundforest.org/arpio/html/validate.go:37.23,38.24 1 1 -code.squareroundforest.org/arpio/html/validate.go:38.24,39.54 1 1 -code.squareroundforest.org/arpio/html/validate.go:39.54,41.5 1 1 -code.squareroundforest.org/arpio/html/validate.go:45.2,45.27 1 1 -code.squareroundforest.org/arpio/html/validate.go:45.27,47.3 1 1 -code.squareroundforest.org/arpio/html/validate.go:49.2,49.23 1 1 -code.squareroundforest.org/arpio/html/validate.go:49.23,50.34 1 1 -code.squareroundforest.org/arpio/html/validate.go:50.34,51.32 1 1 -code.squareroundforest.org/arpio/html/validate.go:51.32,53.5 1 1 -code.squareroundforest.org/arpio/html/validate.go:55.4,57.20 3 1 -code.squareroundforest.org/arpio/html/validate.go:57.20,59.5 1 1 -code.squareroundforest.org/arpio/html/validate.go:61.4,61.12 1 1 -code.squareroundforest.org/arpio/html/validate.go:65.2,65.12 1 1 -code.squareroundforest.org/arpio/html/wrap.go:8.40,15.6 2 1 -code.squareroundforest.org/arpio/html/wrap.go:15.6,17.17 2 1 -code.squareroundforest.org/arpio/html/wrap.go:17.17,18.9 1 1 -code.squareroundforest.org/arpio/html/wrap.go:21.3,21.35 1 1 -code.squareroundforest.org/arpio/html/wrap.go:21.35,22.12 1 1 -code.squareroundforest.org/arpio/html/wrap.go:25.3,25.35 1 1 -code.squareroundforest.org/arpio/html/wrap.go:25.35,26.28 1 1 -code.squareroundforest.org/arpio/html/wrap.go:26.28,28.5 1 1 -code.squareroundforest.org/arpio/html/wrap.go:30.4,30.12 1 1 -code.squareroundforest.org/arpio/html/wrap.go:33.3,34.40 2 1 -code.squareroundforest.org/arpio/html/wrap.go:37.2,37.26 1 1 -code.squareroundforest.org/arpio/html/wrap.go:37.26,39.3 1 1 -code.squareroundforest.org/arpio/html/wrap.go:41.2,41.14 1 1 -code.squareroundforest.org/arpio/html/wrap.go:44.71,52.26 3 1 -code.squareroundforest.org/arpio/html/wrap.go:52.26,53.22 1 1 -code.squareroundforest.org/arpio/html/wrap.go:53.22,55.4 1 1 -code.squareroundforest.org/arpio/html/wrap.go:57.3,58.50 2 1 -code.squareroundforest.org/arpio/html/wrap.go:58.50,62.12 4 1 -code.squareroundforest.org/arpio/html/wrap.go:65.3,65.39 1 1 -code.squareroundforest.org/arpio/html/wrap.go:68.2,68.26 1 1 -code.squareroundforest.org/arpio/html/wrap.go:68.26,70.3 1 1 -code.squareroundforest.org/arpio/html/wrap.go:72.2,73.26 2 1 -code.squareroundforest.org/arpio/html/wrap.go:73.26,74.12 1 1 -code.squareroundforest.org/arpio/html/wrap.go:74.12,76.4 1 1 -code.squareroundforest.org/arpio/html/wrap.go:78.3,79.23 2 1 -code.squareroundforest.org/arpio/html/wrap.go:79.23,80.13 1 1 -code.squareroundforest.org/arpio/html/wrap.go:80.13,82.5 1 1 -code.squareroundforest.org/arpio/html/wrap.go:84.4,84.22 1 1 -code.squareroundforest.org/arpio/html/wrap.go:88.2,88.12 1 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf0f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.cover diff --git a/escape.go b/escape.go new file mode 100644 index 0000000..2b9e96d --- /dev/null +++ b/escape.go @@ -0,0 +1,119 @@ +package html + +import ( + "io" + "bufio" + "bytes" + "errors" + "unicode" +) + +const unicodeNBSP = 0xa0 + +type escapeWriter struct { + out *bufio.Writer + spaceStarted, lastSpace bool + err error +} + +func attributeEscape(value string) string { + var rr []rune + r := []rune(value) + for i := range r { + switch r[i] { + case '"': + rr = append(rr, []rune(""")...) + case '&': + rr = append(rr, []rune("&")...) + default: + rr = append(rr, r[i]) + } + } + + return string(rr) +} + +func newEscapeWriter(out io.Writer) *escapeWriter { + return &escapeWriter{out: bufio.NewWriter(out)} +} + +func (w *escapeWriter) write(r ...rune) { + if w.err != nil { + return + } + + for _, ri := range r { + if _, err := w.out.WriteRune(ri); err != nil { + w.err = err + return + } + } +} + +func (w *escapeWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + runes := bytes.NewBuffer(nil) + if n, err := runes.Write(p); err != nil { + w.err = err + return n, w.err + } + + for { + r, _, err := runes.ReadRune() + if errors.Is(err, io.EOF) { + return len(p), nil + } + + if r == unicode.ReplacementChar { + continue + } + + space := r == ' ' + switch { + case space && w.spaceStarted: + w.write([]rune(" ")...) + case space && w.lastSpace: + w.write([]rune(" ")...) + case w.spaceStarted: + w.write(' ') + } + + w.spaceStarted = space && !w.lastSpace + w.lastSpace = space + if space { + continue + } + + switch r { + case '<': + w.write([]rune("<")...) + case '>': + w.write([]rune(">")...) + case '&': + w.write([]rune("&")...) + case unicodeNBSP: + w.write([]rune(" ")...) + default: + w.write(r) + } + } + + return len(p), w.err +} + +func (w *escapeWriter) Flush() error { + if w.err != nil { + return w.err + } + + if w.spaceStarted { + w.write(' ') + w.spaceStarted = false + } + + w.err = w.out.Flush() + return w.err +} diff --git a/escape_test.go b/escape_test.go new file mode 100644 index 0000000..553c281 --- /dev/null +++ b/escape_test.go @@ -0,0 +1,96 @@ +package html_test + +import ( + "code.squareroundforest.org/arpio/html" + . "code.squareroundforest.org/arpio/html/tags" + "testing" + "bytes" +) + +func TestEscape(t *testing.T) { + t.Run("attribute escape", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Div(Attr("foo", "\"bar\" & \"baz\""))); err != nil { + t.Fatal(err) + } + + if buf.String() != "
" { + t.Fatal(buf.String()) + } + }) + + t.Run("failing writer", func(t *testing.T) { + ew := &errorWriter{} + if err := html.Render(ew, Span("foo bar baz")); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("broken unicode", func(t *testing.T) { + b := []byte{'f', 0xc2, 'o', 'o'} + + var buf bytes.Buffer + if err := html.Render(&buf, Span(string(b))); err != nil { + t.Fatal(err) + } + + if buf.String() != "foo" { + t.Fatal(buf.String()) + } + }) + + t.Run("multiple spaces", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Span("foo bar")); err != nil { + t.Fatal(err) + } + + if buf.String() != "foo bar" { + t.Fatal(buf.String()) + } + }) + + t.Run("single space", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Span("foo bar")); err != nil { + t.Fatal(err) + } + + if buf.String() != "foo bar" { + t.Fatal(buf.String()) + } + }) + + t.Run("tailing space", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Span("foo ")); err != nil { + t.Fatal(err) + } + + if buf.String() != "foo " { + t.Fatal(buf.String()) + } + }) + + t.Run("unicode non-break space", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Span(string([]rune{0xa0}))); err != nil { + t.Fatal(err) + } + + if buf.String() != " " { + t.Fatal(buf.String()) + } + }) + + t.Run("html characters", func(t *testing.T) { + var buf bytes.Buffer + if err := html.Render(&buf, Span("