From aac983b0e0e6d36f50f09daaaa0b020552a089dd Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Tue, 2 Feb 2021 20:49:31 +0100 Subject: [PATCH] tl8render: implement header/footer templates --- cmd/tl8-render/tl8render.go | 145 +++++++++++++++---------------- cmd/tl8-render/tl8render_test.go | 92 ++++++++++++++++++++ internal/tl8/tl8.go | 15 +++- 3 files changed, 174 insertions(+), 78 deletions(-) create mode 100644 cmd/tl8-render/tl8render_test.go diff --git a/cmd/tl8-render/tl8render.go b/cmd/tl8-render/tl8render.go index 2db9452..92c1384 100644 --- a/cmd/tl8-render/tl8render.go +++ b/cmd/tl8-render/tl8render.go @@ -3,62 +3,22 @@ package main import ( "flag" "fmt" + "html/template" "io/ioutil" "log" "os" "path/filepath" "strings" + "translation/internal/tl8" "github.com/google/renameio" - "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) -const preamble = ` - - - - - -i3: i3 User’s Guide - - - - - - -
- - -
-
-` - -const footer = `
- - -` - type translationStatusNode struct { ast.BaseBlock @@ -80,7 +40,7 @@ var kindTranslationStatus = ast.NewNodeKind("TranslationStatus") type versionNode struct { ast.BaseBlock - since string + introduced string } // Kind implements Node.Kind @@ -106,8 +66,8 @@ func modifyASTFromHeading(heading ast.Node) { continue } val := string(b) - if string(attr.Name) == "class" && strings.HasPrefix(val, "since_") { - vsn.since = strings.TrimPrefix(val, "since_") + if string(attr.Name) == "introduced" { + vsn.introduced = val } if string(attr.Name) == "translated" { tsn.translatedVersion = val @@ -115,7 +75,7 @@ func modifyASTFromHeading(heading ast.Node) { //log.Printf("heading attr, name=%s, value=%s", attr.Name, val) } - if vsn.since != "" { + if vsn.introduced != "" { // Adding a child will make it part of the heading HTML element // (e.g.

) heading.AppendChild(heading, vsn) @@ -146,7 +106,9 @@ func numericVersionToHuman(v string) string { return strings.ReplaceAll(v, "_", ".") } -type tl8renderer struct{} +type tl8renderer struct { + basename string +} func (r *tl8renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(kindTranslationStatus, r.renderTranslationStatus) @@ -158,7 +120,7 @@ func (r *tl8renderer) renderVersion(w util.BufWriter, source []byte, node ast.No return ast.WalkContinue, nil } vsn := node.(*versionNode) - fmt.Fprintf(w, `since i3 v%s`, numericVersionToHuman(vsn.since)) + fmt.Fprintf(w, `since i3 v%s`, numericVersionToHuman(vsn.introduced)) return ast.WalkContinue, nil } @@ -177,51 +139,59 @@ func (r *tl8renderer) renderTranslationStatus(w util.BufWriter, source []byte, n Out-of-date! This section’s translation was last updated for i3 v%s (what changed?) (contribute) -`, + +`, translatedVersion, - "userguide", /* TODO */ - "userguide" /* TODO */) + r.basename, + r.basename) return ast.WalkContinue, nil } -func render1(fn string) error { - outfn := strings.TrimSuffix(fn, filepath.Ext(fn)) + ".html" +func render1(fn string, headerTmpl, footerTmpl *template.Template) error { + basename := strings.TrimSuffix(fn, filepath.Ext(fn)) + outfn := basename + ".html" source, err := ioutil.ReadFile(fn) if err != nil { return err } - md := goldmark.New( - // GFM is GitHub Flavored Markdown, which we need for tables, for - // example. - goldmark.WithExtensions(extension.GFM), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - // The Attribute option allows us to id, classes, and arbitrary - // options on headings (for translation status). - parser.WithAttribute(), - parser.WithASTTransformers(util.Prioritized(&tl8transformer{}, 1)), - ), - goldmark.WithRendererOptions( - html.WithHardWraps(), - html.WithXHTML(), - renderer.WithNodeRenderers( - util.Prioritized(&tl8renderer{}, 500)), - ), - ) + md := tl8.NewGoldmarkWithOptions( + []parser.Option{parser.WithASTTransformers(util.Prioritized(&tl8transformer{}, 1))}, + []renderer.Option{renderer.WithNodeRenderers(util.Prioritized(&tl8renderer{ + basename: basename, + }, 500))}) out, err := renameio.TempFile("", outfn) if err != nil { return err } - out.Write([]byte(preamble)) + if headerTmpl != nil { + doc, err := tl8.Segment(source) + if err != nil { + return err + } + + documentHeading := doc.Headings[0] + + if err := headerTmpl.Execute(out, struct { + Title string + }{ + Title: documentHeading.Text, + }); err != nil { + return fmt.Errorf("rendering -header_template: %v", err) + } + } if err := md.Convert(source, out); err != nil { return err } - out.Write([]byte(footer)) + if footerTmpl != nil { + if err := footerTmpl.Execute(out, nil); err != nil { + return fmt.Errorf("rendering -footer_template: %v", err) + } + } if err := out.CloseAtomicallyReplace(); err != nil { return err @@ -230,13 +200,40 @@ func render1(fn string) error { } func tl8render() error { + var ( + header = flag.String("header_template", + "", + "path to a Go template file (https://golang.org/pkg/html/template/) containing the HTML that should be printed before converted markdown content") + + footer = flag.String("footer_template", + "", + "path to a Go template file (https://golang.org/pkg/html/template/) containing the HTML that should be printed after converted markdown content") + ) + flag.Parse() if flag.NArg() < 1 { return fmt.Errorf("syntax: %s [...]", filepath.Base(os.Args[0])) } + var headerTmpl, footerTmpl *template.Template + if *header != "" { + var err error + headerTmpl, err = template.ParseFiles(*header) + if err != nil { + return err + } + } + + if *footer != "" { + var err error + footerTmpl, err = template.ParseFiles(*footer) + if err != nil { + return err + } + } + for _, fn := range flag.Args() { - if err := render1(fn); err != nil { + if err := render1(fn, headerTmpl, footerTmpl); err != nil { return err } } diff --git a/cmd/tl8-render/tl8render_test.go b/cmd/tl8-render/tl8render_test.go new file mode 100644 index 0000000..d93d793 --- /dev/null +++ b/cmd/tl8-render/tl8render_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "bytes" + "html/template" + "io/ioutil" + "path/filepath" + "testing" + "translation/internal/tl8" + + "github.com/google/go-cmp/cmp" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +func TestRenderVersion(t *testing.T) { + md := tl8.NewGoldmarkWithOptions( + []parser.Option{parser.WithASTTransformers(util.Prioritized(&tl8transformer{}, 1))}, + []renderer.Option{renderer.WithNodeRenderers(util.Prioritized(&tl8renderer{}, 500))}) + source := []byte(`# heading {#heading_id introduced="4_16"}`) + var buf bytes.Buffer + if err := md.Convert(source, &buf); err != nil { + t.Fatal(err) + } + got := buf.String() + want := `

headingsince i3 v4.16

+` + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("unexpected output: diff (-want +got):\n%s", diff) + } +} + +func TestRenderTranslation(t *testing.T) { + md := tl8.NewGoldmarkWithOptions( + []parser.Option{parser.WithASTTransformers(util.Prioritized(&tl8transformer{}, 1))}, + []renderer.Option{renderer.WithNodeRenderers(util.Prioritized(&tl8renderer{ + basename: "userguide", + }, 500))}) + source := []byte(`# heading {#heading_id translated="4_17"}`) + var buf bytes.Buffer + if err := md.Convert(source, &buf); err != nil { + t.Fatal(err) + } + got := buf.String() + want := `

heading

+ +Out-of-date! This section’s translation was last updated for i3 v4.17 +(what changed?) +(contribute) + +` + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("unexpected output: diff (-want +got):\n%s", diff) + } +} + +func TestRenderTemplate(t *testing.T) { + headerTmpl, err := template.New("").Parse(` + + {{ .Title }} + + +`) + if err != nil { + t.Fatal(err) + } + tmpDir := t.TempDir() + fn := filepath.Join(tmpDir, "userguide.markdown") + const userguideMarkdown = `# i3 User Guide +` + if err := ioutil.WriteFile(fn, []byte(userguideMarkdown), 0644); err != nil { + t.Fatal(err) + } + if err := render1(fn, headerTmpl, nil); err != nil { + t.Fatal(err) + } + got, err := ioutil.ReadFile(filepath.Join(tmpDir, "userguide.html")) + if err != nil { + t.Fatal(err) + } + const want = ` + + i3 User Guide + + +

i3 User Guide

+` + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Fatalf("RenderTemplate: unexpected diff (-want +got):\n%s", diff) + } +} diff --git a/internal/tl8/tl8.go b/internal/tl8/tl8.go index 9cf7eb1..a5dfbd7 100644 --- a/internal/tl8/tl8.go +++ b/internal/tl8/tl8.go @@ -10,25 +10,30 @@ import ( "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" ) func NewGoldmark() goldmark.Markdown { + return NewGoldmarkWithOptions(nil, nil) +} + +func NewGoldmarkWithOptions(parserOptions []parser.Option, rendererOptions []renderer.Option) goldmark.Markdown { return goldmark.New( // GFM is GitHub Flavored Markdown, which we need for tables, for // example. goldmark.WithExtensions(extension.GFM), - goldmark.WithParserOptions( + goldmark.WithParserOptions(append([]parser.Option{ parser.WithAutoHeadingID(), // The Attribute option allows us to id, classes, and arbitrary // options on headings (for translation status). parser.WithAttribute(), - ), - goldmark.WithRendererOptions( + }, parserOptions...)...), + goldmark.WithRendererOptions(append([]renderer.Option{ html.WithHardWraps(), html.WithXHTML(), - ), + }, rendererOptions...)...), ) } @@ -36,6 +41,7 @@ type Heading struct { Line int ID string Translated string + Text string } type Section struct { @@ -107,6 +113,7 @@ func Segment(source []byte) (*Document, error) { return ast.WalkStop, fmt.Errorf("BUG: could not find line offset for position %d", first.Start) } h.Line = line + 1 + h.Text = string(n.Text(source)) headings = append(headings, h) headingsByID[h.ID] = h }