refactor escaping
This commit is contained in:
parent
1308c164a7
commit
7610db6e71
223
.cover
223
.cover
@ -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
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.cover
|
||||
119
escape.go
Normal file
119
escape.go
Normal file
@ -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
|
||||
}
|
||||
96
escape_test.go
Normal file
96
escape_test.go
Normal file
@ -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() != "<div foo=\""bar" & "baz"\"></div>" {
|
||||
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() != "<span>foo</span>" {
|
||||
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() != "<span>foo bar</span>" {
|
||||
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() != "<span>foo bar</span>" {
|
||||
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() != "<span>foo </span>" {
|
||||
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() != "<span> </span>" {
|
||||
t.Fatal(buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("html characters", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := html.Render(&buf, Span("<foo&bar>")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if buf.String() != "<span><foo&bar></span>" {
|
||||
t.Fatal(buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
8
lib.go
8
lib.go
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// when composing html, the Attr convenience function is recommended to construct input attributes
|
||||
type Attributes map[string]string
|
||||
type Attributes map[string]any
|
||||
|
||||
// immutable
|
||||
// calling creates a new copy with the passed in attributes and child nodes applied only to the copy
|
||||
@ -47,7 +47,7 @@ func Attr(a ...any) Attributes {
|
||||
|
||||
am := make(Attributes)
|
||||
for i := 0; i < len(a); i += 2 {
|
||||
am[fmt.Sprint(a[i])] = fmt.Sprint(a[i+1])
|
||||
am[fmt.Sprint(a[i])] = a[i+1]
|
||||
}
|
||||
|
||||
return am
|
||||
@ -109,7 +109,7 @@ func AllAttributes(t Tag) Attributes {
|
||||
}
|
||||
|
||||
// returns the value of a named attribute if exists, empty string otherwise
|
||||
func Attribute(t Tag, name string) string {
|
||||
func Attribute(t Tag, name string) any {
|
||||
q := attributeQuery{name: name}
|
||||
t()(&q)
|
||||
return q.value
|
||||
@ -132,7 +132,7 @@ func DeleteAttribute(t Tag, name string) Tag {
|
||||
|
||||
// the same as Attribute(t, "class")
|
||||
func Class(t Tag) string {
|
||||
return Attribute(t, "class")
|
||||
return fmt.Sprint(Attribute(t, "class"))
|
||||
}
|
||||
|
||||
// the same as SetAttribute(t, "class", class)
|
||||
|
||||
11
notes.txt
11
notes.txt
@ -1,9 +1,3 @@
|
||||
rendering types:
|
||||
like <br>: inline void
|
||||
inline <hr>: block void
|
||||
like script: no escaping
|
||||
split the validation from the rendering
|
||||
take copies of the children to ensure immutability
|
||||
explain the immutability guarantee in the Go docs: for children yes, for children references no. The general
|
||||
recommendation is not to mutate children. Ofc, creatively breaking the rules is always well appreciated by the
|
||||
right audience
|
||||
@ -12,5 +6,6 @@ test empty block
|
||||
escape extra space between tag boundaries
|
||||
declarations: <!doctype html>
|
||||
comments: <!-- foo -->
|
||||
attritubes, when bool true, then just the name of the attribute
|
||||
implement stringer for the tag
|
||||
render nil as empty
|
||||
support readers
|
||||
review which tags should be of type inline-children
|
||||
|
||||
6
query.go
6
query.go
@ -10,7 +10,7 @@ type attributesQuery struct {
|
||||
|
||||
type attributeQuery struct {
|
||||
name string
|
||||
value string
|
||||
value any
|
||||
found bool
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ func mergeAttributes(c []any) Attributes {
|
||||
return to
|
||||
}
|
||||
|
||||
func findAttribute(c []any, name string) (string, bool) {
|
||||
func findAttribute(c []any, name string) (any, bool) {
|
||||
a, _, _ := groupChildren(c)
|
||||
for i := len(a) - 1; i >= 0; i-- {
|
||||
value, ok := a[i][name]
|
||||
@ -75,7 +75,7 @@ func findAttribute(c []any, name string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func handleQuery(name string, children []any) bool {
|
||||
|
||||
@ -59,7 +59,7 @@ func TestQuery(t *testing.T) {
|
||||
|
||||
t.Run("does not exist", func(t *testing.T) {
|
||||
div := Div(Attr("foo", "bar"))
|
||||
if html.Attribute(div, "qux") != "" {
|
||||
if html.Attribute(div, "qux") != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
464
render.go
464
render.go
@ -6,10 +6,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPWidth = 112
|
||||
unicodeNBSP = 0xa0
|
||||
)
|
||||
const defaultPWidth = 112
|
||||
|
||||
type renderGuide struct {
|
||||
inline bool
|
||||
@ -41,60 +38,6 @@ func mergeRenderingGuides(rgs []renderGuide) renderGuide {
|
||||
return rg
|
||||
}
|
||||
|
||||
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 htmlEscape(s string) string {
|
||||
var (
|
||||
rr []rune
|
||||
lastWS, wsStart bool
|
||||
)
|
||||
|
||||
r := []rune(s)
|
||||
for i := range r {
|
||||
switch r[i] {
|
||||
case '<':
|
||||
rr = append(rr, []rune("<")...)
|
||||
case '>':
|
||||
rr = append(rr, []rune(">")...)
|
||||
case '&':
|
||||
rr = append(rr, []rune("&")...)
|
||||
case unicodeNBSP:
|
||||
rr = append(rr, []rune(" ")...)
|
||||
case ' ':
|
||||
if wsStart && lastWS {
|
||||
rr = append(rr[:len(rr)-1], []rune(" ")...)
|
||||
} else if lastWS {
|
||||
rr = append(rr, []rune(" ")...)
|
||||
} else {
|
||||
rr = append(rr, r[i])
|
||||
}
|
||||
default:
|
||||
rr = append(rr, r[i])
|
||||
}
|
||||
|
||||
ws := r[i] == ' '
|
||||
wsStart = ws && !lastWS
|
||||
lastWS = ws
|
||||
}
|
||||
|
||||
return string(rr)
|
||||
}
|
||||
|
||||
func indentLines(indent string, s string) string {
|
||||
l := strings.Split(s, "\n")
|
||||
for i := range l {
|
||||
@ -121,7 +64,12 @@ func (r *renderer) renderAttributes(tagName string, a []Attributes) {
|
||||
printf := r.getPrintf(tagName)
|
||||
for _, ai := range a {
|
||||
for name, value := range ai {
|
||||
printf(" %s=\"%s\"", name, attributeEscape(value))
|
||||
if isTrue, _ := value.(bool); isTrue {
|
||||
printf(" &s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
printf(" %s=\"%s\"", name, attributeEscape(fmt.Sprint(value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,16 +89,25 @@ func (r *renderer) renderUnindented(name string, rg renderGuide, a []Attributes,
|
||||
continue
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !rg.verbatim && !rg.script {
|
||||
s = htmlEscape(s)
|
||||
if rg.verbatim || rg.script {
|
||||
printf(s)
|
||||
}
|
||||
|
||||
printf(s)
|
||||
if r.err == nil {
|
||||
ew := newEscapeWriter(r.out)
|
||||
ew.Write([]byte(s))
|
||||
ew.Flush()
|
||||
r.err = ew.err
|
||||
}
|
||||
}
|
||||
|
||||
printf("</%s>", name)
|
||||
@ -197,6 +154,10 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi
|
||||
for _, c := range children {
|
||||
ct, isTag := c.(Tag)
|
||||
if !isTag && rg.verbatim {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -210,6 +171,10 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi
|
||||
}
|
||||
|
||||
if !isTag && rg.script {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -222,6 +187,10 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi
|
||||
}
|
||||
|
||||
if !isTag {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -235,8 +204,13 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi
|
||||
newWrapper = true
|
||||
}
|
||||
|
||||
s = htmlEscape(s)
|
||||
printf(s)
|
||||
if r.err == nil {
|
||||
ew := newEscapeWriter(r.out)
|
||||
ew.Write([]byte(s))
|
||||
ew.Flush()
|
||||
r.err = ew.err
|
||||
}
|
||||
|
||||
lastBlock = false
|
||||
continue
|
||||
}
|
||||
@ -311,6 +285,10 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil
|
||||
for _, c := range children {
|
||||
ct, isTag := c.(Tag)
|
||||
if !isTag && rg.verbatim {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -324,6 +302,10 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil
|
||||
}
|
||||
|
||||
if !isTag && rg.script {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -336,6 +318,10 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil
|
||||
}
|
||||
|
||||
if !isTag {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
@ -346,8 +332,13 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil
|
||||
}
|
||||
|
||||
r.ensureWrapper()
|
||||
s = htmlEscape(s)
|
||||
printf(s)
|
||||
if r.err == nil {
|
||||
ew := newEscapeWriter(r.out)
|
||||
ew.Write([]byte(s))
|
||||
ew.Flush()
|
||||
r.err = ew.err
|
||||
}
|
||||
|
||||
lastBlock = false
|
||||
continue
|
||||
}
|
||||
@ -402,344 +393,3 @@ func (r *renderer) render(name string, children []any) {
|
||||
|
||||
r.renderBlock(name, rg, a, c)
|
||||
}
|
||||
|
||||
/*
|
||||
func getPrintf(out io.Writer) func(f string, a ...any) {
|
||||
return func(f string, a ...any) {
|
||||
if r.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, r.err = fmt.Fprintf(r.out, f, a...)
|
||||
if r.err != nil {
|
||||
r.err = fmt.Errorf("tag %s: %w", name, r.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderAttributes(out io.Writer, a []Attributes) {
|
||||
printf := getPrintf(out)
|
||||
for _, ai := range a {
|
||||
for name, value := range ai {
|
||||
printf(" %s=\"%s\"", name, attributeEscape(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderUnindented(r *renderer, name string, rg renderGuide, a []Attributes, children []any) {
|
||||
printf := getPrintf(r.out)
|
||||
printf("<%s", name)
|
||||
renderAttributes(r.out, a)
|
||||
printf(">")
|
||||
if rg.void {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range children {
|
||||
if ct, ok := c.(Tag); ok {
|
||||
ct(r)
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !rg.verbatim && !rg.script {
|
||||
s = htmlEscape(s)
|
||||
}
|
||||
|
||||
printf(s)
|
||||
}
|
||||
|
||||
printf("</%s>", name)
|
||||
}
|
||||
|
||||
func renderInline(r *renderer, name string, rg renderGuide, a []Attributes, children []any) {
|
||||
printf := getPrintf(r.out)
|
||||
printf("<%s", name)
|
||||
renderAttributes(r.out, a)
|
||||
printf(">")
|
||||
if rg.void {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range children {
|
||||
if ct, ok := c.(Tag); ok {
|
||||
var rgq renderGuidesQuery
|
||||
ct(&rgq)
|
||||
crg := mergeRenderingGuides(rgq.value)
|
||||
if crg.inline {
|
||||
ct(r)
|
||||
continue
|
||||
}
|
||||
|
||||
printf("\n")
|
||||
cr := new(renderer)
|
||||
*cr = *r
|
||||
cr.currentIndent += cr.indent
|
||||
ct(cr)
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(c)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !rg.verbatim && !rg.script {
|
||||
s = htmlEscape(s)
|
||||
}
|
||||
|
||||
printf(s)
|
||||
}
|
||||
|
||||
printf("</%s>", name)
|
||||
}
|
||||
|
||||
func renderBlock(r *renderer, name string, rg renderGuide, a []Attributes, children []any) {
|
||||
if r.direct == nil {
|
||||
r.direct = r.out
|
||||
}
|
||||
|
||||
printf := getPrintf(r.direct)
|
||||
printf(r.currentIndent)
|
||||
printf("<%s", name)
|
||||
renderAttributes(r.direct, a)
|
||||
printf(">")
|
||||
if len(c) == 0 {
|
||||
printf("</%s>", name)
|
||||
return
|
||||
}
|
||||
|
||||
if r.indent != "" {
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
var (
|
||||
inlineBuffer bytes.Buffer
|
||||
cr *renderer
|
||||
lastInline bool
|
||||
)
|
||||
|
||||
for i, c := range children {
|
||||
if ct, ok := c.(Tag); ok {
|
||||
var rgq renderGuidesQuery
|
||||
ct(&rgq)
|
||||
crg := mergeRenderingGuides(rgq.value)
|
||||
if crg.inline {
|
||||
if cr == nil {
|
||||
cr = new(renderer)
|
||||
*cr = *r
|
||||
cr.currentIndent += cr.indent
|
||||
}
|
||||
|
||||
cr.out = &inlineBuffer
|
||||
if !lastInline {
|
||||
printf(r.currentIndent + r.indent)
|
||||
}
|
||||
|
||||
ct(cr)
|
||||
lastInline = true
|
||||
continue
|
||||
}
|
||||
|
||||
inline := inlineBuffer.String()
|
||||
if inline != "" {
|
||||
// flush
|
||||
// newline
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
lastInline = true
|
||||
}
|
||||
|
||||
inline := inlineBuffer.String()
|
||||
if inline != "" {
|
||||
// flush inline
|
||||
}
|
||||
|
||||
if r.indent != "" {
|
||||
printf("\n")
|
||||
printf(r.currentIndent)
|
||||
}
|
||||
|
||||
printf("</%s>", name)
|
||||
if r.indent != "" {
|
||||
printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func render(r *renderer, name string, children []any) {
|
||||
if r.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
a, c, rgs := groupChildren(children)
|
||||
rg := mergeRenderingGuides(rgs)
|
||||
if r.indent == "" {
|
||||
renderUnindented(r, name, rg, a, c)
|
||||
return
|
||||
}
|
||||
|
||||
if rg.inline {
|
||||
// TODO:
|
||||
// - may need to wrap it here
|
||||
// - could use a wrapping buffer
|
||||
renderInline(r, name, rg, a, c)
|
||||
return
|
||||
}
|
||||
|
||||
renderBlock(r, name, rg, a, c)
|
||||
|
||||
// --
|
||||
|
||||
printf("<%s", name)
|
||||
|
||||
printf(">")
|
||||
if r.indent != "" && !rg.inline && len(c) > 0 {
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
if rg.void {
|
||||
return
|
||||
}
|
||||
|
||||
var inlineBuffer *bytes.Buffer
|
||||
|
||||
// TODO:
|
||||
// - avoid rendering an inline buffer into another inline buffer
|
||||
// - why?
|
||||
// - or, if inline, just use the inline buffer without indentation
|
||||
// - check the wrapping again, if it preserves or eliminates the spaces the right way
|
||||
for i, ci := range c {
|
||||
// tag && rg.inline && crg.inline
|
||||
// tag && rg.inline && !crg.inline
|
||||
// tag && !rg.inline && crg.inline
|
||||
// tag && !rg.inline && !crg.inline
|
||||
// !tag && rg.inline && crg.inline
|
||||
// !tag && rg.inline && !crg.inline
|
||||
// !tag && !rg.inline && crg.inline
|
||||
// !tag && !rg.inline && !crg.inline
|
||||
|
||||
if tag, ok := ci.(Tag); ok {
|
||||
if rg.inline {
|
||||
if inlineBuffer == nil {
|
||||
inlineBuffer = bytes.NewBuffer(nil)
|
||||
}
|
||||
|
||||
var rgq renderGuidesQuery
|
||||
tag(&rgq)
|
||||
crg := mergeRenderingGuides(rgq.value)
|
||||
if r.indent != "" && !crg.inline && inlineBuffer.Len() > 0 {
|
||||
w := r.pwidth
|
||||
if w == 0 {
|
||||
w = defaultPWidth
|
||||
}
|
||||
|
||||
inlineBuffer = wrap(inlineBuffer, w, "")
|
||||
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
|
||||
inlineBuffer = bytes.NewBuffer(nil)
|
||||
}
|
||||
|
||||
if i > 0 && r.indent != "" && !crg.inline {
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
rr := new(renderer)
|
||||
*rr = *r
|
||||
rr.indent = ""
|
||||
rr.currentIndent = ""
|
||||
tag(rr)
|
||||
} else {
|
||||
var rgq renderGuidesQuery
|
||||
tag(&rgq)
|
||||
crg := mergeRenderingGuides(rgq.value)
|
||||
if r.indent != "" && !crg.inline && inlineBuffer.Len() > 0 {
|
||||
w := r.pwidth
|
||||
if w == 0 {
|
||||
w = defaultPWidth
|
||||
}
|
||||
|
||||
inlineBuffer = wrap(inlineBuffer, w, r.currentIndent+r.indent)
|
||||
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
|
||||
inlineBuffer = bytes.NewBuffer(nil)
|
||||
}
|
||||
|
||||
if i > 0 && r.indent != "" && !crg.inline {
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
rr := new(renderer)
|
||||
*rr = *r
|
||||
rr.currentIndent += r.indent
|
||||
if r.indent != "" && crg.inline {
|
||||
rr.out = inlineBuffer
|
||||
}
|
||||
|
||||
tag(rr)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
s := fmt.Sprint(ci)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !rg.verbatim && !rg.script {
|
||||
s = htmlEscape(s)
|
||||
}
|
||||
|
||||
if r.indent == "" {
|
||||
printf(s)
|
||||
continue
|
||||
}
|
||||
|
||||
inlineBuffer.WriteString(s)
|
||||
}
|
||||
|
||||
if r.indent != "" && inlineBuffer.Len() > 0 {
|
||||
w := r.pwidth
|
||||
if w == 0 {
|
||||
w = defaultPWidth
|
||||
}
|
||||
|
||||
var indent string
|
||||
if !rg.inline && !rg.script {
|
||||
indent = r.currentIndent + r.indent
|
||||
}
|
||||
|
||||
inlineBuffer = wrap(inlineBuffer, w, indent)
|
||||
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
|
||||
r.err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !rg.inline {
|
||||
printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if !rg.inline {
|
||||
printf(r.currentIndent)
|
||||
}
|
||||
|
||||
printf("</%s>", name)
|
||||
if r.indent != "" && !rg.inline {
|
||||
printf("\n")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
21
wrap.go
21
wrap.go
@ -67,7 +67,12 @@ func (w *wrapper) Write(p []byte) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
runes := bytes.NewBuffer(p)
|
||||
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) {
|
||||
@ -75,8 +80,7 @@ func (w *wrapper) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
if r == unicode.ReplacementChar {
|
||||
w.err = errors.New("broken unicode stream")
|
||||
return len(p), w.err
|
||||
continue
|
||||
}
|
||||
|
||||
if w.inSingleQuote {
|
||||
@ -95,11 +99,14 @@ func (w *wrapper) Write(p []byte) (int, error) {
|
||||
w.inSingleQuote = r == '\''
|
||||
w.inQuote = r == '"'
|
||||
w.inTag = r != '>'
|
||||
w.word.WriteRune(r)
|
||||
if w.inTag || !unicode.IsSpace(r) {
|
||||
w.word.WriteRune(r)
|
||||
}
|
||||
|
||||
if !w.inTag {
|
||||
if err := w.feed(); err != nil {
|
||||
w.err = err
|
||||
return len(p), err
|
||||
return len(p), w.err
|
||||
}
|
||||
|
||||
w.lastSpace = unicode.IsSpace(r)
|
||||
@ -114,7 +121,7 @@ func (w *wrapper) Write(p []byte) (int, error) {
|
||||
if !w.inWord {
|
||||
if err := w.feed(); err != nil {
|
||||
w.err = err
|
||||
return len(p), err
|
||||
return len(p), w.err
|
||||
}
|
||||
|
||||
w.lastSpace = unicode.IsSpace(r)
|
||||
@ -137,7 +144,7 @@ func (w *wrapper) Write(p []byte) (int, error) {
|
||||
w.inWord = !w.inTag
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
return len(p), w.err
|
||||
}
|
||||
|
||||
func (w *wrapper) Flush() error {
|
||||
|
||||
94
wrap_test.go
94
wrap_test.go
@ -8,16 +8,83 @@ import (
|
||||
)
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
t.Run("broken unicode", func(t *testing.T) {
|
||||
b := []byte{'f', 0xc2, 'o', 'o'}
|
||||
span := Span(string(b))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, span); err == nil {
|
||||
t.Run("write error", func(t *testing.T) {
|
||||
ew := &errorWriter{failAfter: 9}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t", PWidth: 12},
|
||||
Span(Span("foo"), Span("bar"), Span("baz")),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error in text", func(t *testing.T) {
|
||||
ew := &errorWriter{}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t", PWidth: 2},
|
||||
Span("foo", "bar", "baz"),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error on line feed", func(t *testing.T) {
|
||||
ew := &errorWriter{failAfter: 7}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t", PWidth: 3},
|
||||
Span("foo"),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error on indentation", func(t *testing.T) {
|
||||
ew := &errorWriter{failAfter: 15}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t", PWidth: 3},
|
||||
Div(Span("foo")),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error on flush", func(t *testing.T) {
|
||||
ew := &errorWriter{}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t"},
|
||||
Span(Span("foo"), Span("bar"), Span("baz")),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write error on flush during line feed", func(t *testing.T) {
|
||||
ew := &errorWriter{}
|
||||
if err := html.RenderIndent(
|
||||
ew,
|
||||
html.Indentation{Indent: "\t", PWidth: 10},
|
||||
Span("foo bar", Div),
|
||||
); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("null write", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := html.Render(&buf, Div("")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if buf.String() != "<div></div>" {
|
||||
t.Fatal(buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple words", func(t *testing.T) {
|
||||
span := Span("foo bar baz")
|
||||
|
||||
@ -72,15 +139,16 @@ func TestWrap(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple lines", func(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("special whitespace characters", func(t *testing.T) {
|
||||
})
|
||||
div := Div("foo\nbar\tbaz")
|
||||
|
||||
t.Run("spaces around tags", func(t *testing.T) {
|
||||
})
|
||||
var buf bytes.Buffer
|
||||
if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, div); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("one line primitives", func(t *testing.T) {
|
||||
if buf.String() != "<div>\n\tfoo bar baz\n</div>" {
|
||||
t.Fatal(buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
31
writer_test.go
Normal file
31
writer_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package html_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type errorWriter struct{
|
||||
out io.Writer
|
||||
failAfter int
|
||||
}
|
||||
|
||||
func(ew *errorWriter) Write(p []byte) (int, error) {
|
||||
wp := p
|
||||
if len(wp) > ew.failAfter {
|
||||
wp = wp[:ew.failAfter]
|
||||
}
|
||||
|
||||
ew.failAfter -= len(wp)
|
||||
if ew.out != nil {
|
||||
if n, err := ew.out.Write(wp); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
if ew.failAfter > 0 {
|
||||
return len(wp), nil
|
||||
}
|
||||
|
||||
return len(wp), errors.New("test write error")
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user