Skip to content

Commit fc045df

Browse files
authored
Fix some XML formatting issues (e.g. RSS) (#6)
Also add .gotmpl to the extensions list when given a directory
1 parent d4459ac commit fc045df

8 files changed

Lines changed: 214 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> [!NOTE]
44
> Claude Code helped us get this code over the finish line, but all the tests are hand written (expcept for the fuzz tests, which are generated by Go's fuzzer), and all code is human inspected and understood.
55
6-
This is a Go HTML template formatter (we may add other formats in the future). The formatting is greatly inspired by [prettier-plugin-go-template](https://github.com/NiklasPor/prettier-plugin-go-template), but it will not be the same.
6+
This is a Go HTML template formatter[^1]. It is based one the [text/template/parse](https://pkg.go.dev/text/template/parse) package in Go 1.20.4 (see license note below). The formatting is greatly inspired by [prettier-plugin-go-template](https://github.com/NiklasPor/prettier-plugin-go-template), but it will not be the same.
77

88
* We focus on getting the overall structure right, and not on formatting details (which is often highly subjective).
99
* We use tabs and not spaces for indentation.
@@ -32,7 +32,7 @@ usage: gotmplfmt [flags] [path ...]
3232
-w write result to (source) file instead of stdout
3333
```
3434

35-
Without flags, `gotmplfmt` prints the formatted output to stdout. When given a directory, it processes all template files (`.html`, `.htm`, `.xml`, `.svg`, `.rss`, `.atom`, `.txt`) recursively. It also reads from stdin when no paths are given.
35+
Without flags, `gotmplfmt` prints the formatted output to stdout. When given a directory, it processes all template files (`.html`, `.htm`, `.xml`, `.svg`, `.rss`, `.atom`, `.gotmpl`, `.txt`) recursively. It also reads from stdin when no paths are given.
3636

3737
For the VS Code extension, see [here](vscode/README.md)
3838

@@ -95,3 +95,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
9595
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
9696
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9797
```
98+
99+
100+
[^1]: But it works pretty well for non-HTML Go templates, see the Golden tests.

internal/format/format_test.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"testing"
1111
)
1212

13-
// To update the golden files, set writeOutput to true and run `go test -update`.
13+
// To update the golden files, run `go test -update`.
1414
var (
1515
update = flag.Bool("update", false, "update the golden files")
1616
)
@@ -105,19 +105,24 @@ func tryParseGoTextTemplate(t *testing.T, text string) {
105105
return "test"
106106
}
107107
funcMap := template.FuncMap{
108+
"append": fn,
108109
"cond": fn,
109110
"css": fn,
111+
"default": fn,
112+
"diagrams": fn,
110113
"dict": fn,
114+
"errorf": fn,
115+
"fingerprint": fn,
116+
"first": fn,
111117
"hugo": fn,
112118
"js": fn,
119+
"reflect": fn,
113120
"resources": fn,
114-
"site": fn,
115-
"fingerprint": fn,
116121
"safeCSS": fn,
117-
"append": fn,
118-
"errorf": fn,
119-
"diagrams": fn,
120-
"default": fn,
122+
"safeHTML": fn,
123+
"site": fn,
124+
"transform": fn,
125+
"where": fn,
121126
}
122127

123128
_, err := template.New("").Funcs(funcMap).Parse(text)

internal/format/golden/in/rss.xml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{{- $authorEmail := "" }}
2+
{{- with site.Params.author }}
3+
{{- if reflect.IsMap . }}
4+
{{- with .email }}
5+
{{- $authorEmail = . }}
6+
{{- end }}
7+
{{- end }}
8+
{{- end }}
9+
10+
{{- $authorName := "" }}
11+
{{- with site.Params.author }}
12+
{{- if reflect.IsMap . }}
13+
{{- with .name }}
14+
{{- $authorName = . }}
15+
{{- end }}
16+
{{- else }}
17+
{{- $authorName = . }}
18+
{{- end }}
19+
{{- end }}
20+
21+
{{- $pctx := . }}
22+
{{- if .IsHome }}{{ $pctx = .Site }}{{ end }}
23+
{{- $pages := slice }}
24+
{{- if or $.IsHome $.IsSection }}
25+
{{- $pages = $pctx.RegularPages }}
26+
{{- else }}
27+
{{- $pages = $pctx.Pages }}
28+
{{- end }}
29+
{{- $limit := .Site.Config.Services.RSS.Limit }}
30+
{{- if ge $limit 1 }}
31+
{{- $pages = $pages | first $limit }}
32+
{{- end }}
33+
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML}}
34+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
35+
<channel>
36+
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{else }}{{ with .Title }}{{ .}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
37+
<link>{{ .Permalink }}</link>
38+
<description>Recent content {{if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title}}</description>
39+
<generator>Hugo</generator>
40+
<language>{{ site.Language.Locale }}</language>{{ with $authorEmail}}
41+
<managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{with $authorEmail}}
42+
<webMaster>{{ . }}{{with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
43+
<copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
44+
<lastBuildDate>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
45+
{{- with .OutputFormats.Get "RSS" }}
46+
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
47+
{{- end }}
48+
{{- range $pages}}
49+
<item>
50+
<title>{{ .Title }}</title>
51+
<link>{{ .Permalink }}</link>
52+
<pubDate>{{.PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" |safeHTML}}</pubDate>
53+
{{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
54+
<guid>{{ .Permalink }}</guid>
55+
<description>{{ .Summary | transform.XMLEscape | safeHTML}}</description>
56+
</item>
57+
{{- end }}
58+
</channel>
59+
</rss>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
2+
<urlset
3+
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
4+
xmlns:xhtml="http://www.w3.org/1999/xhtml">
5+
{{ range where .Pages "Sitemap.Disable" "ne" true }}
6+
{{- if .Permalink -}}
7+
<url>
8+
<loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
9+
<lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
10+
<changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
11+
<priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
12+
<xhtml:link
13+
rel="alternate"
14+
hreflang="{{ .Language.Locale }}"
15+
href="{{ .Permalink }}"
16+
/>{{ end }}
17+
<xhtml:link
18+
rel="alternate"
19+
hreflang="{{ .Language.Locale }}"
20+
href="{{ .Permalink }}"
21+
/>{{ end }}
22+
</url>
23+
{{- end -}}
24+
{{ end }}
25+
</urlset>

internal/format/golden/out/rss.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{{- $authorEmail := "" }}
2+
{{- with site.Params.author }}
3+
{{- if reflect.IsMap . }}
4+
{{- with .email }}
5+
{{- $authorEmail = . }}
6+
{{- end }}
7+
{{- end }}
8+
{{- end }}
9+
10+
{{- $authorName := "" }}
11+
{{- with site.Params.author }}
12+
{{- if reflect.IsMap . }}
13+
{{- with .name }}
14+
{{- $authorName = . }}
15+
{{- end }}
16+
{{- else }}
17+
{{- $authorName = . }}
18+
{{- end }}
19+
{{- end }}
20+
21+
{{- $pctx := . }}
22+
{{- if .IsHome }}{{ $pctx = .Site }}{{ end }}
23+
{{- $pages := slice }}
24+
{{- if or $.IsHome $.IsSection }}
25+
{{- $pages = $pctx.RegularPages }}
26+
{{- else }}
27+
{{- $pages = $pctx.Pages }}
28+
{{- end }}
29+
{{- $limit := .Site.Config.Services.RSS.Limit }}
30+
{{- if ge $limit 1 }}
31+
{{- $pages = $pages | first $limit }}
32+
{{- end }}
33+
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
34+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
35+
<channel>
36+
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }}</title>
37+
<link>{{ .Permalink }}</link>
38+
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
39+
<generator>Hugo</generator>
40+
<language>{{ site.Language.Locale }}</language>
41+
{{ with $authorEmail }}
42+
<managingEditor>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>
43+
{{ end }}
44+
{{ with $authorEmail }}
45+
<webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>
46+
{{ end }}
47+
{{ with .Site.Copyright }}
48+
<copyright>{{ . }}</copyright>
49+
{{ end }}
50+
{{ if not .Date.IsZero }}
51+
<lastBuildDate>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>
52+
{{ end }}
53+
{{- with .OutputFormats.Get "RSS" }}
54+
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
55+
{{- end }}
56+
{{- range $pages }}
57+
<item>
58+
<title>{{ .Title }}</title>
59+
<link>{{ .Permalink }}</link>
60+
<pubDate>{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
61+
{{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
62+
<guid>{{ .Permalink }}</guid>
63+
<description>{{ .Summary | transform.XMLEscape | safeHTML }}</description>
64+
</item>
65+
{{- end }}
66+
</channel>
67+
</rss>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
2+
<urlset
3+
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
4+
xmlns:xhtml="http://www.w3.org/1999/xhtml">
5+
{{ range where .Pages "Sitemap.Disable" "ne" true }}
6+
{{- if .Permalink -}}
7+
<url>
8+
<loc>{{ .Permalink }}</loc>
9+
{{ if not .Lastmod.IsZero }}
10+
<lastmod>{{ safeHTML (.Lastmod.Format "2006-01-02T15:04:05-07:00") }}</lastmod>
11+
{{ end }}
12+
{{ with .Sitemap.ChangeFreq }}
13+
<changefreq>{{ . }}</changefreq>
14+
{{ end }}
15+
{{ if ge .Sitemap.Priority 0.0 }}
16+
<priority>{{ .Sitemap.Priority }}</priority>
17+
{{ end }}
18+
{{ if .IsTranslated }}
19+
{{ range .Translations }}
20+
<xhtml:link
21+
rel="alternate"
22+
hreflang="{{ .Language.Locale }}"
23+
href="{{ .Permalink }}"
24+
/>
25+
{{ end }}
26+
<xhtml:link
27+
rel="alternate"
28+
hreflang="{{ .Language.Locale }}"
29+
href="{{ .Permalink }}"
30+
/>
31+
{{ end }}
32+
</url>
33+
{{- end -}}
34+
{{ end }}
35+
</urlset>

internal/parse/node.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,13 @@ func (p *printer) computeHTMLDeltas(text string) (pre, post int) {
316316
if tag.SelfClose || tag.IsVoid {
317317
// no depth change
318318
} else if tag.IsClose {
319+
if voidElements[tag.Name] {
320+
// XML contexts (RSS, Atom, SVG) can use HTML void names
321+
// like <link> as normal elements. We skip depth change on
322+
// the open (treated as void) so we must also skip on close
323+
// to keep indentation balanced.
324+
return
325+
}
319326
delta--
320327
if delta < minDelta {
321328
minDelta = delta
@@ -685,6 +692,9 @@ func (p *printer) splitHTMLLine(line string) []string {
685692
return
686693
}
687694
if tag.IsClose {
695+
if voidElements[tag.Name] {
696+
return
697+
}
688698
depth--
689699
} else {
690700
depth++

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func processReader(r io.Reader, w io.Writer) error {
118118
func isTemplateFile(path string) bool {
119119
ext := strings.ToLower(filepath.Ext(path))
120120
switch ext {
121-
case ".html", ".htm", ".xml", ".svg", ".rss", ".atom", ".txt":
121+
case ".html", ".htm", ".xml", ".svg", ".rss", ".atom", ".gotmpl", ".txt":
122122
return true
123123
}
124124
return false

0 commit comments

Comments
 (0)