Go idiomatic way to handle html template and regexp

Hi,

I have the code below, the code is working fine, but I don’t know if it is Go idiomatic.
I am using html template to generate the html based on some type and send the final string to http.ResponseWriter.
But, after the template is executed I need to check to anything in this patter [PageName] and replace it for a proper html tag.

The solution I got is the one below, it uses bytes.Buffer to hold all data and after all necessary steps (template and regexp) I send it to http.ResponseWriter. It means that html variable will hold all page data.

Is it a good way to solve this issue? Do you have any feedback about how to improve it?

Thank you in advance!

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}

// How to improve code below?
	html := new(bytes.Buffer)
	err = templates.ExecuteTemplate(html, "view.html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	finalResponse := validLink.ReplaceAllString(html.String(), "<a href=\"/view/${1}\">${1}</a>")
	_, err = io.WriteString(w, finalResponse)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

It’s a very bad idea to use regular expressions in HTML. In your case, the HTML is yours, but it’s only a matter of time before you inadvertently break the replacer.

Why do you need to replace the links? Can’t you generate them correctly to begin with? How does the template look like?

2 Likes

As @Giulio_Iotti says, don’t use regular expressions to manipulate HTML or XML.

Can’t your template view.html already set the correct title? Doesn’t p know the title?

You should try to pass everything that is required to execute the template as the data parameter to ExecuteTemplate.

1 Like

Take a look at the example in the documentation. Note how the data struct contains everything necessary to execute the template. Can’t you do something similar?

With a well written template and a complete data parameter, it should not be necessary to manipulate the filled template.

1 Like

Thank you for your comments.

I was studying this link: https://golang.org/doc/articles/wiki/
At the end it give us some challenges and I am trying to implement the latest one, that consist to change wiki page text links to another page to the correct html tag.

That was the only way I found to accomplish it. Probably there is better way to solves it, that is why I am asking you experts to advice me. :wink:

That is my template view.html:

<html>
    <head>
        <title>View Page</title>
    </head>
    <body>
        <h1>{{.Title}}</h1>

        <p>[<a href="/edit/{{.Title}}">edit</a>]</p>

        <div>{{range .Lines}}{{ . }}<br/>{{end}}</div>
    </body>
</html>

Template edit.html:

<html>
    <head>
        <title>Edit Page</title>
    </head>
    <body>
        <h1>Editing {{.Title}}</h1>

        <form action="/save/{{.Title}}" method="POST">
            <div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
            <div><input type="submit" value="Save"></div>
        </form>
    </body>
</html>

That is my entire code (it is long, sorry!):

package main

import (
	"bytes"
	"html/template"
	"io"
	"io/ioutil"
	"net/http"
	"path/filepath"
	"regexp"
	"strings"
)

const (
	templatePath = "tmpl"
	dataPath     = "data"
)

var (
	templates = template.Must(template.ParseFiles(
		filepath.Join(templatePath, "edit.html"),
		filepath.Join(templatePath, "view.html"),
	))
	validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
	validLink = regexp.MustCompile("\\[([a-zA-Z0-9]+)\\]")
)

func main() {
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Here we will extract the page title from the Request,
		// and call the provided handler 'fn'
		m := validPath.FindStringSubmatch(r.URL.Path)
		if m == nil {
			http.NotFound(w, r)
			return
		}
		fn(w, r, m[2])
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, "/view/FrontPage", http.StatusFound)
}

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}

	html := new(bytes.Buffer)
	err = templates.ExecuteTemplate(html, "view.html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	finalResponse := validLink.ReplaceAllString(html.String(), "<a href=\"/view/${1}\">${1}</a>")
	_, err = io.WriteString(w, finalResponse)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	err = templates.ExecuteTemplate(w, "edit.html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err := p.save()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

// Page :
type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := filepath.Join(dataPath, p.Title+".txt")
	return ioutil.WriteFile(filename, p.Body, 0600)
}

// Lines :
func (p *Page) Lines() []string {
	return strings.Split(string(p.Body), "\n")
}

func loadPage(title string) (*Page, error) {
	filename := filepath.Join(dataPath, title+".txt")
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return &Page{title, body}, nil
}

Are you referring to this?

Implement inter-page linking by converting instances of [PageName] to
<a href="/view/PageName">PageName</a>. (hint: you could use regexp.ReplaceAllFunc to do this) 

So the idea is to support some kind of wiki markup, correct?

Exactly that! Yes, support internal wiki link markup.

It is just an exercise for me to learn Go, I am not trying to really create my wiki engine!

And does the support for this wiki markup be implemented for html.template instances?

Sorry, I couldn’t follow you exactly.

The trick is, how to do it inside template?
I can replace it on the date itself before run the template, but in this case the template will print the html tag as text, not as a real html tag.

I’m not an expert of html.template but maybe you could try html.template.HTML. You could prepare the link using regular expressions and then use it on your template.

Right now I am busy but maybe later I have the time to try it out.

Thank you for your time. I will try to learn it.

When you have time if you can contribute with my knowledge, I will really appreciate it!

tldr; I guess using gorilla/mux is more elegant and safer.

@geosoft1 Probably it is, for sure, but as it is just some exercise for me to learn Go I want to find a better way to solve it with the code that I already have.

Using all your comments, I could find the solution below. Searching for it I found people saying that template should not have logic, but without it I don’t know how to solve.

Any feedback?


My new handler, please note that I am using a new type (viewData), not the original one (Page).

func view2Handler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}

	vd := &viewData{Title: p.Title}
	for _, l := range p.Lines() {
		l = strings.Replace(l, "\r", "", -1)
		nl := line{}
		for _, w := range strings.Split(l, " ") {
			nw := word(w)
			nl.Words = append(nl.Words, nw)
		}
		vd.Lines = append(vd.Lines, nl)
	}
	err = templates.ExecuteTemplate(w, "view2.html", vd)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

New types to support my new logic:

type viewData struct {
	Title string
	Lines []line
}
type line struct {
	Words []word
}

type word string

func (w word) IsWikiLink() bool {
	return validLink.MatchString(string(w))
}
func (w word) RemoveWikiTag() string {
	v := string(w)
	return v[1 : len(v)-1]
}

And this is the new template file:

<html>
    <head>
        <title>View Page</title>
    </head>
    <body>
        <h1>{{.Title}}</h1>

        <p>[<a href="/edit/{{.Title}}">edit</a>]</p>

        <div>
            {{range .Lines}}
                {{range .Words}}
                    {{if .IsWikiLink }}
                        <a href="/view/{{ .RemoveWikiTag }}">{{ .RemoveWikiTag }}</a>
                    {{else}}
                        {{ . }}
                    {{end}}
                {{end}}
                <br/>
            {{end}}
        </div>
    </body>
</html>