Skip to content

Commit

Permalink
play: Allow for setting render options
Browse files Browse the repository at this point in the history
Options are derived from command-line arguments given to Grawkit itself,
and generally control rendering of the graph.
  • Loading branch information
deuill committed Jul 22, 2024
1 parent 77cf684 commit 73f5a36
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 17 deletions.
2 changes: 1 addition & 1 deletion grawkit
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ function parse_arguments(argc, argv, _, arg, i, option) {
if (arg == "help") {
printf "Usage: grawkit [OPTION]... [FILE]\nOptions:\n"
for (k in config) {
printf " --%s=\"%s\"\n \t%s\n", k, config[k], comment[k]
printf " --%s=\"%s\"\n \t%s\n", k, config[k], comment[k]
}
exit exit_code = 0
} else {
Expand Down
2 changes: 1 addition & 1 deletion play/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM docker.io/golang:1.22 AS builder
WORKDIR /src

COPY play/ /src/
RUN go build -o /play play.go
RUN go build -o /play main.go

FROM docker.io/debian:stable-slim
WORKDIR /play
Expand Down
4 changes: 2 additions & 2 deletions play/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ AWK implementation built entirely in Go, and used here as a library.

## Installation & Usage

Only a fairly recent version of Go is required to build this package, and the included `play.go`
file can be executed on-the-fly using `go run`.
Only a fairly recent version of Go is required to build this package, and the included `main.go`
file can be executed on-the-fly using `go run .`.

The program expects to find a `static` directory (which is provided alongside the source code
here), as well as the `grawkit` script; by default, these are expected to be found in the current
Expand Down
154 changes: 142 additions & 12 deletions play/play.go → play/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ package main

import (
// Standard library
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
"text/template"
"time"
Expand Down Expand Up @@ -42,17 +45,112 @@ var (
program *parser.Program // The parsed version of the Grawkit script.
)

// Option represents optional configuration passed to Grawkit, affecting rendering of graphs.
type Option struct {
Name string // The name of the configuration option.
Value string // The value for the configuration option.
Type string // The optional kind of value, defaults to a plain string.
}

// Config represents collected configuration options for Grawkit.
type Config []Option

// CmdlineArgs returns configuration options in command-line argument format.
func (c Config) CmdlineArgs() []string {
var result []string
for _, o := range c {
result = append(result, "--"+o.Name+"="+o.Value)
}

return result
}

// The default configuration values, as derived from Grawkit itself.
var defaultConfig Config

// GetDefaultConfig fills in configuration defaults based on usage documentation returned by Grawkit
// itself.
func getDefaultConfig(program *parser.Program) error {
var buf bytes.Buffer
config := &interp.Config{
Output: &buf,
Args: []string{"--help"},
NoExec: true,
NoArgVars: true,
NoFileWrites: true,
NoFileReads: true,
}

// Render generated preview from content given.
if _, err := interp.ExecProgram(program, config); err != nil {
return fmt.Errorf("error executing program: %w", err)
}

scanner := bufio.NewScanner(&buf)
for scanner.Scan() {
line, ok := strings.CutPrefix(scanner.Text(), " --")
if !ok {
continue
}

name, value, ok := strings.Cut(line, "=")
if !ok {
continue
}

value = strings.Trim(value, `"`)
if value == "" {
continue
}

var kind = "text"
_, err := strconv.Atoi(value)
if err == nil {
kind = "number"
}

defaultConfig = append(defaultConfig, Option{
Name: name,
Value: value,
Type: kind,
})
}

return scanner.Err()
}

// ParseConfig processes the given form into a set of configuration options, validating against the
// pre-existing set of default options.
func parseConfig(form url.Values) Config {
var validOptions = make(map[string]int)
var result = make(Config, len(defaultConfig))
for i, o := range defaultConfig {
validOptions["config-"+o.Name], result[i] = i, o
}

// Update values from user-provided form fields, where default options exist.
for name := range form {
if idx, ok := validOptions[name]; ok {
result[idx].Value = form.Get(name)
}
}

return result
}

// ParseContent accepts un-filtered POST form content, and returns the content to render as a string.
// An error is returned if the content is missing or otherwise invalid.
func parseContent(form url.Values) (string, error) {
if _, ok := form["content"]; !ok || len(form["content"]) == 0 {
return "", errors.New("missing or empty content")
return "", fmt.Errorf("missing or empty content")
}

var content = form["content"][0]
switch true {
case content == "":
return "", fmt.Errorf("empty content given")
case len(content) > maxContentSize:
return "", errors.New("content too large")
return "", fmt.Errorf("content too large")
}

return content, nil
Expand All @@ -64,12 +162,13 @@ func parseContent(form url.Values) (string, error) {
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Handle template rendering on root path.
if r.URL.Path == "/" {
var outbuf, errbuf bytes.Buffer
var data struct {
Content string
Preview string
Config Config
Error string
}
var outbuf, errbuf bytes.Buffer

switch r.Method {
case "POST":
Expand All @@ -78,22 +177,28 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
} else if data.Content, err = parseContent(r.PostForm); err != nil {
data.Error = errValidate + ": " + err.Error()
} else {
data.Config = parseConfig(r.PostForm)
config := &interp.Config{
Stdin: bytes.NewReader([]byte(data.Content)),
Output: &outbuf,
Error: &errbuf,
Stdin: bytes.NewReader([]byte(data.Content)),
Output: &outbuf,
Error: &errbuf,
Args: data.Config.CmdlineArgs(),
NoArgVars: true,
NoFileWrites: true,
NoFileReads: true,
}

// Render generated preview from content given.
if n, err := interp.ExecProgram(program, config); err != nil {
data.Error = errRender
log.Printf("error executing program: %s", err)
} else if n != 0 {
data.Error = "Error: " + string(errbuf.Bytes())
} else if _, ok := r.PostForm["generate"]; ok {
data.Preview = string(outbuf.Bytes())
} else if _, ok = r.PostForm["download"]; ok {
w.Header().Set("Content-Disposition", `attachment; filename="generated.svg"`)
http.ServeContent(w, r, "generated.svg", time.Now(), bytes.NewReader(outbuf.Bytes()))
w.Header().Set("Content-Disposition", `attachment; filename="grawkit.svg"`)
http.ServeContent(w, r, "grawkit.svg", time.Now(), bytes.NewReader(outbuf.Bytes()))
return
}
}
Expand All @@ -106,6 +211,10 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}

if data.Config == nil {
data.Config = defaultConfig
}

// Render index page template.
if err := index.Execute(w, data); err != nil {
log.Printf("error rendering template: %s", err)
Expand Down Expand Up @@ -133,15 +242,36 @@ func setup() error {
path.Join("template", "default-preview.template"),
}

if index, err = template.ParseFS(static.FS, files...); err != nil {
index = template.New("index.template").Funcs(template.FuncMap{
"group": func(v []Option, n int) (result [][]Option) {
if n == 0 {
return [][]Option{v}
}
for i := range v {
if (i % n) == 0 {
result = append(result, []Option{})
}
l := len(result) - 1
result[l] = append(result[l], v[i])
}
return result
},
})

if index, err = index.ParseFS(static.FS, files...); err != nil {
return err
}

// Parse Grawkit script into concrete representation.
if script, err := os.ReadFile(*scriptPath); err != nil {
return err
return fmt.Errorf("failed reading script file at path '%s': %w", *scriptPath, err)
} else if program, err = parser.ParseProgram(script, nil); err != nil {
return err
return fmt.Errorf("failed parsing script: %w", err)
}

// Set up default configuration for Grawkit.
if err := getDefaultConfig(program); err != nil {
return fmt.Errorf("failed getting default configuration: %w", err)
}

return nil
Expand Down
17 changes: 16 additions & 1 deletion play/static/template/index.template
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,27 @@
<div id="preview-error" class="alert alert-danger">{{if .Error}}{{printf .Error}}{{end}}</div>
<div class="row">
<div class="col-sm mb-3">
<textarea id="editor" name="content" class="form-control">{{if .Content}}{{printf .Content}}{{else}}{{template "default-content.template"}}{{end}}</textarea>
<textarea id="editor" name="content" class="form-control bg-light">{{if .Content}}{{printf .Content}}{{else}}{{template "default-content.template"}}{{end}}</textarea>
</div>
<div class="col-sm mb-3">
<div id="preview-generated">{{if .Preview}}{{printf .Preview}}{{else}}{{template "default-preview.template"}}{{end}}</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<h2>Options</h2>
</div>
</div>
{{range $config := group .Config 3}}
<div class="row">
{{range $config}}
<div class="col-sm form-group">
<label for="config-{{.Name}}">{{.Name}}</label>
<input type="{{.Type}}" name="config-{{.Name}}" value="{{.Value}}" class="form-control">
</div>
{{end}}
</div>
{{end}}
</div>
</form>
<script src="/js/main.js"></script>
Expand Down

0 comments on commit 73f5a36

Please sign in to comment.