~reesmichael1/chantpointer

58e4df58e556983fd28bdd3cf2a47677e978f7f3 — Michael Rees 3 years ago 8f27a0c
Prepare for production
13 files changed, 375 insertions(+), 63 deletions(-)

M chant.go
A error.html
M handlers.go
R templates/html/index.html => index.html
M main.go
D page.go
M parser.go
M parser_test.go
R templates/psalm.sty => psalm.sty
M psalm.tem
A static/demo.png
A writer.go
A writer_test.go
M chant.go => chant.go +1 -0
@@ 16,4 16,5 @@ type Chant struct {
type Verse struct {
	FirstPart  string
	SecondPart string
	IsSecond   bool
}

A error.html => error.html +27 -0
@@ 0,0 1,27 @@
<html>
    <head>
        <title>Chant Pointer</title>
        <link href="/static/bulma.min.css" rel="stylesheet" type="text/css" media="all">
        <style> 
html {
    overflow-y: auto;
    font-family: Arial, sans-serif;
}
        </style>
    </head>
    <body>
        <section class="hero is-primary has-text-centered">
            <div class="hero-body ">
                <h1 class="title">Chant Pointer</h1>
                <h2 class="subtitle">Easily point Anglican chants</h2>
            </div>
        </section>

        <div class="section">
            <p>Oh no! An error occurred. The error message was:</p>
            <pre>{{ .Message }}</pre>
            <p>Please try again. If this keeps happening, please <a href="mailto:reesmichael1@vivaldi.net">let me know</a>.</p>
            <p><a href="/">Return to Home</a></p>
        </div>
    </body>
</html>

M handlers.go => handlers.go +76 -46
@@ 3,62 3,81 @@ package main
import (
	"bytes"
	"fmt"
	htmlTemplate "html/template"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	textTemplate "text/template"
)

// GenerateHandler handles incoming requests to the generate endpoint
func GenerateHandler(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
// ErrorHandler handles any errors that occur in the other endpoints
// and nicely displays them to the user
type ErrorHandler func(http.ResponseWriter, *http.Request) error

var templates = template.Must(template.ParseFiles("index.html", "error.html"))

// Page holds the link to the generated PDF, if it exists
type Page struct {
	URL string
}

// ErrorPage holds an error message to show to the user
type ErrorPage struct {
	Message string
}

func (fn ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := fn(w, r); err != nil {
		log.Printf("error: %v\n", err)
		templates.ExecuteTemplate(w, "error.html", ErrorPage{err.Error()})
	}
}

	templatePath := "psalm.tem"
	t, err := textTemplate.New(templatePath).Delims("((", "))").ParseFiles(templatePath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
// ChantHandler serves the finished PDFs of chants
func ChantHandler(w http.ResponseWriter, r *http.Request) {
	chantID := path.Base(r.URL.Path)
	url := fmt.Sprintf("/tmp/chant%s/psalm.pdf", chantID)
	http.ServeFile(w, r, url)
}

// GenerateHandler handles incoming requests to the generate endpoint
func GenerateHandler(w http.ResponseWriter, r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}

	var buf bytes.Buffer
	file, _, err := r.FormFile("score")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	dir, err := ioutil.TempDir("", "chant")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	stylePath := filepath.Join(dir, "psalm.sty")
	style, err := ioutil.ReadFile("templates/psalm.sty")
	style, err := ioutil.ReadFile("psalm.sty")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	err = ioutil.WriteFile(stylePath, style, 0644)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	io.Copy(&buf, file)
	chantPath := filepath.Join(dir, "chant.png")
	err = ioutil.WriteFile(chantPath, buf.Bytes(), 0644)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	buf.Reset()


@@ 71,43 90,54 @@ func GenerateHandler(w http.ResponseWriter, r *http.Request) {
		PointSize: 12,
	}

	err = t.Execute(&buf, c)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	texPath := filepath.Join(dir, "psalm.tex")
	err = ioutil.WriteFile(texPath, buf.Bytes(), 0644)

	buf.Reset()

	pdfPath := filepath.Join(dir, "psalm.pdf")
	texFile, err := os.OpenFile(texPath, os.O_CREATE|os.O_RDWR, 0644)
	defer texFile.Close()
	err = WriteTexForChant(texFile, &c)

	cmd := exec.Command("pdflatex", texPath)
	cmd.Stdout = &buf
	cmd.Stderr = &buf
	startDir, err := os.Getwd()
	if err != nil {
		return err
	}

	os.Chdir(dir)
	err = cmd.Run()
	if err != nil {
		fmt.Println(string(buf.Bytes()))
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		log.Printf("pdflatex error: %v", string(buf.Bytes()))
		return fmt.Errorf("pdflatex error: %v", err)
	}

	http.Redirect(w, r, pdfPath, http.StatusTemporaryRedirect)
}
	os.Chdir(startDir)

// IndexHandler handles incoming requests to the generate endpoint
func IndexHandler(w http.ResponseWriter, r *http.Request) {
	t, err := htmlTemplate.ParseFiles("templates/html/index.html")
	relativeURL := "/"
	u, err := url.Parse(relativeURL)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		return err
	}

	err = t.Execute(w, Page{})
	queryString := u.Query()
	queryString.Set("id", dir[len("/tmp/chant"):])
	u.RawQuery = queryString.Encode()

	baseURL := "/"
	base, err := url.Parse(baseURL)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		log.Fatal(err)
	}

	http.Redirect(w, r, fmt.Sprint(base.ResolveReference(u)), http.StatusFound)
	return nil
}

// IndexHandler handles incoming requests to the generate endpoint
func IndexHandler(w http.ResponseWriter, r *http.Request) error {
	chant := r.URL.Query().Get("id")
	if chant != "" {
		chant = fmt.Sprintf("/chant/%s", chant)
		return templates.ExecuteTemplate(w, "index.html", Page{chant})
	}
	return templates.ExecuteTemplate(w, "index.html", Page{})
}

R templates/html/index.html => index.html +61 -5
@@ 3,7 3,10 @@
        <title>Chant Pointer</title>
        <link href="/static/bulma.min.css" rel="stylesheet" type="text/css" media="all">
        <style> 
html {overflow-y: auto;}
html {
    overflow-y: auto;
    font-family: Arial, sans-serif;
}
        </style>
    </head>
    <body>


@@ 18,7 21,7 @@ html {overflow-y: auto;}
                <div class="field">
                    <label class="label">Chant Score</label>
                    <div class="control">
                        <input class="input" type="file" name="score" accept="image/*">
                        <input class="input" type="file" name="score" accept="image/*" required>
                    </div>
                </div>
                <div class="field">


@@ 38,17 41,70 @@ html {overflow-y: auto;}
                <div class="field">
                    <label class="label">Chant</label>
                    <div class="control">
                        <textarea name="chant" class="textarea"></textarea>
                        <textarea name="chant" class="textarea" required></textarea>
                    </div>
                </div>

                <div class="field is-grouped">
                <div class="field">
                    <div class="control">
                        <button class="button is-link">Generate PDF</button>
                    </div>
                </div>

                {{ if ne .URL "" }}
                <div class="field">
                    <div class="control">
                        <a href="{{ .URL }}">Download PDF</a>
                        <p>This link will work and may be shared for the next 24 hours.</p>
                        <p>(If you need to edit your results, you should be able to go back and make any changes.)</P>
                    </div>
                </div>
                {{ end }}
            </form>
        </div>
    </body>
        <div class="section">
            <h2 class="subtitle">Demo</h2>
            <div class="columns">
                <div class="column">
                    <div class="content">
                        <pre>The earth is the Lord's and all that | is in | it,
the world and | all who | dwell there|in.

For it is he who founded it u|pon the | s\"eas
and made it firm | u_pon | (the rivers) (of the) | deep.

^Who is he, this | King of | glory?
The Lord of hosts,\\ he | is the | King of | glory.</pre>
                    </div>
                </div>
                <div class="column">
                    <img src="./static/demo.png">
                </div>
            </div>
        </div>
        <div class="section">
            <h2 class="subtitle">How-To</h2>
            <div class="content">
                <ul>
                    <li>Use line breaks to separate parts of a verse</li>
                    <li>Use <code>\\</code> to insert a line break in the output while staying in the same verse</li>
                    <li>Use <code>\"</code> in front of a vowel to insert a diacritic</li>
                    <li>Use the <code>|</code> character to divide bars</li>
                    <li>Use parentheses to group syllables: <code>(glory of) the Lord</code></li>
                    <li>Add a <code>^</code> at the beginning of a verse to indicate that the verse should use the second part of the chant</li>
                    <li>Need help or want to see something else added? Feel free to <a href="mailto:reesmichael1@vivaldi.net">contact me</a>!</li>
                </ul>
            </div>
        </div>

        <footer class="footer">
            <div class="content">
                <p>
                If you find this useful, you may also be interested in <a href="https://soubasse.com">Soubasse</a>.
                </p>
                <p>This website was built on top of a <a href="https://github.com/gregrs-uk/anglican-chant-template">template for typesetting Anglican chant</a> originally written by <a href="https://github.com/gregrs-uk">@gregrs-uk</a>.
                <p>Built by <a href="https://reesmichael1.com">Michael Rees</a>. If you are interested, the source <a href="https://gitlab.com/reesmichael1/chantpointer">is available on Gitlab</a>.</p>
            </div>
        </footer>
    </body>
</html>

M main.go => main.go +3 -4
@@ 7,10 7,9 @@ import (

func main() {
	static := http.FileServer(http.Dir("static"))
	tmp := http.FileServer(http.Dir("/tmp"))
	http.Handle("/static/", http.StripPrefix("/static", static))
	http.Handle("/tmp/", http.StripPrefix("/tmp", tmp))
	http.HandleFunc("/generate", GenerateHandler)
	http.HandleFunc("/", IndexHandler)
	http.HandleFunc("/chant/", ChantHandler)
	http.Handle("/generate", ErrorHandler(GenerateHandler))
	http.Handle("/", ErrorHandler(IndexHandler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

D page.go => page.go +0 -4
@@ 1,4 0,0 @@
package main

// Page holds the data in the index page
type Page struct{}

M parser.go => parser.go +34 -2
@@ 5,7 5,7 @@ import (
	"strings"
)

var re = regexp.MustCompile(`\((.*)\)`)
var re = regexp.MustCompile(`\((.*?)\)`)

const (
	// FIRST signals to the parser that it is looking


@@ 19,10 19,20 @@ const (
// ProcessLine takes a line and returns a "processed" version
// (i.e., one that uses the TeX commands instead of the user inputted ones)
func ProcessLine(line string) string {
	// Escape LaTeX reserved characters
	line = strings.Replace(line, "#", `\#`, -1)
	line = strings.Replace(line, "$", `\$`, -1)
	line = strings.Replace(line, "%", `\%`, -1)
	line = strings.Replace(line, "{", `\{`, -1)
	line = strings.Replace(line, "}", `\}`, -1)
	line = strings.Replace(line, "~", `\~`, -1)
	// Format interline breaks
	line = strings.Replace(line, `\\`, `\breaklongline `, -1)
	line = strings.Replace(line, `\\`, `\breaklongline{} `, -1)
	// Format brackets over words
	line = re.ReplaceAllString(line, `\bracket{${1}}`)
	// Smallcap "Lord"
	line = strings.Replace(line, "Lord", `\textsc{Lord}`, -1)
	line = strings.Replace(line, "LORD", `\textsc{Lord}`, -1)
	return line
}



@@ 34,6 44,22 @@ func ParseInput(input string) (verses []Verse) {
	v := Verse{}
	parity := FIRST

	if len(lines) == 1 {
		if strings.HasPrefix(lines[0], "^") {
			return []Verse{
				Verse{
					FirstPart: ProcessLine(lines[0][1:]),
					IsSecond:  true,
				},
			}
		}
		return []Verse{
			Verse{
				FirstPart: ProcessLine(lines[0]),
			},
		}
	}

	for _, l := range lines {
		// Empty lines are used to separate verses
		if strings.TrimSpace(l) == "" {


@@ 41,6 67,11 @@ func ParseInput(input string) (verses []Verse) {
			continue
		}
		line := ProcessLine(l)
		if strings.HasPrefix(line, "^") {
			v.IsSecond = true
			line = line[1:]
		}

		if parity == FIRST {
			v.FirstPart = line
			parity = SECOND


@@ 48,6 79,7 @@ func ParseInput(input string) (verses []Verse) {
			v.SecondPart = line
			verses = append(verses, v)
			parity = FIRST
			v = Verse{}
		}
	}


M parser_test.go => parser_test.go +71 -1
@@ 23,7 23,7 @@ func TestLineBreakExtraction(t *testing.T) {
	expected := []Verse{
		Verse{
			FirstPart:  "abc",
			SecondPart: `12\breaklongline 3`,
			SecondPart: `12\breaklongline{} 3`,
		},
	}



@@ 93,3 93,73 @@ func TestNonEmptyLineBetweenVerses(t *testing.T) {

	equals(t, expected, verses)
}

func TestCaratSignalsSecondPart(t *testing.T) {
	input := "^abc\ndef"
	verses := ParseInput(input)
	expected := []Verse{
		Verse{
			FirstPart:  "abc",
			SecondPart: "def",
			IsSecond:   true,
		},
	}

	equals(t, expected, verses)
}

func TestMultipleBracketsOnSameLineExtraction(t *testing.T) {
	input := "a(bc) 1(23)\n456"
	verses := ParseInput(input)
	expected := []Verse{
		Verse{
			FirstPart:  `a\bracket{bc} 1\bracket{23}`,
			SecondPart: "456",
		},
	}

	equals(t, expected, verses)
}

func TestCaratSignalsSecondVerseWithPreviousVerses(t *testing.T) {
	input := "abc\n123\n\n^def\n456"
	verses := ParseInput(input)
	expected := []Verse{
		Verse{
			FirstPart:  "abc",
			SecondPart: "123",
		},
		Verse{
			FirstPart:  "def",
			SecondPart: "456",
			IsSecond:   true,
		},
	}

	equals(t, expected, verses)
}

func TestLaTeXReservedCharactersAreEscaped(t *testing.T) {
	input := `# $ % { } ~`
	verses := ParseInput(input)
	expected := []Verse{
		Verse{
			FirstPart: `\# \$ \% \{ \} \~`,
		},
	}

	equals(t, expected, verses)
}

func TestLordIsSmallCapped(t *testing.T) {
	input := "Lord\nLORD"
	verses := ParseInput(input)
	expected := []Verse{
		Verse{
			FirstPart:  `\textsc{Lord}`,
			SecondPart: `\textsc{Lord}`,
		},
	}

	equals(t, expected, verses)
}

R templates/psalm.sty => psalm.sty +0 -0
M psalm.tem => psalm.tem +1 -1
@@ 22,7 22,7 @@
\begin{psalm}

((range $v := .Verses ))
\vs{}{(( $v.FirstPart ))}
\vs{(( if .IsSecond ))\second(( end ))}{(( $v.FirstPart ))}
{}{(( $v.SecondPart ))}

(( end ))

A static/demo.png => static/demo.png +0 -0
A writer.go => writer.go +23 -0
@@ 0,0 1,23 @@
package main

import (
	"io"
	"text/template"
)

// WriteTexForChant writes the given Chant to an io.Writer
// (in practice, a text file, or for testing, an in-memory buffer)
func WriteTexForChant(f io.Writer, c *Chant) error {
	templatePath := "psalm.tem"
	t, err := template.New(templatePath).Delims("((", "))").ParseFiles(templatePath)
	if err != nil {
		return err
	}

	err = t.Execute(f, c)
	if err != nil {
		return err
	}

	return nil
}

A writer_test.go => writer_test.go +78 -0
@@ 0,0 1,78 @@
package main

import (
	"bytes"
	"testing"
)

func TestWriteVerses(t *testing.T) {
	buf := bytes.NewBuffer([]byte{})

	c := Chant{
		Title:    "Title",
		Subtitle: "Subtitle",
		Verses: []Verse{
			Verse{
				FirstPart:  "abc",
				SecondPart: "def",
			},

			Verse{
				FirstPart:  "123",
				SecondPart: "456",
			},
			Verse{
				FirstPart:  "ghi",
				SecondPart: "789",
				IsSecond:   true,
			},
		},
		PointSize: 12,
		ScorePath: "/tmp/chant.png",
	}

	expected := `\documentclass[12pt]{article}

\usepackage[margin=1in]{geometry}
\usepackage[T1]{fontenc}
\usepackage{graphicx}
\usepackage{psalm}

\pagestyle{empty}

\begin{document}

\begin{center}
    {\Large \textbf{Title}}\\[12pt]
    \textit{Subtitle}\\[30pt]
    \includegraphics[width=\textwidth]{/tmp/chant.png}
\end{center}
\vspace{24pt}

\setlength\textamount{12cm}

\begin{center}
\begin{psalm}


\vs{}{abc}
{}{def}


\vs{}{123}
{}{456}


\vs{\second}{ghi}
{}{789}


\end{psalm}
\end{center}

\end{document}
`

	WriteTexForChant(buf, &c)
	equals(t, expected, string(buf.Bytes()))
}