wip
This commit is contained in:
commit
1df0dbe178
223
.cover
Normal file
223
.cover
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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
|
||||||
49
Makefile
Normal file
49
Makefile
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
SOURCES = $(shell find . -name "*.go" | grep -v [.]gen[.]go)
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
all: clean fmt build cover
|
||||||
|
|
||||||
|
build: $(SOURCES) tags promote-to-tags
|
||||||
|
go build
|
||||||
|
|
||||||
|
tags: tags/block.gen.go tags/inline.gen.go tags/void.block.gen.go tags/void.inline.gen.go tags/script.gen.go
|
||||||
|
|
||||||
|
promote-to-tags: tags/promote.gen.go
|
||||||
|
|
||||||
|
tags/block.gen.go: $(SOURCES) tags.block.txt
|
||||||
|
go run script/generate-tags.go < tags.block.txt > tags/block.gen.go
|
||||||
|
|
||||||
|
tags/inline.gen.go: $(SOURCES) tags.inline.txt
|
||||||
|
go run script/generate-tags.go Inline < tags.inline.txt > tags/inline.gen.go
|
||||||
|
|
||||||
|
tags/void.block.gen.go: $(SOURCES) tags.void.block.txt
|
||||||
|
go run script/generate-tags.go Void < tags.void.block.txt > tags/void.block.gen.go
|
||||||
|
|
||||||
|
tags/void.inline.gen.go: $(SOURCES) tags.void.inline.txt
|
||||||
|
go run script/generate-tags.go Void Inline < tags.void.inline.txt > tags/void.inline.gen.go
|
||||||
|
|
||||||
|
tags/script.gen.go: $(SOURCES) tags.script.txt
|
||||||
|
go run script/generate-tags.go ScriptContent < tags.script.txt > tags/script.gen.go
|
||||||
|
|
||||||
|
tags/promote.gen.go: $(SOURCES) promote-to-tags.txt
|
||||||
|
go run script/promote-to-tags.go < promote-to-tags.txt > tags/promote.gen.go
|
||||||
|
|
||||||
|
fmt: $(SOURCES) tags
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
check: $(SOURCES) tags promote-to-tags
|
||||||
|
go test -count 1
|
||||||
|
|
||||||
|
.cover: $(SOURCES) tags promote-to-tags
|
||||||
|
go test -count 1 -coverprofile .cover
|
||||||
|
|
||||||
|
cover: .cover
|
||||||
|
go tool cover -func .cover
|
||||||
|
|
||||||
|
showcover: .cover
|
||||||
|
go tool cover -html .cover
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm -f tags/*.gen.go
|
||||||
59
eq.go
Normal file
59
eq.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
func eq2(t1, t2 Tag) bool {
|
||||||
|
if Name(t1) != Name(t2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
a1, a2 := AllAttributes(t1), AllAttributes(t2)
|
||||||
|
if len(a1) != len(a2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range a1 {
|
||||||
|
v1 := a1[name]
|
||||||
|
v2, ok := a2[name]
|
||||||
|
if !ok || v1 != v2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c1, c2 := Children(t1), Children(t2)
|
||||||
|
if len(c1) != len(c2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range c1 {
|
||||||
|
ct1, ok1 := c1[i].(Tag)
|
||||||
|
ct2, ok2 := c2[i].(Tag)
|
||||||
|
if ok1 != ok2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok1 && !Eq(ct1, ct2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c1[i] != c2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func eq(t ...Tag) bool {
|
||||||
|
if len(t) < 2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !eq2(t[0], t[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return eq(t[1:]...)
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module code.squareroundforest.org/arpio/html
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 // indirect
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
|
||||||
|
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
||||||
220
lib.go
Normal file
220
lib.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// Package html provides functions for programmatically composing and rendering HTML.
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// when composing html, the Attr convenience function is recommended to construct input attributes
|
||||||
|
type Attributes map[string]string
|
||||||
|
|
||||||
|
// immutable
|
||||||
|
// calling creates a new copy with the passed in attributes and child nodes applied only to the copy
|
||||||
|
// input parameters
|
||||||
|
// rendering of child nodes
|
||||||
|
// instances of tags can be used to create further tags with extended set of attributes and child tags
|
||||||
|
// builtin tags in the tags sub-package
|
||||||
|
// custom tags are supported via the NewTag constructor. Functions with the same signature, but created by other
|
||||||
|
// means, will not be rendered, unless they return a tag created by NewTag() or a builtin tag
|
||||||
|
type Tag func(...any) Tag
|
||||||
|
|
||||||
|
type Template[Data any] func(Data) Tag
|
||||||
|
|
||||||
|
// convenience function primarily aimed to help with construction of html with tags
|
||||||
|
// the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
|
||||||
|
func Attr(a ...any) Attributes {
|
||||||
|
if len(a)%2 != 0 {
|
||||||
|
a = append(a, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
am := make(Attributes)
|
||||||
|
for i := 0; i < len(a); i += 2 {
|
||||||
|
am[fmt.Sprint(a[i])] = fmt.Sprint(a[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return am
|
||||||
|
}
|
||||||
|
|
||||||
|
// defines a new tag with name and initial attributes and child nodes
|
||||||
|
func NewTag(name string, children ...any) Tag {
|
||||||
|
if handleQuery(name, children) {
|
||||||
|
children = children[:len(children)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(children1 ...any) Tag {
|
||||||
|
if name == "br" {
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewTag(name, append(children, children1...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the name of a tag
|
||||||
|
func Name(t Tag) string {
|
||||||
|
q := nameQuery{}
|
||||||
|
t()(&q)
|
||||||
|
return q.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns all attributes of a tag
|
||||||
|
func AllAttributes(t Tag) Attributes {
|
||||||
|
q := attributesQuery{}
|
||||||
|
t()(&q)
|
||||||
|
a := make(Attributes)
|
||||||
|
for name, value := range q.value {
|
||||||
|
a[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the value of a named attribute if exists, empty string otherwise
|
||||||
|
func Attribute(t Tag, name string) string {
|
||||||
|
q := attributeQuery{name: name}
|
||||||
|
t()(&q)
|
||||||
|
return q.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new tag with all the existing attributes and child nodes of the input tag, and the new attribute value
|
||||||
|
func SetAttribute(t Tag, name string, value any) Tag {
|
||||||
|
return t()(Attr(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new tag with all the existing attributes and child nodes of the input tag, except the attribute to
|
||||||
|
// be deleted
|
||||||
|
func DeleteAttribute(t Tag, name string) Tag {
|
||||||
|
n := Name(t)
|
||||||
|
a := AllAttributes(t)
|
||||||
|
c := Children(t)
|
||||||
|
delete(a, name)
|
||||||
|
return NewTag(n, append(c, a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the same as Attribute(t, "class")
|
||||||
|
func Class(t Tag) string {
|
||||||
|
return Attribute(t, "class")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the same as SetAttribute(t, "class", class)
|
||||||
|
func SetClass(t Tag, class string) Tag {
|
||||||
|
return SetAttribute(t, "class", class)
|
||||||
|
}
|
||||||
|
|
||||||
|
// like SetClass, but it appends the new class to the existing classes, regardless if the same class exists
|
||||||
|
func AddClass(t Tag, class string) Tag {
|
||||||
|
current := Class(t)
|
||||||
|
if current != "" {
|
||||||
|
class = fmt.Sprintf("%s %s", current, class)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetClass(t, class)
|
||||||
|
}
|
||||||
|
|
||||||
|
// like DeleteAttribute, but it only deletes the specified class from the class attribute
|
||||||
|
func DeleteClass(t Tag, class string) Tag {
|
||||||
|
c := Class(t)
|
||||||
|
cc := strings.Split(c, " ")
|
||||||
|
|
||||||
|
var ccc []string
|
||||||
|
for _, ci := range cc {
|
||||||
|
if ci != class {
|
||||||
|
ccc = append(ccc, ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetClass(t, strings.Join(ccc, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the child nodes of a tag
|
||||||
|
func Children(t Tag) []any {
|
||||||
|
var q childrenQuery
|
||||||
|
t()(&q)
|
||||||
|
c := make([]any, len(q.value))
|
||||||
|
copy(c, q.value)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders html with t as the root node with indentation
|
||||||
|
// child nodes are rendered via fmt.Sprint, tolerating fmt.Stringer implementations
|
||||||
|
// consecutive spaces are considered to be so on purpose, and are converted into
|
||||||
|
// spaces around tags can behave different from when using unindented rendering
|
||||||
|
// as a last resort, one can use rendered html inside a verbatim tag
|
||||||
|
func RenderIndent(out io.Writer, indent string, pwidth int, t Tag) error {
|
||||||
|
r := renderer{out: out, indent: indent, pwidth: pwidth}
|
||||||
|
t()(&r)
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders html with t as the root node without indentation
|
||||||
|
func Render(out io.Writer, t Tag) error {
|
||||||
|
return RenderIndent(out, "", 0, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new tag from t marking it verbatim. The content of verbatim tags is rendered without HTML escaping.
|
||||||
|
// This may cause security issues when using it in an incosiderate way. The tag can contain non-tag child nodes.
|
||||||
|
// Verbatim content gets indented when rendering with indentation
|
||||||
|
func Verbatim(t Tag) Tag {
|
||||||
|
return t()(renderGuide{verbatim: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// marks a tag as script-style content for rendering. Script-style content is not escaped and not indented
|
||||||
|
func ScriptContent(t Tag) Tag {
|
||||||
|
return t()(renderGuide{script: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// inline tags are not broken into separate lines when rendering with indentation
|
||||||
|
// deprecated in HTML, but only used for indentation
|
||||||
|
func Inline(t Tag) Tag {
|
||||||
|
return t()(renderGuide{inline: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// void tags do not accept child nodes
|
||||||
|
// deprecated in HTML, but only used for indentation
|
||||||
|
func Void(t Tag) Tag {
|
||||||
|
return t()(renderGuide{void: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// same name, same attributes, same child tags, child nodes in the same order and equal by reference or value
|
||||||
|
// depending on the child node type
|
||||||
|
func Eq(t ...Tag) bool {
|
||||||
|
tt := make([]Tag, len(t))
|
||||||
|
for i := range t {
|
||||||
|
tt[i] = t[i]()
|
||||||
|
}
|
||||||
|
|
||||||
|
return eq(tt...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns a template into a tag for composition
|
||||||
|
func FromTemplate[Data any](f Template[Data]) Tag {
|
||||||
|
return func(a ...any) Tag {
|
||||||
|
var (
|
||||||
|
t Data
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
t, ok = a[0].(Data)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
a = append(a[:i], a[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(t)(a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the functional programming sense
|
||||||
|
func Map(data []any, tag Tag) []Tag {
|
||||||
|
var tags []Tag
|
||||||
|
for _, d := range data {
|
||||||
|
tags = append(tags, tag(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
101
lib_test.go
Normal file
101
lib_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package html_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.squareroundforest.org/arpio/html"
|
||||||
|
. "code.squareroundforest.org/arpio/html/tags"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLib(t *testing.T) {
|
||||||
|
t.Run("templated tag", func(t *testing.T) {
|
||||||
|
type (
|
||||||
|
member struct {
|
||||||
|
name string
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
|
||||||
|
team struct {
|
||||||
|
name string
|
||||||
|
rank int
|
||||||
|
members []member
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
memberHTML := html.FromTemplate(
|
||||||
|
func(m member) Tag {
|
||||||
|
return Li(
|
||||||
|
Div("Name: ", m.name),
|
||||||
|
Div("Level: ", m.level),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
teamHTML := html.FromTemplate(
|
||||||
|
func(t team) Tag {
|
||||||
|
return Div(
|
||||||
|
H3(t.name),
|
||||||
|
P("Rank: ", t.rank),
|
||||||
|
Ul(html.Map(t.members, memberHTML)...),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
myTeam := team{
|
||||||
|
name: "Foo",
|
||||||
|
rank: 3,
|
||||||
|
members: []member{{
|
||||||
|
name: "Bar",
|
||||||
|
level: 4,
|
||||||
|
}, {
|
||||||
|
name: "Baz",
|
||||||
|
level: 1,
|
||||||
|
}, {
|
||||||
|
name: "Qux",
|
||||||
|
level: 4,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if html.RenderIndent(&b, "\t", 0, teamHTML(myTeam)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != `<div>
|
||||||
|
<h3>
|
||||||
|
Foo
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Rank: 3
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Name: Bar
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Level: 4
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Name: Baz
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Level: 1
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Name: Qux
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Level: 4
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
5
notes.txt
Normal file
5
notes.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
rendering types:
|
||||||
|
like <br>: inline void
|
||||||
|
inline <hr>: block void
|
||||||
|
like script: no escaping
|
||||||
|
split the validation from the rendering
|
||||||
2
promote-to-tags.txt
Normal file
2
promote-to-tags.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Attr
|
||||||
|
NewTag
|
||||||
129
query.go
Normal file
129
query.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
type nameQuery struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type attributesQuery struct {
|
||||||
|
value Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type attributeQuery struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
found bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type childrenQuery struct {
|
||||||
|
value []any
|
||||||
|
}
|
||||||
|
|
||||||
|
type validator struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderGuidesQuery struct {
|
||||||
|
value []renderGuide
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupChildren(c []any) ([]Attributes, []any, []renderGuide) {
|
||||||
|
var (
|
||||||
|
a []Attributes
|
||||||
|
cc []any
|
||||||
|
rg []renderGuide
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, ci := range c {
|
||||||
|
if ai, ok := ci.(Attributes); ok {
|
||||||
|
a = append(a, ai)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rgi, ok := ci.(renderGuide); ok {
|
||||||
|
rg = append(rg, rgi)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cc = append(cc, ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, cc, rg
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeAttributes(c []any) Attributes {
|
||||||
|
a, _, _ := groupChildren(c)
|
||||||
|
if len(a) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
to := make(Attributes)
|
||||||
|
for _, ai := range a {
|
||||||
|
for name, value := range ai {
|
||||||
|
to[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return to
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAttribute(c []any, name string) (string, bool) {
|
||||||
|
a, _, _ := groupChildren(c)
|
||||||
|
for i := len(a) - 1; i >= 0; i-- {
|
||||||
|
value, ok := a[i][name]
|
||||||
|
if ok {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleQuery(name string, children []any) bool {
|
||||||
|
if len(children) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
last := len(children) - 1
|
||||||
|
lastChild := children[last]
|
||||||
|
if q, ok := lastChild.(*nameQuery); ok {
|
||||||
|
q.value = name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if q, ok := lastChild.(*attributesQuery); ok {
|
||||||
|
q.value = mergeAttributes(children[:last])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if q, ok := lastChild.(*attributeQuery); ok {
|
||||||
|
q.value, q.found = findAttribute(children[:last], q.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if q, ok := lastChild.(*childrenQuery); ok {
|
||||||
|
_, q.value, _ = groupChildren(children[:last])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if q, ok := lastChild.(*renderGuidesQuery); ok {
|
||||||
|
_, _, q.value = groupChildren(children[:last])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := lastChild.(*validator); ok {
|
||||||
|
v.err = validate(name, children[:last])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, ok := lastChild.(*renderer); ok {
|
||||||
|
if err := validate(name, children[:last]); err != nil {
|
||||||
|
r.err = err
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
render(r, name, children[:last])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
150
query_test.go
Normal file
150
query_test.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package html_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"code.squareroundforest.org/arpio/html"
|
||||||
|
. "code.squareroundforest.org/arpio/html/tags"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuery(t *testing.T) {
|
||||||
|
t.Run("group children", func(t *testing.T) {
|
||||||
|
inlineDiv := html.Inline(Div(Attr("foo", "bar"), "baz"))
|
||||||
|
attr := html.AllAttributes(inlineDiv)
|
||||||
|
if len(attr) != 1 || attr["foo"] != "bar" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
c := html.Children(inlineDiv)
|
||||||
|
if len(c) != 1 || c[0] != "baz" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, inlineDiv); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := b.String()
|
||||||
|
if h != `<div foo="bar">baz</div>` {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("merge attributes", func(t *testing.T) {
|
||||||
|
t.Run("has attributes", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar"))
|
||||||
|
attr := html.AllAttributes(div)
|
||||||
|
if len(attr) != 1 || attr["foo"] != "bar" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no attributes", func(t *testing.T) {
|
||||||
|
div := Div()
|
||||||
|
attr := html.AllAttributes(div)
|
||||||
|
if len(attr) != 0 {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find attributes", func(t *testing.T) {
|
||||||
|
t.Run("exists", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar"))
|
||||||
|
if html.Attribute(div, "foo") != "bar" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not exist", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar"))
|
||||||
|
if html.Attribute(div, "qux") != "" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handle query", func(t *testing.T) {
|
||||||
|
t.Run("no chlidren", func(t *testing.T) {
|
||||||
|
div := Div()
|
||||||
|
div2 := div()
|
||||||
|
if html.Name(div2) != "div" || !html.Eq(div, div2) {
|
||||||
|
t.Fatal(html.Name(div2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("name", func(t *testing.T) {
|
||||||
|
div := Div()
|
||||||
|
if html.Name(div) != "div" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all attributes", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar", "baz", "qux"))
|
||||||
|
attr := html.AllAttributes(div)
|
||||||
|
if len(attr) != 2 || attr["foo"] != "bar" || attr["baz"] != "qux" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("one attribute", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar", "baz", "qux"))
|
||||||
|
foo := html.Attribute(div, "foo")
|
||||||
|
if foo != "bar" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("children", func(t *testing.T) {
|
||||||
|
div := Div("foo", "bar", "baz")
|
||||||
|
c := html.Children(div)
|
||||||
|
if len(c) != 3 || c[0] != "foo" || c[1] != "bar" || c[2] != "baz" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("render guides", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "\t", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<span>foo</span>\n</div>\n" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("validate and render", func(t *testing.T) {
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
script := Script(`function() { return "Hello, world!" }`)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, script); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo+", "bar"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid child", func(t *testing.T) {
|
||||||
|
div := Div(Div(Attr("foo+", "bar")))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
250
render.go
Normal file
250
render.go
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPWidth = 112
|
||||||
|
|
||||||
|
type renderGuide struct {
|
||||||
|
inline bool
|
||||||
|
void bool
|
||||||
|
script bool
|
||||||
|
verbatim bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderer struct {
|
||||||
|
out io.Writer
|
||||||
|
indent string
|
||||||
|
pwidth int
|
||||||
|
currentIndent string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeRenderingGuides(rgs []renderGuide) renderGuide {
|
||||||
|
var rg renderGuide
|
||||||
|
for _, rgi := range rgs {
|
||||||
|
rg.inline = rg.inline || rgi.inline
|
||||||
|
rg.void = rg.void || rgi.void
|
||||||
|
rg.script = rg.script || rgi.script
|
||||||
|
rg.verbatim = rg.verbatim || rgi.verbatim
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ' ', 0xA0:
|
||||||
|
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] == ' ' || r[i] == 0xA0
|
||||||
|
wsStart = ws && !lastWS
|
||||||
|
lastWS = ws
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(r *renderer, name string, children []any) {
|
||||||
|
if r.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printf := 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a, c, rgs := groupChildren(children)
|
||||||
|
rg := mergeRenderingGuides(rgs)
|
||||||
|
printf(r.currentIndent)
|
||||||
|
printf("<%s", name)
|
||||||
|
for _, ai := range a {
|
||||||
|
for name, value := range ai {
|
||||||
|
printf(" %s=\"%s\"", name, attributeEscape(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(">")
|
||||||
|
if r.indent != "" && !rg.inline && len(c) > 0 {
|
||||||
|
printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rg.void {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var inlineBuffer *bytes.Buffer
|
||||||
|
if r.indent != "" {
|
||||||
|
inlineBuffer = bytes.NewBuffer(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if tag, ok := ci.(Tag); ok {
|
||||||
|
if rg.inline {
|
||||||
|
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, "")
|
||||||
|
println(inlineBuffer.String())
|
||||||
|
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)
|
||||||
|
println(inlineBuffer.String())
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
172
render_test.go
Normal file
172
render_test.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package html_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"code.squareroundforest.org/arpio/html"
|
||||||
|
. "code.squareroundforest.org/arpio/html/tags"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type failingWriter struct {
|
||||||
|
after int
|
||||||
|
}
|
||||||
|
|
||||||
|
func failWriteAfter(n int) *failingWriter {
|
||||||
|
return &failingWriter{n}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *failingWriter) Write(p []byte) (int, error) {
|
||||||
|
w.after -= len(p)
|
||||||
|
if w.after < 0 {
|
||||||
|
return len(p) + w.after, errors.New("test error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
|
t.Run("merge render guides", func(t *testing.T) {
|
||||||
|
foo := html.Inline(html.Verbatim(NewTag("foo")))
|
||||||
|
foo = foo("<bar><baz></bar>")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "\t", 0, foo); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<foo><bar><baz></bar></foo>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attribute escaping", func(t *testing.T) {
|
||||||
|
span := Span(Attr("foo", "bar=\"&\""))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<span foo=\"bar="&"\"></span>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("html escape", func(t *testing.T) {
|
||||||
|
t.Run("basic escape", func(t *testing.T) {
|
||||||
|
span := Span("<foo>bar&baz</foo>")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<span><foo>bar&baz</foo></span>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("consecutive spaces", func(t *testing.T) {
|
||||||
|
span := Span("consecutive spaces: \" \"")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<span>consecutive spaces: \" \"</span>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("write error", func(t *testing.T) {
|
||||||
|
t.Run("fail immediately", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo"))
|
||||||
|
w := failWriteAfter(0)
|
||||||
|
if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test error") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fail in tag", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo"))
|
||||||
|
w := failWriteAfter(6)
|
||||||
|
if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test error") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("partial text children", func(t *testing.T) {
|
||||||
|
div := Div("foo", Div("bar"), "baz")
|
||||||
|
w := failWriteAfter(5)
|
||||||
|
if err := html.RenderIndent(w, "\t", 0, div); err == nil || !strings.Contains(err.Error(), "test error") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("text children", func(t *testing.T) {
|
||||||
|
div := Div("foo", "bar", "baz")
|
||||||
|
w := failWriteAfter(5)
|
||||||
|
if err := html.RenderIndent(w, "\t", 0, div); err == nil || !strings.Contains(err.Error(), "test error") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("indent", func(t *testing.T) {
|
||||||
|
t.Run("simple tag", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "\t", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<span>foo</span>\n</div>\n" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty tag", func(t *testing.T) {
|
||||||
|
div := Div(Br())
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "\t", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<br>\n</div>\n" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline fragment between blocks", func(t *testing.T) {
|
||||||
|
div := Div("foo bar baz", Div("qux quux"), "corge")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "\t", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\tfoo bar baz\n\t<div>\n\t\tqux quux\n\t</div>\n\tcorge\n</div>\n" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("block inside inline", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo bar baz", Div("qux quux"), "corge"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, "XYZ", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
62
script/generate-tags.go
Normal file
62
script/generate-tags.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func splitAllBy(s string, ss ...string) []string {
|
||||||
|
var sss []string
|
||||||
|
for _, si := range ss {
|
||||||
|
sis := strings.Split(si, s)
|
||||||
|
for i := range sis {
|
||||||
|
sis[i] = strings.TrimSpace(sis[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sisi := range sis {
|
||||||
|
if sisi != "" {
|
||||||
|
sss = append(sss, sisi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sss
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(b)
|
||||||
|
ss := splitAllBy("\n", s)
|
||||||
|
ss = splitAllBy(",", ss...)
|
||||||
|
ss = splitAllBy(" ", ss...)
|
||||||
|
printf := func(f string, a ...any) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(os.Stdout, f, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("// generated by ../script/generate-tags.go\n")
|
||||||
|
printf("\n")
|
||||||
|
printf("package tags\n")
|
||||||
|
printf("import \"code.squareroundforest.org/arpio/html\"\n")
|
||||||
|
for _, si := range ss {
|
||||||
|
exp := fmt.Sprintf("html.NewTag(\"%s\")", si)
|
||||||
|
for _, a := range os.Args[1:] {
|
||||||
|
exp = fmt.Sprintf("html.%s(%s)", a, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
rname := []rune(si)
|
||||||
|
rname[0] = unicode.ToUpper(rname[0])
|
||||||
|
printf("var %s = %s\n", string(rname), exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
script/promote-to-tags.go
Normal file
54
script/promote-to-tags.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func splitAllBy(s string, ss ...string) []string {
|
||||||
|
var sss []string
|
||||||
|
for _, si := range ss {
|
||||||
|
sis := strings.Split(si, s)
|
||||||
|
for i := range sis {
|
||||||
|
sis[i] = strings.TrimSpace(sis[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sisi := range sis {
|
||||||
|
if sisi != "" {
|
||||||
|
sss = append(sss, sisi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sss
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(b)
|
||||||
|
ss := splitAllBy("\n", s)
|
||||||
|
ss = splitAllBy(",", ss...)
|
||||||
|
ss = splitAllBy(" ", ss...)
|
||||||
|
printf := func(f string, a ...any) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(os.Stdout, f, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("// generated by ../script/generate-tags.go\n")
|
||||||
|
printf("\n")
|
||||||
|
printf("package tags\n")
|
||||||
|
printf("import \"code.squareroundforest.org/arpio/html\"\n")
|
||||||
|
for _, si := range ss {
|
||||||
|
printf("var %s = html.%s\n", si, si)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
tags.block.txt
Normal file
58
tags.block.txt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
address
|
||||||
|
article
|
||||||
|
audio
|
||||||
|
aside
|
||||||
|
blockquote
|
||||||
|
body
|
||||||
|
canvas
|
||||||
|
caption
|
||||||
|
center
|
||||||
|
col
|
||||||
|
colgroup
|
||||||
|
datalist
|
||||||
|
dd
|
||||||
|
del
|
||||||
|
details
|
||||||
|
dialog
|
||||||
|
div
|
||||||
|
dl
|
||||||
|
dt
|
||||||
|
fieldset
|
||||||
|
figcaption
|
||||||
|
figure
|
||||||
|
footer
|
||||||
|
form
|
||||||
|
head
|
||||||
|
header
|
||||||
|
hgroup
|
||||||
|
html
|
||||||
|
ins
|
||||||
|
li
|
||||||
|
link
|
||||||
|
main
|
||||||
|
map
|
||||||
|
math
|
||||||
|
menu
|
||||||
|
nav
|
||||||
|
noscript
|
||||||
|
ol
|
||||||
|
optgroup
|
||||||
|
p
|
||||||
|
picture
|
||||||
|
pre
|
||||||
|
rp
|
||||||
|
search
|
||||||
|
section
|
||||||
|
summary
|
||||||
|
table
|
||||||
|
tbody
|
||||||
|
td
|
||||||
|
template
|
||||||
|
textarea
|
||||||
|
tfoot
|
||||||
|
th
|
||||||
|
thead
|
||||||
|
title
|
||||||
|
tr
|
||||||
|
ul
|
||||||
|
video
|
||||||
39
tags.inline.txt
Normal file
39
tags.inline.txt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
a
|
||||||
|
abbr
|
||||||
|
b
|
||||||
|
bdi
|
||||||
|
bdo
|
||||||
|
button
|
||||||
|
cite
|
||||||
|
code
|
||||||
|
data
|
||||||
|
dfn
|
||||||
|
em
|
||||||
|
h1, h2, h3, h4, h5, h6
|
||||||
|
i
|
||||||
|
kbd
|
||||||
|
label
|
||||||
|
legend
|
||||||
|
mark
|
||||||
|
meter
|
||||||
|
object
|
||||||
|
option
|
||||||
|
output
|
||||||
|
progress
|
||||||
|
q
|
||||||
|
rt
|
||||||
|
ruby
|
||||||
|
s
|
||||||
|
samp
|
||||||
|
select
|
||||||
|
selectedcontent
|
||||||
|
slot
|
||||||
|
small
|
||||||
|
span
|
||||||
|
strong
|
||||||
|
sub
|
||||||
|
sup
|
||||||
|
svg
|
||||||
|
time
|
||||||
|
u
|
||||||
|
var
|
||||||
2
tags.script.txt
Normal file
2
tags.script.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
script
|
||||||
|
style
|
||||||
7
tags.void.block.txt
Normal file
7
tags.void.block.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
area
|
||||||
|
base
|
||||||
|
hr
|
||||||
|
iframe
|
||||||
|
meta
|
||||||
|
source
|
||||||
|
track
|
||||||
5
tags.void.inline.txt
Normal file
5
tags.void.inline.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
br
|
||||||
|
embed
|
||||||
|
img
|
||||||
|
input
|
||||||
|
wbr
|
||||||
62
tags/block.gen.go
Normal file
62
tags/block.gen.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var Address = html.NewTag("address")
|
||||||
|
var Article = html.NewTag("article")
|
||||||
|
var Audio = html.NewTag("audio")
|
||||||
|
var Aside = html.NewTag("aside")
|
||||||
|
var Blockquote = html.NewTag("blockquote")
|
||||||
|
var Body = html.NewTag("body")
|
||||||
|
var Canvas = html.NewTag("canvas")
|
||||||
|
var Caption = html.NewTag("caption")
|
||||||
|
var Center = html.NewTag("center")
|
||||||
|
var Col = html.NewTag("col")
|
||||||
|
var Colgroup = html.NewTag("colgroup")
|
||||||
|
var Datalist = html.NewTag("datalist")
|
||||||
|
var Dd = html.NewTag("dd")
|
||||||
|
var Del = html.NewTag("del")
|
||||||
|
var Details = html.NewTag("details")
|
||||||
|
var Dialog = html.NewTag("dialog")
|
||||||
|
var Div = html.NewTag("div")
|
||||||
|
var Dl = html.NewTag("dl")
|
||||||
|
var Dt = html.NewTag("dt")
|
||||||
|
var Fieldset = html.NewTag("fieldset")
|
||||||
|
var Figcaption = html.NewTag("figcaption")
|
||||||
|
var Figure = html.NewTag("figure")
|
||||||
|
var Footer = html.NewTag("footer")
|
||||||
|
var Form = html.NewTag("form")
|
||||||
|
var Head = html.NewTag("head")
|
||||||
|
var Header = html.NewTag("header")
|
||||||
|
var Hgroup = html.NewTag("hgroup")
|
||||||
|
var Html = html.NewTag("html")
|
||||||
|
var Ins = html.NewTag("ins")
|
||||||
|
var Li = html.NewTag("li")
|
||||||
|
var Link = html.NewTag("link")
|
||||||
|
var Main = html.NewTag("main")
|
||||||
|
var Map = html.NewTag("map")
|
||||||
|
var Math = html.NewTag("math")
|
||||||
|
var Menu = html.NewTag("menu")
|
||||||
|
var Nav = html.NewTag("nav")
|
||||||
|
var Noscript = html.NewTag("noscript")
|
||||||
|
var Ol = html.NewTag("ol")
|
||||||
|
var Optgroup = html.NewTag("optgroup")
|
||||||
|
var P = html.NewTag("p")
|
||||||
|
var Picture = html.NewTag("picture")
|
||||||
|
var Pre = html.NewTag("pre")
|
||||||
|
var Rp = html.NewTag("rp")
|
||||||
|
var Search = html.NewTag("search")
|
||||||
|
var Section = html.NewTag("section")
|
||||||
|
var Summary = html.NewTag("summary")
|
||||||
|
var Table = html.NewTag("table")
|
||||||
|
var Tbody = html.NewTag("tbody")
|
||||||
|
var Td = html.NewTag("td")
|
||||||
|
var Template = html.NewTag("template")
|
||||||
|
var Textarea = html.NewTag("textarea")
|
||||||
|
var Tfoot = html.NewTag("tfoot")
|
||||||
|
var Th = html.NewTag("th")
|
||||||
|
var Thead = html.NewTag("thead")
|
||||||
|
var Title = html.NewTag("title")
|
||||||
|
var Tr = html.NewTag("tr")
|
||||||
|
var Ul = html.NewTag("ul")
|
||||||
|
var Video = html.NewTag("video")
|
||||||
48
tags/inline.gen.go
Normal file
48
tags/inline.gen.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var A = html.Inline(html.NewTag("a"))
|
||||||
|
var Abbr = html.Inline(html.NewTag("abbr"))
|
||||||
|
var B = html.Inline(html.NewTag("b"))
|
||||||
|
var Bdi = html.Inline(html.NewTag("bdi"))
|
||||||
|
var Bdo = html.Inline(html.NewTag("bdo"))
|
||||||
|
var Button = html.Inline(html.NewTag("button"))
|
||||||
|
var Cite = html.Inline(html.NewTag("cite"))
|
||||||
|
var Code = html.Inline(html.NewTag("code"))
|
||||||
|
var Data = html.Inline(html.NewTag("data"))
|
||||||
|
var Dfn = html.Inline(html.NewTag("dfn"))
|
||||||
|
var Em = html.Inline(html.NewTag("em"))
|
||||||
|
var H1 = html.Inline(html.NewTag("h1"))
|
||||||
|
var H2 = html.Inline(html.NewTag("h2"))
|
||||||
|
var H3 = html.Inline(html.NewTag("h3"))
|
||||||
|
var H4 = html.Inline(html.NewTag("h4"))
|
||||||
|
var H5 = html.Inline(html.NewTag("h5"))
|
||||||
|
var H6 = html.Inline(html.NewTag("h6"))
|
||||||
|
var I = html.Inline(html.NewTag("i"))
|
||||||
|
var Kbd = html.Inline(html.NewTag("kbd"))
|
||||||
|
var Label = html.Inline(html.NewTag("label"))
|
||||||
|
var Legend = html.Inline(html.NewTag("legend"))
|
||||||
|
var Mark = html.Inline(html.NewTag("mark"))
|
||||||
|
var Meter = html.Inline(html.NewTag("meter"))
|
||||||
|
var Object = html.Inline(html.NewTag("object"))
|
||||||
|
var Option = html.Inline(html.NewTag("option"))
|
||||||
|
var Output = html.Inline(html.NewTag("output"))
|
||||||
|
var Progress = html.Inline(html.NewTag("progress"))
|
||||||
|
var Q = html.Inline(html.NewTag("q"))
|
||||||
|
var Rt = html.Inline(html.NewTag("rt"))
|
||||||
|
var Ruby = html.Inline(html.NewTag("ruby"))
|
||||||
|
var S = html.Inline(html.NewTag("s"))
|
||||||
|
var Samp = html.Inline(html.NewTag("samp"))
|
||||||
|
var Select = html.Inline(html.NewTag("select"))
|
||||||
|
var Selectedcontent = html.Inline(html.NewTag("selectedcontent"))
|
||||||
|
var Slot = html.Inline(html.NewTag("slot"))
|
||||||
|
var Small = html.Inline(html.NewTag("small"))
|
||||||
|
var Span = html.Inline(html.NewTag("span"))
|
||||||
|
var Strong = html.Inline(html.NewTag("strong"))
|
||||||
|
var Sub = html.Inline(html.NewTag("sub"))
|
||||||
|
var Sup = html.Inline(html.NewTag("sup"))
|
||||||
|
var Svg = html.Inline(html.NewTag("svg"))
|
||||||
|
var Time = html.Inline(html.NewTag("time"))
|
||||||
|
var U = html.Inline(html.NewTag("u"))
|
||||||
|
var Var = html.Inline(html.NewTag("var"))
|
||||||
6
tags/promote.gen.go
Normal file
6
tags/promote.gen.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var Attr = html.Attr
|
||||||
|
var NewTag = html.NewTag
|
||||||
6
tags/script.gen.go
Normal file
6
tags/script.gen.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var Script = html.ScriptContent(html.NewTag("script"))
|
||||||
|
var Style = html.ScriptContent(html.NewTag("style"))
|
||||||
11
tags/void.block.gen.go
Normal file
11
tags/void.block.gen.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var Area = html.Void(html.NewTag("area"))
|
||||||
|
var Base = html.Void(html.NewTag("base"))
|
||||||
|
var Hr = html.Void(html.NewTag("hr"))
|
||||||
|
var Iframe = html.Void(html.NewTag("iframe"))
|
||||||
|
var Meta = html.Void(html.NewTag("meta"))
|
||||||
|
var Source = html.Void(html.NewTag("source"))
|
||||||
|
var Track = html.Void(html.NewTag("track"))
|
||||||
9
tags/void.inline.gen.go
Normal file
9
tags/void.inline.gen.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// generated by ../script/generate-tags.go
|
||||||
|
|
||||||
|
package tags
|
||||||
|
import "code.squareroundforest.org/arpio/html"
|
||||||
|
var Br = html.Inline(html.Void(html.NewTag("br")))
|
||||||
|
var Embed = html.Inline(html.Void(html.NewTag("embed")))
|
||||||
|
var Img = html.Inline(html.Void(html.NewTag("img")))
|
||||||
|
var Input = html.Inline(html.Void(html.NewTag("input")))
|
||||||
|
var Wbr = html.Inline(html.Void(html.NewTag("wbr")))
|
||||||
66
validate.go
Normal file
66
validate.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
symbolExp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_.]*$`)
|
||||||
|
scriptTagExp = regexp.MustCompile(`<\s*/?\s*[sS][cC][rR][iI][pP][tT]([^a-zA-Z0-9]+|$)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateSymbol(s string) error {
|
||||||
|
if !symbolExp.MatchString(s) {
|
||||||
|
return errors.New("invalid symbol")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTagName(name string) error {
|
||||||
|
return validateSymbol(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAttributeName(name string) error {
|
||||||
|
return validateSymbol(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(name string, children []any) error {
|
||||||
|
if err := validateTagName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a, c, rgs := groupChildren(children)
|
||||||
|
rg := mergeRenderingGuides(rgs)
|
||||||
|
if rg.void && len(c) > 0 {
|
||||||
|
return fmt.Errorf("tag %s is void but it has children", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ai := range a {
|
||||||
|
for name := range ai {
|
||||||
|
if err := validateAttributeName(name); err != nil {
|
||||||
|
return fmt.Errorf("tag %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ci := range c {
|
||||||
|
if tag, ok := ci.(Tag); ok {
|
||||||
|
if rg.verbatim || rg.script {
|
||||||
|
return fmt.Errorf("tag %s does not allow child elements", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var v validator
|
||||||
|
tag(&v)
|
||||||
|
if v.err != nil {
|
||||||
|
return v.err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
111
validate_test.go
Normal file
111
validate_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package html_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"code.squareroundforest.org/arpio/html"
|
||||||
|
. "code.squareroundforest.org/arpio/html/tags"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
t.Run("symbol", func(t *testing.T) {
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
mytag := html.NewTag("foo+bar")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, mytag); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid with allowed chars number", func(t *testing.T) {
|
||||||
|
mytag := html.NewTag("0foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, mytag); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid with allowed chars delimiter", func(t *testing.T) {
|
||||||
|
mytag := html.NewTag("-foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, mytag); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
mytag := html.NewTag("foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, mytag); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid with special chars", func(t *testing.T) {
|
||||||
|
mytag := html.NewTag("foo-bar-1")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, mytag); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid attribute name", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo+", "bar"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("void tag with children", func(t *testing.T) {
|
||||||
|
br := Br("foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, br); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verbatim with child tag", func(t *testing.T) {
|
||||||
|
div := html.Verbatim(Div(Br()))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("script with child tag", func(t *testing.T) {
|
||||||
|
script := Script(Br())
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, script); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid child tag", func(t *testing.T) {
|
||||||
|
div := Div(Div(Attr("foo+", "bar")))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid child tag", func(t *testing.T) {
|
||||||
|
div := Div(Div(Attr("foo", "bar")))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err != nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
89
wrap.go
Normal file
89
wrap.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func words(buf *bytes.Buffer) []string {
|
||||||
|
var (
|
||||||
|
words []string
|
||||||
|
currentWord []rune
|
||||||
|
inTag bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
r, _, err := buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == unicode.ReplacementChar {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inTag && unicode.IsSpace(r) {
|
||||||
|
if len(currentWord) > 0 {
|
||||||
|
words, currentWord = append(words, string(currentWord)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWord = append(currentWord, r)
|
||||||
|
inTag = inTag && r != '>' || r == '<'
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentWord) > 0 {
|
||||||
|
words = append(words, string(currentWord))
|
||||||
|
}
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(buf *bytes.Buffer, pwidth int, indent string) *bytes.Buffer {
|
||||||
|
var (
|
||||||
|
lines [][]string
|
||||||
|
currentLine []string
|
||||||
|
currentLen int
|
||||||
|
)
|
||||||
|
|
||||||
|
words := words(buf)
|
||||||
|
for _, w := range words {
|
||||||
|
if currentLen != 0 {
|
||||||
|
currentLen++
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLen += len(w)
|
||||||
|
if currentLen > pwidth && len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = []string{w}
|
||||||
|
currentLen = len(w)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine = append(currentLine, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentLine) > 0 {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := bytes.NewBuffer(nil)
|
||||||
|
for i, l := range lines {
|
||||||
|
if i > 0 {
|
||||||
|
ret.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.WriteString(indent)
|
||||||
|
for j, w := range l {
|
||||||
|
if j > 0 {
|
||||||
|
ret.WriteRune(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.WriteString(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
76
wrap_test.go
Normal file
76
wrap_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package html_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"code.squareroundforest.org/arpio/html"
|
||||||
|
. "code.squareroundforest.org/arpio/html/tags"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, "\t", 0, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "<span>foo</span>" {
|
||||||
|
t.Fatal(buf.String(), buf.Len(), len("<span>foo</span>"), buf.Bytes(), []byte("<span>foo</span>"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple words", func(t *testing.T) {
|
||||||
|
span := Span("foo bar baz")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&buf, "\t", 2, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "<span>foo\nbar\nbaz</span>" {
|
||||||
|
t.Fatal(buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tag not split", func(t *testing.T) {
|
||||||
|
span := Span("foo ", Span("bar"), " baz")
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&buf, "\t", 2, span); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "<span>foo\n<span>bar</span>\nbaz</span>" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("normal text", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo bar baz qux quux corge"))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&buf, "\t", 9, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "<div>\n\t<span>foo\n\tbar baz\n\tqux quux\n\tcorge</span>\n</div>\n" {
|
||||||
|
t.Fatal(buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline space preserved", func(t *testing.T) {
|
||||||
|
div := Div(Span("foo"), " ", Span("bar"))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&buf, "\t", 0, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "<div>\n\t<span>foo</span> <span>bar</span>\n</div>\n" {
|
||||||
|
t.Fatal(buf.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user