Skip to content

Commit

Permalink
tl8render: implement header/footer templates
Browse files Browse the repository at this point in the history
  • Loading branch information
stapelberg committed Feb 2, 2021
1 parent 2d56c27 commit aac983b
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 78 deletions.
145 changes: 71 additions & 74 deletions cmd/tl8-render/tl8render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
<meta name="generator" content="AsciiDoc 9.0.3" />
<title>i3: i3 User’s Guide</title>
<link rel="stylesheet" href="https://i3wm.org/css/style.css" type="text/css" />
<link rel="stylesheet" href="markdownxhtml11.css" type="text/css" />
<script type="text/javascript">
/*<![CDATA[*/
document.addEventListener("DOMContentLoaded", function(){asciidoc.footnotes(); asciidoc.toc(2);}, false);
/*]]>*/
</script>
<script type="text/javascript" src="/js/asciidoc-xhtml11.js"></script>
</head>
<body>
<header>
<a class="logo" href="/">
<img src="https://i3wm.org/img/logo.svg" alt="i3 WM logo" />
</a>
<nav>
<ul>
<li><a style="border-bottom: 2px solid #fff" href="/docs">Docs</a></li>
<li><a href="/screenshots">Screens</a></li>
<li><a href="https://www.reddit.com/r/i3wm/">FAQ</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="https://github.com/i3/i3/issues">Bugs</a></li>
</ul>
</nav>
</header>
<main>
`

const footer = `</main>
</body>
</html>
`

type translationStatusNode struct {
ast.BaseBlock

Expand All @@ -80,7 +40,7 @@ var kindTranslationStatus = ast.NewNodeKind("TranslationStatus")
type versionNode struct {
ast.BaseBlock

since string
introduced string
}

// Kind implements Node.Kind
Expand All @@ -106,16 +66,16 @@ 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
}
//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. <h1>)
heading.AppendChild(heading, vsn)
Expand Down Expand Up @@ -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)
Expand All @@ -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, `<span class="sinceversion">since i3 v%s</span>`, numericVersionToHuman(vsn.since))
fmt.Fprintf(w, `<span class="introduced">since i3 v%s</span>`, numericVersionToHuman(vsn.introduced))
return ast.WalkContinue, nil
}

Expand All @@ -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
(<a href="https://github.com/i3/i3/commits/next/docs/%s">what changed?</a>)
(<a href="https://github.com/i3/i3/edit/next/docs/%s">contribute</a>)
</i>`,
</i>
`,
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
Expand All @@ -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 <markdown-file> [<markdown-file>...]", 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
}
}
Expand Down
92 changes: 92 additions & 0 deletions cmd/tl8-render/tl8render_test.go
Original file line number Diff line number Diff line change
@@ -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 := `<h1 id="heading_id">heading<span class="introduced">since i3 v4.16</span></h1>
`
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 := `<h1 id="heading_id">heading</h1>
<i>
Out-of-date! This section’s translation was last updated for i3 v4.17
(<a href="https://github.com/i3/i3/commits/next/docs/userguide">what changed?</a>)
(<a href="https://github.com/i3/i3/edit/next/docs/userguide">contribute</a>)
</i>
`
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(`<html>
<head>
<title>{{ .Title }}</title>
</head>
<body>
`)
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 = `<html>
<head>
<title>i3 User Guide</title>
</head>
<body>
<h1 id="i3-user-guide">i3 User Guide</h1>
`
if diff := cmp.Diff(string(want), string(got)); diff != "" {
t.Fatalf("RenderTemplate: unexpected diff (-want +got):\n%s", diff)
}
}
15 changes: 11 additions & 4 deletions internal/tl8/tl8.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,38 @@ 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...)...),
)
}

type Heading struct {
Line int
ID string
Translated string
Text string
}

type Section struct {
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit aac983b

Please sign in to comment.