diff --git a/demo/main.c b/demo/main.c
new file mode 100644
index 0000000..d913067
--- /dev/null
+++ b/demo/main.c
@@ -0,0 +1,400 @@
+#include "cweb.h"
+
+#if 0
+#define WL_STR(X) ((WL_String) { (X), (int) sizeof(X)-1})
+
+#define HTML_STR(X) html_str(HTTP_STR(#X))
+
+static HTTP_String html_str(HTTP_String str)
+{
+ str = cweb_trim(str);
+ if (str.len > 0 && str.ptr[0] == '(') {
+ str.ptr++;
+ str.len--;
+ }
+ if (str.len > 0 && str.ptr[str.len-1] == ')')
+ str.len--;
+ return str;
+}
+
+int load_file(char *file, char **data, long *size)
+{
+ FILE *f = fopen(file, "rb");
+ if (f == NULL) return -1;
+
+ fseek(f, 0, SEEK_END);
+ *size = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ *data = malloc(*size + 1);
+
+ fread(*data, 1, *size, f);
+ (*data)[*size] = '\0';
+
+ fclose(f);
+ return 0;
+}
+
+static void *alloc(WL_Arena *arena, int num, int align)
+{
+ int pad = -(uintptr_t) (arena->ptr + arena->cur) & (align-1);
+ if (arena->len - arena->cur < num + pad)
+ return NULL;
+
+ void *ptr = arena->ptr + arena->cur + pad;
+ arena->cur += num + pad;
+
+ return ptr;
+}
+
+static bool is_hex_digit(char c)
+{
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+}
+
+static int hex_digit_to_int(char c)
+{
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ return c - '0';
+}
+
+#define USERNAME_LIMIT 64
+
+bool valid_name(HTTP_String str)
+{
+ (void) str; // TODO
+ return true;
+}
+
+bool valid_email(HTTP_String str)
+{
+ (void) str; // TODO
+ return true;
+}
+
+bool valid_pass(HTTP_String str)
+{
+ (void) str; // TODO
+ return true;
+}
+
+bool valid_post_title(HTTP_String str)
+{
+ (void) str; // TODO
+ return true;
+}
+
+bool valid_post_content(HTTP_String str)
+{
+ (void) str; // TODO
+ return true;
+}
+
+bool valid_link(HTTP_String str)
+{
+ (void) str;
+ return true;
+}
+
+bool valid_comment_content(HTTP_String str)
+{
+ (void) str;
+ return true;
+}
+#endif
+
+static int user_exists(CWEB *cweb, HTTP_String name, HTTP_String pass)
+{
+ CWEB_QueryResult res = cweb_database_select(cweb, "SELECT id, hash FROM Users WHERE username=?", name);
+
+ int64_t user_id;
+ CWEB_PasswordHash hash;
+ int ret = cweb_next_query_row(&res, &user_id, &hash);
+ if (ret < 0) {
+ cweb_free_query_result(&res);
+ return -1;
+ }
+ if (ret == 0) {
+ cweb_free_query_result(&res);
+ return 0;
+ }
+
+ ret = check_password(pass.ptr, pass.len, hash);
+ if (ret < 0) {
+ cweb_free_query_result(&res);
+ return -500;
+ }
+ if (ret > 0) {
+ cweb_free_query_result(&res);
+ return -400;
+ }
+
+ return user_id;
+}
+
+static void endpoint_api_login(CWEB_Request *req)
+{
+ if (cweb_get_user_id(req) != -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ CWEB_String name = cweb_get_param_s(req, CWEB_STR("username"));
+ CWEB_String pass = cweb_get_param_s(req, CWEB_STR("password"));
+ if (!valid_name(name) || !valid_pass(pass)) {
+ cweb_respond_basic(req, 200, CWEB_STR("
Invalid credentials
"));
+ return;
+ }
+
+ int ret = user_exists(req->cweb, name, pass);
+ if (ret < 0) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid credentials
"));
+ return;
+ }
+ if (cweb_set_user_id(req, ret) < 0) {
+ // TODO
+ }
+
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+}
+
+static void endpoint_api_signup(CWEB_Request *req)
+{
+ if (cweb_get_user_id(req) != -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ CWEB_String name = cweb_get_param_s(req, CWEB_STR("username"));
+ CWEB_String email = cweb_get_param_s(req, CWEB_STR("email"));
+ CWEB_String pass1 = cweb_get_param_s(req, CWEB_STR("password1"));
+ CWEB_String pass2 = cweb_get_param_s(req, CWEB_STR("password2"));
+
+ if (!valid_name(name) || !valid_email(email) || !valid_pass(pass1)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid credentials
"));
+ return;
+ }
+
+ if (!cweb_streq(pass1, pass2)) {
+ cweb_respond_basic(req, 200, CWEB_STR("The password was repeated incorrectly
"));
+ return;
+ }
+
+ CWEB_PasswordHash hash;
+ int ret = cweb_hash_password(pass1.ptr, pass1.len, 12, &hash);
+ if (ret) {
+ cweb_respond_basic(req, 400, CWEB_STR("Internal error
"));
+ return;
+ }
+
+ int64_t insert_id = cweb_database_insert(req->cweb, "INSERT INTO Users(username, email, hash) VALUES (?, ?, ?)", name, email, hash);
+ if (insert_id < 0) {
+ // TODO: What if the user exists?
+ cweb_respond_basic(req, 400, CWEB_STR("Internal error
"));
+ return;
+ }
+ cweb_set_user_id(req, insert_id);
+
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+}
+
+static void endpoint_api_logout(CWEB_Request *req)
+{
+ cweb_set_current_user_id(req, -1);
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+}
+
+static void endpoint_api_post(CWEB_Request *req)
+{
+ int user_id = cweb_get_session_user_id(req);
+ if (user_id == -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ CWEB_String title = cweb_get_param_s(req, CWEB_STR("title"));
+ CWEB_String link = cweb_get_param_s(req, CWEB_STR("link"));
+ CWEB_String content = cweb_get_param_s(req, CWEB_STR("content"));
+ CWEB_String csrf = cweb_get_param_s(req, CWEB_STR("csrf"));
+
+ if (!cweb_streq(cweb_get_session_csrf(req), csrf)) {
+ cweb_respond_basic(req, 400, CWEB_STR("Invalid request"));
+ return;
+ }
+
+ if (!valid_post_title(title)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid title
"));
+ return;
+ }
+
+ if (!valid_link(link)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid link
"));
+ return;
+ }
+
+ if (!valid_post_content(content)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid content
"));
+ return;
+ }
+
+ bool is_link = false;
+ if (link.len > 0) {
+ is_link = true;
+ content = link;
+ }
+
+ int64_t insert_id = cweb_database_insert(req->cweb,
+ "INSERT INTO Posts(author, title, is_link, content) VALUES (?, ?, ?, ?)",
+ user_id, title, is_link, content);
+
+ if (insert_id < 0) {
+ cweb_respond_basic(req, 500, CWEB_STR("Internal error
"));
+ return;
+ }
+ int post_id = (int) insert_id;
+
+ cweb_respond_redirect(req, "/post?id=%d", post_id);
+}
+
+static void endpoint_api_comment(CWEB_Request *req)
+{
+ int user_id = cweb_get_session_user_id(req);
+ if (user_id == -1) {
+ cweb_respond_basic(req, 200, CWEB_STR("You are not logged in
"));
+ return;
+ }
+
+ int parent_post = cweb_get_param_i(req, CWEB_STR("parent_post"));
+ int parent_comment = cweb_get_param_i(req, CWEB_STR("parent_comment"));
+ CWEB_String content = cweb_get_param_s(req, CWEB_STR("content"));
+ CWEB_String csrf2 = cweb_get_param_s(req, CWEB_STR("csrf"));
+
+ if (!cweb_streq(cweb_get_session_csrf(req), csrf2)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid request
"));
+ return;
+ }
+
+ content = cweb_trim(content);
+ if (!valid_comment_content(content)) {
+ cweb_respond_basic(req, 200, CWEB_STR("Invalid content
"));
+ return;
+ }
+
+ int64_t insert_id;
+ if (parent_comment == -1) insert_id = cweb_database_insert(req->cweb, "INSERT INTO Comments(author, content, parent_post) VALUES (?, ?, ?)", user_id, content, parent_post);
+ else insert_id = cweb_database_insert(req->cweb, "INSERT INTO Comments(author, content, parent_post, parent_comment) VALUES (?, ?, ?, ?)", user_id, content, parent_post, parent_comment);
+ if (insert_id < 0) {
+ cweb_respond_basic(req, 200, CWEB_STR("Internal error
"));
+ return;
+ }
+
+ cweb_respond_redirect(req, "/post?id=%d", parent_post);
+}
+
+static void endpoint_index(CWEB_Request *req)
+{
+ cweb_respond_template(req, 200, CWEB_STR("pages/index.wl"), -1);
+}
+
+static void endpoint_write(CWEB_Request *req)
+{
+ if (cweb_get_current_user_id(req) == -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ cweb_respond_template(req, 200, CWEB_STR("pages/write.wl"), -1);
+}
+
+static void endpoint_login(CWEB_Request *req)
+{
+ if (cweb_get_current_user_id(req) != -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ cweb_respond_template(req, 200, CWEB_STR("pages/login.wl"), -1);
+}
+
+static void endpoint_signup(CWEB_Request *req)
+{
+ if (cweb_get_current_user_id(req) != -1) {
+ cweb_respond_redirect(req, CWEB_STR("/index"));
+ return;
+ }
+
+ cweb_respond_template(req, 200, CWEB_STR("pages/signup.wl"), -1);
+}
+
+static void endpoint_post(CWEB_Request *req)
+{
+ int post_id = cweb_request_get_parami(req, CWEB_STR("id"));
+ if (post_id < 0) {
+ cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
+ return;
+ }
+
+ CWEB_QueryResult res = cweb_database_select(req->cweb,
+ "SELECT COUNT(*) FROM Posts WHERE id=?", post_id);
+
+ int num;
+ if (cweb_next_query_row(&res, &num) != 1) {
+ cweb_respond_basic(req, 500, CWEB_STR(""));
+ cweb_free_query_result(&res);
+ return;
+ }
+ cweb_free_query_result(&res);
+
+ if (num < 0) {
+ cweb_respond_basic(req, 500, CWEB_STR(""));
+ return;
+ }
+
+ if (num == 0) {
+ cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
+ return;
+ }
+
+ cweb_respond_template(req, 200, CWEB_STR("pages/post.wl"), post_id);
+}
+
+static void endpoint_fallback(CWEB_Request *req)
+{
+ cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
+}
+
+int main(void)
+{
+ CWEB_String addr = CWEB_STR("127.0.0.1");
+ uint16_t port = 8080;
+ CWEB *cweb;
+
+ if (cweb_global_init() < 0)
+ return -1;
+
+ if (cweb_init(&cweb, addr, port) < 0)
+ return -1;
+
+ for (;;) {
+ CWEB_Request request = cweb_wait(&cweb);
+ cweb_add_endpoint(cweb, CWEB_STR("/api/login"), endpoint_api_login, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
+ cweb_add_endpoint(cweb, CWEB_STR("/api/signup"), endpoint_api_signup, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
+ cweb_add_endpoint(cweb, CWEB_STR("/api/logout"), endpoint_api_logout, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
+ cweb_add_endpoint(cweb, CWEB_STR("/api/post"), endpoint_api_post, CWEB_ENDPOINT_POST);
+ cweb_add_endpoint(cweb, CWEB_STR("/api/comment"), endpoint_api_comment, CWEB_ENDPOINT_POST);
+ cweb_add_endpoint(cweb, CWEB_STR("/index"), endpoint_index, CWEB_ENDPOINT_GET);
+ cweb_add_endpoint(cweb, CWEB_STR("/write"), endpoint_write, CWEB_ENDPOINT_GET);
+ cweb_add_endpoint(cweb, CWEB_STR("/login"), endpoint_login, CWEB_ENDPOINT_GET);
+ cweb_add_endpoint(cweb, CWEB_STR("/signup"), endpoint_signup, CWEB_ENDPOINT_GET);
+ cweb_add_endpoint(cweb, CWEB_STR("/post"), endpoint_post, CWEB_ENDPOINT_GET);
+ cweb_fallback_endpoint(cweb, endpoint_fallback);
+ }
+
+ cweb_run(&cweb);
+ cweb_free(&cweb);
+ cweb_global_free();
+ return 0;
+}
diff --git a/demo/pages/index.wl b/demo/pages/index.wl
new file mode 100644
index 0000000..57ef104
--- /dev/null
+++ b/demo/pages/index.wl
@@ -0,0 +1,67 @@
+include "pages/page.wl"
+
+let posts = $query("SELECT P.id, P.title, P.is_link, P.content, (SELECT COUNT(*) FROM Comments as C WHERE c.parent_post=P.id) as num_comments, CURRENT_TIMESTAMP as date FROM Posts as P")
+
+if posts == none:
+ posts = []
+
+let style =
+
+
+let main =
+
+
+ \if len(posts) == 0:
+ There are no posts yet!
+
+ \for post in posts: {
+
+ let link
+ if post.is_link != 0:
+ link = post.content
+ else
+ link = ["/post?id=", post.id]
+
+
+
+
+ \escape(post.date) | \escape(post.num_comments) comments
+
+
+ }
+
+
+page("Index", $login_user_id, style, main)
\ No newline at end of file
diff --git a/demo/pages/login.wl b/demo/pages/login.wl
new file mode 100644
index 0000000..6a5913b
--- /dev/null
+++ b/demo/pages/login.wl
@@ -0,0 +1,22 @@
+include "pages/page.wl"
+include "pages/login_and_signup_style.wl"
+
+let main =
+
+ Welcome back!
+
+
+
+
+
+
+page("Log-In", none, style, main)
diff --git a/demo/pages/login_and_signup_style.wl b/demo/pages/login_and_signup_style.wl
new file mode 100644
index 0000000..5a20fd6
--- /dev/null
+++ b/demo/pages/login_and_signup_style.wl
@@ -0,0 +1,74 @@
+let style =
+
\ No newline at end of file
diff --git a/demo/pages/notfound.wl b/demo/pages/notfound.wl
new file mode 100644
index 0000000..70d6ea4
--- /dev/null
+++ b/demo/pages/notfound.wl
@@ -0,0 +1,60 @@
+include "pages/page.wl"
+
+let style =
+
+
+let main =
+
+
+
404
+
Page Not Found
+
+ Looks like this page wandered off somewhere.
+
+
+
+
+page("Not Found", $login_user_id, style, main)
diff --git a/demo/pages/page.wl b/demo/pages/page.wl
new file mode 100644
index 0000000..4229124
--- /dev/null
+++ b/demo/pages/page.wl
@@ -0,0 +1,75 @@
+
+procedure page(title, login_user_id, style, main)
+
+
+
+ CN - \escape(title)
+
+ \(style)
+
+
+
+
+ \main
+
+
+
diff --git a/demo/pages/post.wl b/demo/pages/post.wl
new file mode 100644
index 0000000..47146f4
--- /dev/null
+++ b/demo/pages/post.wl
@@ -0,0 +1,246 @@
+include "pages/page.wl"
+
+let posts = $query("SELECT U.username, P.title, P.content FROM Posts as P, Users as U WHERE P.id=? AND U.id=P.author", $post_id)
+let comments = $query("SELECT C.id, U.username, C.content, C.parent_post, C.parent_comment FROM Comments as C, Users as U WHERE C.parent_post=? AND U.id=C.author", $post_id)
+
+let lookup = {}
+
+for comment in comments: {
+ comment.child = []
+ lookup[comment.id] = comment
+}
+
+let root_comments = []
+
+for comment in comments: {
+ if comment.parent_comment == none:
+ root_comments << comment
+ else
+ lookup[comment.parent_comment].child << comment
+}
+
+let post = posts[0]
+
+let style =
+
+
+let main =
+
+
+
+ \procedure render_comment(comment)
+
+
+ \if len(root_comments) == 0:
+
+ else for comment in root_comments:
+ render_comment(comment)
+
+
+page(post.title, $login_user_id, style, main)
\ No newline at end of file
diff --git a/demo/pages/post_old.wl b/demo/pages/post_old.wl
new file mode 100644
index 0000000..6984736
--- /dev/null
+++ b/demo/pages/post_old.wl
@@ -0,0 +1,73 @@
+include "pages/page.wl"
+
+let posts = $query("SELECT title, content FROM Posts WHERE id=?", $post_id)
+let comments = $query("SELECT C.id, U.username, C.content, C.parent_post, C.parent_comment FROM Comments as C, Users as U WHERE C.parent_post=? AND U.id == C.author", $post_id)
+
+let lookup = {}
+
+for comment in comments: {
+ comment.child = []
+ lookup[comment.id] = comment
+}
+
+let roots = []
+
+for comment in comments: {
+ if comment.parent_comment == none:
+ roots << comment
+ else
+ lookup[comment.parent_comment].child << comment
+}
+
+comments = roots
+
+let post = posts[0]
+
+let style =
+
+
+procedure render_comment(parent)
+
+
\parent.username
+
+ \parent.content
+
+
+ \if $login_user_id != none:
+
+ \for child in parent.child:
+ render_comment(child)
+
+
+
+let main =
+
+ \post.title
+ \post.content
+
+
+
+ \if len comments == 0:
+ No comments yet!
+ else for comment in comments:
+ render_comment(comment)
+
+
+page(post.title, $login_user_id, style, main)
\ No newline at end of file
diff --git a/demo/pages/signup.wl b/demo/pages/signup.wl
new file mode 100644
index 0000000..d8e6569
--- /dev/null
+++ b/demo/pages/signup.wl
@@ -0,0 +1,24 @@
+include "pages/page.wl"
+include "pages/login_and_signup_style.wl"
+
+let main =
+
+ Welcome!
+
+
+
+
+
+
+page("Log-In", none, style, main)
diff --git a/demo/pages/write.wl b/demo/pages/write.wl
new file mode 100644
index 0000000..325c8ea
--- /dev/null
+++ b/demo/pages/write.wl
@@ -0,0 +1,95 @@
+include "pages/page.wl"
+
+let style =
+
+
+let main =
+
+
+
+
+
+
+page("Write Post", $login_user_id, style, main)
\ No newline at end of file
+ reply +
+