Index: experimental/webtry/webtry.go |
diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go |
index 1b678ecb778334dcc6757afd34ee6322eb59b202..16f6f4ff223051c8240d30cc3cd844979d835999 100644 |
--- a/experimental/webtry/webtry.go |
+++ b/experimental/webtry/webtry.go |
@@ -13,6 +13,7 @@ import ( |
htemplate "html/template" |
"io/ioutil" |
"log" |
+ "math/rand" |
"net/http" |
"os" |
"os/exec" |
@@ -45,9 +46,12 @@ var ( |
// indexTemplate is the main index.html page we serve. |
indexTemplate *htemplate.Template = nil |
- // recentTemplate is a list of recent images. |
+ // recentTemplate is a list of recent images. |
recentTemplate *htemplate.Template = nil |
+ // workspaceTemplate is the page for workspaces, a series of webtrys. |
+ workspaceTemplate *htemplate.Template = nil |
+ |
// db is the database, nil if we don't have an SQL database to store data into. |
db *sql.DB = nil |
@@ -56,6 +60,35 @@ var ( |
// imageLink is the regex that matches URLs paths that are direct links to PNGs. |
imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$") |
+ |
+ // workspaceLink is the regex that matches URLs paths for workspaces. |
+ workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$") |
+ |
+ // workspaceNameAdj is a list of adjectives for building workspace names. |
+ workspaceNameAdj = []string{ |
+ "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", |
+ "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", |
+ "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", |
+ "billowing", "broken", "cold", "damp", "falling", "frosty", "green", |
+ "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", |
+ "red", "rough", "still", "small", "sparkling", "throbbing", "shy", |
+ "wandering", "withered", "wild", "black", "young", "holy", "solitary", |
+ "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", |
+ "polished", "ancient", "purple", "lively", "nameless", |
+ } |
+ |
+ // workspaceNameNoun is a list of nouns for building workspace names. |
+ workspaceNameNoun = []string{ |
+ "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", |
+ "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", |
+ "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", |
+ "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", |
+ "feather", "grass", "haze", "mountain", "night", "pond", "darkness", |
+ "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", |
+ "violet", "water", "wildflower", "wave", "water", "resonance", "sun", |
+ "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", |
+ "frog", "smoke", "star", |
+ } |
) |
// flags |
@@ -90,12 +123,24 @@ func init() { |
panic(err) |
} |
// Convert index.html into a template, which is expanded with the code. |
- indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/index.html")) |
+ indexTemplate, err = htemplate.ParseFiles( |
+ filepath.Join(cwd, "templates/index.html"), |
+ filepath.Join(cwd, "templates/titlebar.html"), |
+ ) |
if err != nil { |
panic(err) |
} |
- |
- recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/recent.html")) |
+ recentTemplate, err = htemplate.ParseFiles( |
+ filepath.Join(cwd, "templates/recent.html"), |
+ filepath.Join(cwd, "templates/titlebar.html"), |
+ ) |
+ if err != nil { |
+ panic(err) |
+ } |
+ workspaceTemplate, err = htemplate.ParseFiles( |
+ filepath.Join(cwd, "templates/workspace.html"), |
+ filepath.Join(cwd, "templates/titlebar.html"), |
+ ) |
if err != nil { |
panic(err) |
} |
@@ -123,6 +168,7 @@ func init() { |
panic(err) |
} |
} else { |
+ log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) |
// Fallback to sqlite for local use. |
db, err = sql.Open("sqlite3", "./webtry.db") |
if err != nil { |
@@ -135,8 +181,25 @@ func init() { |
hash CHAR(64) DEFAULT '' NOT NULL, |
PRIMARY KEY(hash) |
)` |
- db.Exec(sql) |
- log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) |
+ _, err = db.Exec(sql) |
+ log.Printf("Info: status creating sqlite table for webtry: %q\n", err) |
+ sql = `CREATE TABLE workspace ( |
+ name CHAR(64) DEFAULT '' NOT NULL, |
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
+ PRIMARY KEY(name) |
+ )` |
+ _, err = db.Exec(sql) |
+ log.Printf("Info: status creating sqlite table for workspace: %q\n", err) |
+ sql = `CREATE TABLE workspacetry ( |
+ name CHAR(64) DEFAULT '' NOT NULL, |
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
+ hash CHAR(64) DEFAULT '' NOT NULL, |
+ hidden INTEGER DEFAULT 0 NOT NULL, |
+ |
+ FOREIGN KEY (name) REFERENCES workspace(name) |
+ )` |
+ _, err = db.Exec(sql) |
+ log.Printf("Info: status creating sqlite table for workspace try: %q\n", err) |
} |
} |
@@ -231,19 +294,28 @@ func reportError(w http.ResponseWriter, r *http.Request, err error, message stri |
w.Write(resp) |
} |
-func writeToDatabase(hash string, code string) { |
+func writeToDatabase(hash string, code string, workspaceName string) { |
if db == nil { |
return |
} |
if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil { |
log.Printf("ERROR: Failed to insert code into database: %q\n", err) |
} |
+ if workspaceName != "" { |
+ if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil { |
+ log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err) |
+ } |
+ } |
} |
func cssHandler(w http.ResponseWriter, r *http.Request) { |
http.ServeFile(w, r, "css/webtry.css") |
} |
+func jsHandler(w http.ResponseWriter, r *http.Request) { |
+ http.ServeFile(w, r, "js/run.js") |
+} |
+ |
// imageHandler serves up the PNG of a specific try. |
func imageHandler(w http.ResponseWriter, r *http.Request) { |
log.Printf("Image Handler: %q\n", r.URL.Path) |
@@ -294,6 +366,79 @@ func recentHandler(w http.ResponseWriter, r *http.Request) { |
} |
} |
+type Workspace struct { |
+ Name string |
+ Code string |
+ Tries []Try |
+} |
+ |
+// newWorkspace generates a new random workspace name and stores it in the database. |
+func newWorkspace() (string, error) { |
+ for i := 0; i < 10; i++ { |
+ adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))] |
+ noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))] |
+ suffix := rand.Intn(1000) |
+ name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix) |
+ if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil { |
+ return name, nil |
+ } else { |
+ log.Printf("ERROR: Failed to insert workspace into database: %q\n", err) |
+ } |
+ } |
+ return "", fmt.Errorf("Failed to create a new workspace") |
+} |
+ |
+// getCode returns the code for a given hash, or the empty string if not found. |
+func getCode(hash string) string { |
+ code := "" |
+ if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil { |
+ log.Printf("ERROR: Code for hash is missing: %q\n", err) |
+ } |
+ return code |
+} |
+ |
+func workspaceHandler(w http.ResponseWriter, r *http.Request) { |
+ log.Printf("Workspace Handler: %q\n", r.URL.Path) |
+ if r.Method == "GET" { |
+ tries := []Try{} |
+ match := workspaceLink.FindStringSubmatch(r.URL.Path) |
+ name := "" |
+ if len(match) == 2 { |
+ name = match[1] |
+ rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts DESC ", name) |
+ if err != nil { |
+ reportError(w, r, err, "Failed to select.") |
+ return |
+ } |
+ for rows.Next() { |
+ var hash string |
+ var create_ts time.Time |
+ if err := rows.Scan(&create_ts, &hash); err != nil { |
+ log.Printf("Error: failed to fetch from database: %q", err) |
+ continue |
+ } |
+ tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")}) |
+ } |
+ } |
+ var code string |
+ if len(tries) == 0 { |
+ code = DEFAULT_SAMPLE |
+ } else { |
+ code = getCode(tries[len(tries)-1].Hash) |
+ } |
+ if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name}); err != nil { |
+ log.Printf("ERROR: Failed to expand template: %q\n", err) |
+ } |
+ } else if r.Method == "POST" { |
+ name, err := newWorkspace() |
+ if err != nil { |
+ http.Error(w, "Failed to create a new workspace.", 500) |
+ return |
+ } |
+ http.Redirect(w, r, "/w/"+name, 302) |
+ } |
+} |
+ |
// hasPreProcessor returns true if any line in the code begins with a # char. |
func hasPreProcessor(code string) bool { |
lines := strings.Split(code, "\n") |
@@ -305,6 +450,11 @@ func hasPreProcessor(code string) bool { |
return false |
} |
+type TryRequest struct { |
+ Code string `json:"code"` |
+ Name string `json:"name"` |
+} |
+ |
// mainHandler handles the GET and POST of the main page. |
func mainHandler(w http.ResponseWriter, r *http.Request) { |
log.Printf("Main Handler: %q\n", r.URL.Path) |
@@ -340,18 +490,22 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { |
reportError(w, r, err, "Code too large.") |
return |
} |
- code := string(buf.Bytes()) |
- if hasPreProcessor(code) { |
+ request := TryRequest{} |
+ if err := json.Unmarshal(buf.Bytes(), &request); err != nil { |
+ reportError(w, r, err, "Coulnd't decode JSON.") |
+ return |
+ } |
+ if hasPreProcessor(request.Code) { |
err := fmt.Errorf("Found preprocessor macro in code.") |
reportError(w, r, err, "Preprocessor macros aren't allowed.") |
return |
} |
- hash, err := expandCode(LineNumbers(code)) |
+ hash, err := expandCode(LineNumbers(request.Code)) |
if err != nil { |
reportError(w, r, err, "Failed to write the code to compile.") |
return |
} |
- writeToDatabase(hash, code) |
+ writeToDatabase(hash, request.Code, request.Name) |
message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true) |
if err != nil { |
reportError(w, r, err, "Failed to compile the code:\n"+message) |
@@ -403,8 +557,10 @@ func mainHandler(w http.ResponseWriter, r *http.Request) { |
func main() { |
flag.Parse() |
http.HandleFunc("/i/", imageHandler) |
+ http.HandleFunc("/w/", workspaceHandler) |
http.HandleFunc("/recent/", recentHandler) |
http.HandleFunc("/css/", cssHandler) |
+ http.HandleFunc("/js/", jsHandler) |
http.HandleFunc("/", mainHandler) |
log.Fatal(http.ListenAndServe(*port, nil)) |
} |