Download raw body.
gotwebd: templates take 2
here's a third version; compared to the previous this imports the
template utility in the top-level directory and moves the tests under
regress as per stsp suggestion (thanks!)
i've also fixed a typo of the previous: in the template I called the
function gotweb_render_head/foot instead of header/footer.
diff refs/heads/main refs/heads/tmpl
commit - b546f5e4a2345f89a875526edc57cf729564decf
commit + b816a10d37a9833f793b5f6b6491b05a95960ab3
blob - bd9d446ba6e36728e565674927327fb69fc7b3c9
blob + 819941027860474c6e36a936822d7c2f9c640bbc
--- Makefile
+++ Makefile
@@ -7,7 +7,7 @@ SUBDIR += gotweb gotwebd gotd gotsh gotctl
.endif
.if make(clean) || make(obj) || make(release)
-SUBDIR += gotweb gotwebd gotd gotsh gotctl
+SUBDIR += gotweb gotwebd gotd gotsh gotctl template
.endif
.if make(tags) || make(cleandir)
@@ -33,13 +33,19 @@ web:
diff -u got-dist.txt got-dist.txt.new
rm got-dist.txt.new
+tmpl:
+ ${MAKE} -C template
+
+tmpl-regress:
+ ${MAKE} -C regress/template
+
web:
${MAKE} -C gotweb
web-install:
${MAKE} -C gotweb install
-webd:
+webd: tmpl
${MAKE} -C gotwebd
webd-install:
blob - 8744ffb84ade1da058f9e9837fdbbd91cd5130fc
blob + 1f2783a446360c7e3379bbca5be3bea0ee1e76c5
--- gotwebd/Makefile
+++ gotwebd/Makefile
@@ -1,4 +1,5 @@
.PATH:${.CURDIR}/../lib
+.PATH:${.CURDIR}/../template
SUBDIR = libexec
@@ -7,7 +8,7 @@ SRCS = config.c sockets.c log.c gotwebd.c parse.y pro
PROG = gotwebd
SRCS = config.c sockets.c log.c gotwebd.c parse.y proc.c \
- fcgi.c gotweb.c got_operations.c
+ fcgi.c gotweb.c got_operations.c tmpl.c pages.c
SRCS += blame.c commit_graph.c delta.c diff.c \
diffreg.c error.c fileindex.c object.c object_cache.c \
object_idset.c object_parse.c opentemp.c path.c pack.c \
@@ -21,9 +22,20 @@ MAN = ${PROG}.conf.5 ${PROG}.8
object_open_privsep.c read_gitconfig_privsep.c \
read_gotconfig_privsep.c pollfd.c reference_parse.c
+.if exists(${.CURDIR}/../template/obj/template)
+TEMPLATE = ${.CURDIR}/../template/obj/template
+.else
+TEMPLATE = ${.CURDIR}/../template/template
+.endif
+
+.SUFFIXES: .tmpl
+.tmpl.c:
+ ${TEMPLATE} -o $@ $<
+
MAN = ${PROG}.conf.5 ${PROG}.8
CPPFLAGS += -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}
+CPPFLAGS += -I${.CURDIR}/../template
LDADD += -lz -levent -lutil -lm
YFLAGS =
DPADD = ${LIBEVENT} ${LIBUTIL}
blob - 04e9e72ece31adeacc0aef6f6fb7cffbaace078a
blob + b4f85fc2121de39b9a1c7ff2f9fcab75f5b7da42
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -36,6 +36,7 @@
#include "proc.h"
#include "gotwebd.h"
+#include "tmpl.h"
size_t fcgi_parse_record(uint8_t *, size_t, struct request *);
void fcgi_parse_begin_request(uint8_t *, uint16_t, struct request *,
@@ -288,6 +289,21 @@ fcgi_vprintf(struct request *c, const char *fmt, va_li
}
int
+fcgi_puts(struct template *tp, const char *str)
+{
+ if (str == NULL)
+ return 0;
+ return fcgi_gen_binary_response(tp->tp_arg, str, strlen(str));
+}
+
+int
+fcgi_putc(struct template *tp, int ch)
+{
+ uint8_t c = ch;
+ return fcgi_gen_binary_response(tp->tp_arg, &c, 1);
+}
+
+int
fcgi_vprintf(struct request *c, const char *fmt, va_list ap)
{
char *str;
@@ -483,6 +499,7 @@ fcgi_cleanup_request(struct request *c)
event_del(&c->ev);
close(c->fd);
+ template_free(c->tp);
gotweb_free_transport(c->t);
free(c);
}
blob - fc0c94f6c50f1675561ef2a30f20c748df22416c
blob + 0ce90275bab1028c73efcf66d2d59a727f7ca7c7
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -51,11 +51,6 @@ enum gotweb_ref_tm {
#include "proc.h"
#include "gotwebd.h"
-enum gotweb_ref_tm {
- TM_DIFF,
- TM_LONG,
-};
-
static const struct querystring_keys querystring_keys[] = {
{ "action", ACTION },
{ "commit", COMMIT },
@@ -86,8 +81,6 @@ static const struct got_error *gotweb_render_header(st
char *);
static const struct got_error *gotweb_assign_querystring(struct querystring **,
char *, char *);
-static const struct got_error *gotweb_render_header(struct request *);
-static const struct got_error *gotweb_render_footer(struct request *);
static const struct got_error *gotweb_render_index(struct request *);
static const struct got_error *gotweb_init_repo_dir(struct repo_dir **,
const char *);
@@ -97,9 +90,7 @@ static const struct got_error *gotweb_render_navs(stru
struct server *, const char *, int);
static const struct got_error *gotweb_get_clone_url(char **, struct server *,
const char *, int);
-static const struct got_error *gotweb_render_navs(struct request *);
static const struct got_error *gotweb_render_blame(struct request *);
-static const struct got_error *gotweb_render_briefs(struct request *);
static const struct got_error *gotweb_render_commits(struct request *);
static const struct got_error *gotweb_render_diff(struct request *);
static const struct got_error *gotweb_render_summary(struct request *);
@@ -108,6 +99,8 @@ static void gotweb_free_querystring(struct querystring
static const struct got_error *gotweb_render_tree(struct request *);
static const struct got_error *gotweb_render_branches(struct request *);
+const struct got_error *gotweb_render_navs(struct request *);
+
static void gotweb_free_querystring(struct querystring *);
static void gotweb_free_repo_dir(struct repo_dir *);
@@ -195,11 +188,8 @@ render:
}
html = 1;
- error = gotweb_render_header(c);
- if (error) {
- log_warnx("%s: %s", __func__, error->msg);
+ if (gotweb_render_header(c->tp) == -1)
goto err;
- }
if (error2) {
error = error2;
@@ -215,11 +205,8 @@ render:
}
break;
case BRIEFS:
- error = gotweb_render_briefs(c);
- if (error) {
- log_warnx("%s: %s", __func__, error->msg);
+ if (gotweb_render_briefs(c->tp) == -1)
goto err;
- }
break;
case COMMITS:
error = gotweb_render_commits(c);
@@ -296,7 +283,7 @@ done:
return;
done:
if (html && srv != NULL)
- gotweb_render_footer(c);
+ gotweb_render_footer(c->tp);
}
struct server *
@@ -692,145 +679,7 @@ static const struct got_error *
return NULL;
}
-static const struct got_error *
-gotweb_render_header(struct request *c)
-{
- const struct got_error *err = NULL;
- struct server *srv = c->srv;
- struct querystring *qs = c->t->qs;
- int r;
-
- r = fcgi_printf(c, "<!doctype html>\n"
- "<html>\n"
- "<head>\n"
- "<title>%s</title>\n"
- "<meta charset='utf-8' />\n"
- "<meta name='viewport' content='initial-scale=.75' />\n"
- "<meta name='msapplication-TileColor' content='#da532c' />\n"
- "<meta name='theme-color' content='#ffffff'/>\n"
- "<link rel='apple-touch-icon' sizes='180x180'"
- " href='%sapple-touch-icon.png' />\n"
- "<link rel='icon' type='image/png' sizes='32x32'"
- " href='%sfavicon-32x32.png' />\n"
- "<link rel='icon' type='image/png' sizes='16x16'"
- " href='%sfavicon-16x16.png' />\n"
- "<link rel='manifest' href='%ssite.webmanifest'/>\n"
- "<link rel='mask-icon' href='%ssafari-pinned-tab.svg' />\n"
- "<link rel='stylesheet' type='text/css' href='%s%s' />\n"
- "</head>\n"
- "<body>\n"
- "<div id='gw_body'>\n"
- "<div id='header'>\n"
- "<div id='got_link'>"
- "<a href='%s' target='_blank'>"
- "<img src='%s%s' alt='logo' id='logo' />"
- "</a>\n"
- "</div>\n" /* #got_link */
- "</div>\n" /* #header */
- "<div id='site_path'>\n"
- "<div id='site_link'>\n"
- "<a href='?index_page=%d'>%s</a>",
- srv->site_name,
- c->script_name,
- c->script_name,
- c->script_name,
- c->script_name,
- c->script_name,
- c->script_name, srv->custom_css,
- srv->logo_url,
- c->script_name, srv->logo,
- qs->index_page, srv->site_link);
- if (r == -1)
- goto done;
-
- if (qs->path != NULL) {
- char *epath;
-
- if (fcgi_printf(c, " / ") == -1)
- goto done;
-
- err = gotweb_escape_html(&epath, qs->path);
- if (err)
- return err;
- r = gotweb_link(c, &(struct gotweb_url){
- .action = SUMMARY,
- .index_page = -1,
- .page = -1,
- .path = qs->path,
- }, "%s", epath);
- free(epath);
- if (r == -1)
- goto done;
- }
- if (qs->action != INDEX) {
- const char *action = "";
-
- switch (qs->action) {
- case BLAME:
- action = "blame";
- break;
- case BRIEFS:
- action = "briefs";
- break;
- case COMMITS:
- action = "commits";
- break;
- case DIFF:
- action = "diff";
- break;
- case SUMMARY:
- action = "summary";
- break;
- case TAG:
- action = "tag";
- break;
- case TAGS:
- action = "tags";
- break;
- case TREE:
- action = "tree";
- break;
- }
-
- if (fcgi_printf(c, " / %s", action) == -1)
- goto done;
- }
-
- fcgi_printf(c, "</div>\n" /* #site_path */
- "</div>\n" /* #site_link */
- "<div id='content'>\n");
-
-done:
- return NULL;
-}
-
-static const struct got_error *
-gotweb_render_footer(struct request *c)
-{
- const struct got_error *error = NULL;
- struct server *srv = c->srv;
- const char *siteowner = " ";
- char *escaped_owner = NULL;
-
- if (srv->show_site_owner) {
- error = gotweb_escape_html(&escaped_owner, srv->site_owner);
- if (error)
- return error;
- siteowner = escaped_owner;
- }
-
- fcgi_printf(c, "<div id='site_owner_wrapper'>\n"
- "<div id='site_owner'>%s</div>\n"
- "</div>\n" /* #site_owner_wrapper */
- "</div>\n" /* #content */
- "</div>\n" /* #gw_body */
- "</body>\n</html>\n", siteowner);
-
- free(escaped_owner);
- return NULL;
-}
-
-static const struct got_error *
+const struct got_error *
gotweb_render_navs(struct request *c)
{
const struct got_error *error = NULL;
@@ -996,7 +845,7 @@ gotweb_render_index(struct request *c)
struct dirent **sd_dent = NULL;
unsigned int d_cnt, d_i, d_disp = 0;
unsigned int d_skipped = 0;
- int r, type;
+ int type;
d = opendir(srv->repos_path);
if (d == NULL) {
@@ -1011,26 +860,9 @@ gotweb_render_index(struct request *c)
goto done;
}
- r = fcgi_printf(c, "<div id='index_header'>\n"
- "<div id='index_header_project'>Project</div>\n");
- if (r == -1)
+ if (gotweb_render_repo_table_hdr(c->tp) == -1)
goto done;
- if (srv->show_repo_description)
- if (fcgi_printf(c, "<div id='index_header_description'>"
- "Description</div>\n") == -1)
- goto done;
- if (srv->show_repo_owner)
- if (fcgi_printf(c, "<div id='index_header_owner'>"
- "Owner</div>\n") == -1)
- goto done;
- if (srv->show_repo_age)
- if (fcgi_printf(c, "<div id='index_header_age'>"
- "Last Change</div>\n") == -1)
- goto done;
- if (fcgi_printf(c, "</div>\n") == -1) /* #index_header */
- goto done;
-
for (d_i = 0; d_i < d_cnt; d_i++) {
if (srv->max_repos > 0 && t->prev_disp == srv->max_repos)
break;
@@ -1074,112 +906,9 @@ gotweb_render_index(struct request *c)
d_disp++;
t->prev_disp++;
- if (fcgi_printf(c, "<div class='index_wrapper'>\n"
- "<div class='index_project'>") == -1)
+ if (gotweb_render_repo_fragment(c->tp, repo_dir) == -1)
goto done;
- r = gotweb_link(c, &(struct gotweb_url){
- .action = SUMMARY,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name,
- }, "%s", repo_dir->name);
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, "</div>") == -1) /* .index_project */
- goto done;
-
- if (srv->show_repo_description) {
- r = fcgi_printf(c,
- "<div class='index_project_description'>\n"
- "%s</div>\n", repo_dir->description);
- if (r == -1)
- goto done;
- }
-
- if (srv->show_repo_owner) {
- r = fcgi_printf(c, "<div class='index_project_owner'>"
- "%s</div>\n", repo_dir->owner);
- if (r == -1)
- goto done;
- }
-
- if (srv->show_repo_age) {
- r = fcgi_printf(c, "<div class='index_project_age'>"
- "%s</div>\n", repo_dir->age);
- if (r == -1)
- goto done;
- }
-
- if (fcgi_printf(c, "<div class='navs_wrapper'>"
- "<div class='navs'>") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = SUMMARY,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name
- }, "summary");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, " | ") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = BRIEFS,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name
- }, "commit briefs");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, " | ") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = COMMITS,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name
- }, "commits");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, " | ") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = TAGS,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name
- }, "tags");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, " | ") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = TREE,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name
- }, "tree");
- if (r == -1)
- goto done;
-
- r = fcgi_printf(c, "</div>" /* .navs */
- "<div class='dotted_line'></div>\n"
- "</div>\n" /* .navs_wrapper */
- "</div>\n"); /* .index_wrapper */
- if (r == -1)
- goto done;
-
gotweb_free_repo_dir(repo_dir);
repo_dir = NULL;
t->next_disp++;
@@ -1264,143 +993,6 @@ gotweb_render_briefs(struct request *c)
}
static const struct got_error *
-gotweb_render_briefs(struct request *c)
-{
- const struct got_error *error = NULL;
- struct repo_commit *rc = NULL;
- struct server *srv = c->srv;
- struct transport *t = c->t;
- struct querystring *qs = t->qs;
- struct repo_dir *repo_dir = t->repo_dir;
- char *smallerthan, *newline;
- char *age = NULL, *author = NULL, *msg = NULL;
- int r;
-
- r = fcgi_printf(c, "<div id='briefs_title_wrapper'>\n"
- "<div id='briefs_title'>Commit Briefs</div>\n"
- "</div>\n" /* #briefs_title_wrapper */
- "<div id='briefs_content'>\n");
- if (r == -1)
- goto done;
-
- if (qs->action == SUMMARY) {
- qs->action = BRIEFS;
- error = got_get_repo_commits(c, D_MAXSLCOMMDISP);
- } else
- error = got_get_repo_commits(c, srv->max_commits_display);
- if (error)
- goto done;
-
- TAILQ_FOREACH(rc, &t->repo_commits, entry) {
- error = gotweb_get_time_str(&age, rc->committer_time, TM_DIFF);
- if (error)
- goto done;
-
- smallerthan = strchr(rc->author, '<');
- if (smallerthan)
- *smallerthan = '\0';
-
- newline = strchr(rc->commit_msg, '\n');
- if (newline)
- *newline = '\0';
-
- error = gotweb_escape_html(&author, rc->author);
- if (error)
- goto done;
- error = gotweb_escape_html(&msg, rc->commit_msg);
- if (error)
- goto done;
-
- r = fcgi_printf(c, "<div class='briefs_age'>%s</div>\n"
- "<div class='briefs_author'>%s</div>\n"
- "<div class='briefs_log'>",
- age, author);
- if (r == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = DIFF,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name,
- .commit = rc->commit_id,
- .headref = qs->headref,
- }, "%s", msg);
- if (r == -1)
- goto done;
-
- if (rc->refs_str) {
- char *refs;
-
- error = gotweb_escape_html(&refs, rc->refs_str);
- if (error)
- goto done;
- r = fcgi_printf(c,
- " <span class='refs_str'>(%s)</span>", refs);
- free(refs);
- if (r == -1)
- goto done;
- }
- if (fcgi_printf(c, "</div>\n") == -1) /* .briefs_log */
- goto done;
-
- r = fcgi_printf(c, "<div class='navs_wrapper'>\n"
- "<div class='navs'>");
- if (r == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = DIFF,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name,
- .commit = rc->commit_id,
- .headref = qs->headref,
- }, "diff");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, " | ") == -1)
- goto done;
-
- r = gotweb_link(c, &(struct gotweb_url){
- .action = TREE,
- .index_page = -1,
- .page = -1,
- .path = repo_dir->name,
- .commit = rc->commit_id,
- .headref = qs->headref,
- }, "tree");
- if (r == -1)
- goto done;
-
- if (fcgi_printf(c, "</div>\n" /* .navs */
- "</div>\n" /* .navs_wrapper */
- "<div class='dotted_line'></div>\n") == -1)
- goto done;
-
- free(age);
- age = NULL;
- free(author);
- author = NULL;
- free(msg);
- msg = NULL;
- }
-
- if (t->next_id || t->prev_id) {
- error = gotweb_render_navs(c);
- if (error)
- goto done;
- }
- fcgi_printf(c, "</div>\n"); /* #briefs_content */
-done:
- free(age);
- free(author);
- free(msg);
- return error;
-}
-
-static const struct got_error *
gotweb_render_commits(struct request *c)
{
const struct got_error *error = NULL;
@@ -1811,11 +1403,8 @@ gotweb_render_summary(struct request *c)
if (r == -1)
goto done;
- error = gotweb_render_briefs(c);
- if (error) {
- log_warnx("%s: %s", __func__, error->msg);
+ if (gotweb_render_briefs(c->tp) == -1)
goto done;
- }
error = gotweb_render_tags(c);
if (error) {
@@ -2177,8 +1766,8 @@ static inline const char *
return escaped;
}
-static inline const char *
-action_name(int action)
+const char *
+gotweb_action_name(int action)
{
switch (action) {
case BLAME:
@@ -2208,14 +1797,14 @@ static int
}
}
-static int
-gotweb_print_url(struct request *c, struct gotweb_url *url)
+int
+gotweb_render_url(struct request *c, struct gotweb_url *url)
{
const char *sep = "?", *action;
char *tmp;
int r;
- action = action_name(url->action);
+ action = gotweb_action_name(url->action);
if (action != NULL) {
if (fcgi_printf(c, "?action=%s", action) == -1)
return -1;
@@ -2309,7 +1898,7 @@ gotweb_link(struct request *c, struct gotweb_url *url,
if (fcgi_printf(c, "<a href='") == -1)
return -1;
- if (gotweb_print_url(c, url) == -1)
+ if (gotweb_render_url(c, url) == -1)
return -1;
if (fcgi_printf(c, "'>") == -1)
blob - 14426b3f4ad9787e7876dc233211e8fb8875cab6
blob + 64fce7c6d8ee3f57305034f0c0434cff991a82bd
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -202,10 +202,12 @@ struct request {
PRIV_FDS__MAX,
};
+struct template;
struct request {
struct socket *sock;
struct server *srv;
struct transport *t;
+ struct template *tp;
struct event ev;
struct event tmo;
@@ -415,6 +417,11 @@ extern struct gotwebd *gotwebd_env;
ACTIONS__MAX,
};
+enum gotweb_ref_tm {
+ TM_DIFF,
+ TM_LONG,
+};
+
extern struct gotwebd *gotwebd_env;
/* sockets.c */
@@ -432,6 +439,8 @@ int gotweb_link(struct request *, struct gotweb_url *,
const struct got_error *gotweb_get_time_str(char **, time_t, int);
const struct got_error *gotweb_init_transport(struct transport **);
const struct got_error *gotweb_escape_html(char **, const char *);
+const char *gotweb_action_name(int);
+int gotweb_render_url(struct request *, struct gotweb_url *);
int gotweb_link(struct request *, struct gotweb_url *, const char *, ...)
__attribute__((__format__(printf, 3, 4)))
__attribute__((__nonnull__(3)));
@@ -440,6 +449,13 @@ void gotweb_free_transport(struct transport *);
void gotweb_process_request(struct request *);
void gotweb_free_transport(struct transport *);
+/* pages.tmpl */
+int gotweb_render_header(struct template *);
+int gotweb_render_footer(struct template *);
+int gotweb_render_repo_table_hdr(struct template *);
+int gotweb_render_repo_fragment(struct template *, struct repo_dir *);
+int gotweb_render_briefs(struct template *);
+
/* parse.y */
int parse_config(const char *, struct gotwebd *);
int cmdline_symset(char *);
@@ -450,6 +466,8 @@ int fcgi_vprintf(struct request *, const char *, va_li
void fcgi_cleanup_request(struct request *);
void fcgi_create_end_record(struct request *);
void dump_fcgi_record(const char *, struct fcgi_record_header *);
+int fcgi_puts(struct template *, const char *);
+int fcgi_putc(struct template *, int);
int fcgi_vprintf(struct request *, const char *, va_list);
int fcgi_printf(struct request *, const char *, ...)
__attribute__((__format__(printf, 2, 3)))
blob - /dev/null
blob + 0d49898e174e9bfad77b158729eb04ab949bd736 (mode 644)
--- /dev/null
+++ gotwebd/pages.tmpl
@@ -0,0 +1,302 @@
+{!
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery <tracey@traceyemery.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+
+#include <event.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <imsg.h>
+
+#include "proc.h"
+
+#include "gotwebd.h"
+#include "tmpl.h"
+
+const struct got_error *gotweb_render_navs(struct request *);
+
+static int
+gotweb_render_age(struct template *tp, time_t time, int ref_tm)
+{
+ const struct got_error *err;
+ char *age;
+ int r;
+
+ err = gotweb_get_time_str(&age, time, ref_tm);
+ if (err)
+ return 0;
+ r = tp->tp_puts(tp, age);
+ free(age);
+ return r;
+}
+
+!}
+
+{{ define gotweb_render_header(struct template *tp) }}
+{!
+ struct request *c = tp->tp_arg;
+ struct server *srv = c->srv;
+ struct querystring *qs = c->t->qs;
+ struct gotweb_url u_path;
+ const char *prfx = c->script_name;
+ const char *css = srv->custom_css;
+
+ memset(&u_path, 0, sizeof(u_path));
+ u_path.index_page = -1;
+ u_path.page = -1;
+ u_path.action = SUMMARY;
+!}
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>{{ srv->site_name }}</title>
+ <meta name="viewport" content="initial-scale=.75" />
+ <meta name="msapplication-TileColor" content="#da532c" />
+ <meta name="theme-color" content="#ffffff"/>
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ prfx }}apple-touch-icon.png" />
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ prfx }}favicon-32x32.png" />
+ <link rel="icon" type="image/png" sizes="16x16" href="{{ prfx }}favicon-16x16.png" />
+ <link rel="manifest" href="{{ prfx }}site.webmanifest"/>
+ <link rel="mask-icon" href="{{ prfx }}safari-pinned-tab.svg" />
+ <link rel="stylesheet" type="text/css" href="{{ prfx }}{{ css }}" />
+ </head>
+ <body>
+ <div id="gw_body">
+ <div id="header">
+ <div id="got_link">
+ <a href="{{ srv->logo_url }}" target="_blank">
+ <img src="{{ prfx }}{{ srv->logo }}" />
+ </a>
+ </div>
+ </div>
+ <div id="site_path">
+ <div id="site_link">
+ <a href="?index_page={{ printf "%d", qs->index_page }}">
+ {{ srv->site_link }}
+ </a>
+ {{ if qs->path }}
+ {! u_path.path = qs->path; !}
+ {{ " / " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &u_path)}}">
+ {{ qs->path }}
+ </a>
+ {{ end }}
+ {{ if qs->action != INDEX }}
+ {{ " / " }}{{ gotweb_action_name(qs->action) }}
+ {{ end }}
+ </div>
+ </div>
+ <div id="content">
+{{ end }}
+
+{{ define gotweb_render_footer(struct template *tp) }}
+{!
+ struct request *c = tp->tp_arg;
+ struct server *srv = c->srv;
+!}
+ <div id="site_owner_wrapper">
+ <div id="site_owner">
+ {{ if srv->show_site_owner }}
+ {{ srv->site_owner }}
+ {{ end }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
+{{ end }}
+
+{{ define gotweb_render_repo_table_hdr(struct template *tp) }}
+{!
+ struct request *c = tp->tp_arg;
+ struct server *srv = c->srv;
+!}
+<div id="index_header">
+ <div id="index_header_project">
+ Project
+ </div>
+ {{ if srv->show_repo_description }}
+ <div id="index_header_description">
+ Description
+ </div>
+ {{ end }}
+ {{ if srv->show_repo_owner }}
+ <div id="index_header_owner">
+ Owner
+ </div>
+ {{ end }}
+ {{ if srv->show_repo_age }}
+ <div id="index_header_age">
+ Last Change
+ </div>
+ {{ end }}
+</div>
+{{ end }}
+
+{{ define gotweb_render_repo_fragment(struct template *tp, struct repo_dir *repo_dir) }}
+{!
+ struct request *c = tp->tp_arg;
+ struct server *srv = c->srv;
+ struct gotweb_url summary = {
+ .action = SUMMARY,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ }, briefs = {
+ .action = BRIEFS,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ }, commits = {
+ .action = COMMITS,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ }, tags = {
+ .action = TAGS,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ }, tree = {
+ .action = TREE,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ };
+!}
+<div class="index_wrapper">
+ <div class="index_project">
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &summary) }}">{{ repo_dir->name }}</a>
+ </div>
+ {{ if srv->show_repo_description }}
+ <div class="index_project_description">
+ {{ repo_dir->description }}
+ </div>
+ {{ end }}
+ {{ if srv->show_repo_owner }}
+ <div class="index_project_owner">
+ {{ repo_dir->owner }}
+ </div>
+ {{ end }}
+ {{ if srv->show_repo_age }}
+ <div class="index_project_age">
+ {{ repo_dir->age }}
+ </div>
+ {{ end }}
+ <div class="navs_wrapper">
+ <div class="navs">
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &summary) }}">summary</a>
+ {{ " | " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &briefs) }}">briefs</a>
+ {{ " | " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &commits) }}">commits</a>
+ {{ " | " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &tags) }}">tags</a>
+ {{ " | " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &tree) }}">tree</a>
+ </div>
+ <div class="dotted_line"></div>
+ </div>
+</div>
+{{ end }}
+
+{{ define gotweb_render_briefs(struct template *tp) }}
+{!
+ const struct got_error *error;
+ struct request *c = tp->tp_arg;
+ struct server *srv = c->srv;
+ struct transport *t = c->t;
+ struct querystring *qs = c->t->qs;
+ struct repo_commit *rc;
+ struct repo_dir *repo_dir = t->repo_dir;
+ struct gotweb_url diff_url, tree_url;
+ char *tmp;
+
+ diff_url = (struct gotweb_url){
+ .action = DIFF,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ .headref = qs->headref,
+ };
+ tree_url = (struct gotweb_url){
+ .action = TREE,
+ .index_page = -1,
+ .page = -1,
+ .path = repo_dir->name,
+ .headref = qs->headref,
+ };
+
+ if (qs->action == SUMMARY) {
+ qs->action = BRIEFS;
+ error = got_get_repo_commits(c, D_MAXSLCOMMDISP);
+ } else
+ error = got_get_repo_commits(c, srv->max_commits_display);
+ if (error)
+ return -1;
+!}
+<div id="briefs_title_wrapper">
+ <div id="briefs_title">Commit Briefs</div>
+</div>
+<div id="briefs_content">
+ {{ tailq-foreach rc &t->repo_commits entry }}
+ {!
+ diff_url.commit = rc->commit_id;
+ tree_url.commit = rc->commit_id;
+
+ tmp = strchr(rc->author, '<');
+ if (tmp)
+ *tmp = '\0';
+
+ tmp = strchr(rc->commit_msg, '\n');
+ if (tmp)
+ *tmp = '\0';
+ !}
+ <div class="briefs_age">
+ {{ render gotweb_render_age(tp, rc->committer_time, TM_DIFF) }}
+ </div>
+ <div class="briefs_author">
+ {{ rc->author }}
+ </div>
+ <div class="briefs_log">
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &diff_url) }}">
+ {{ rc->commit_msg }}
+ </a>
+ {{ if rc->refs_str }}
+ {{ " " }} <span class="refs_str">({{ rc->refs_str }})</span>
+ {{ end }}
+ </a>
+ </div>
+ <div class="navs_wrapper">
+ <div class="navs">
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &diff_url) }}">diff</a>
+ {{ " | " }}
+ <a href="{{ render gotweb_render_url(tp->tp_arg, &tree_url) }}">tree</a>
+ </div>
+ </div>
+ <div class="dotted_line"></div>
+ {{ end }}
+ {{ if t->next_id || t->prev_id }}
+ {! gotweb_render_navs(c); !}
+ {{ end }}
+</div>
+{{ end }}
blob - c0691c5575dca8c931af0e8e30d9e086b5a5bccc
blob + cfba5dfb0e7368b0b37738d2eb71b53d437db169
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -55,6 +55,7 @@
#include "proc.h"
#include "gotwebd.h"
+#include "tmpl.h"
#define SOCKS_BACKLOG 5
#define MAXIMUM(a, b) (((a) > (b)) ? (a) : (b))
@@ -619,6 +620,15 @@ sockets_socket_accept(int fd, short event, void *arg)
return;
}
+ c->tp = template(c, fcgi_puts, fcgi_putc);
+ if (c->tp == NULL) {
+ log_warn("%s", __func__);
+ close(s);
+ cgi_inflight--;
+ free(c);
+ return;
+ }
+
c->fd = s;
c->sock = sock;
memcpy(c->priv_fd, sock->priv_fd, sizeof(c->priv_fd));
blob - /dev/null
blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + c2d0fad50754e9ec3740571ad5e77d7dc475ab26 (mode 644)
--- /dev/null
+++ regress/template/01-noise-only.tmpl
@@ -0,0 +1,2 @@
+only
+noise
blob - /dev/null
blob + c35618a4287faab518c419c4bbbe06ecbca7f157 (mode 644)
--- /dev/null
+++ regress/template/02-only-verbatim.tmpl
@@ -0,0 +1,17 @@
+{!
+#include <stdio.h>
+
+#include "tmpl.h"
+!}
+
+noise {! /* woops */ !}
+here
+
+{!
+int
+main(void)
+{
+ puts("hello, world!");
+ return (0);
+}
+!}
blob - /dev/null
blob + 270c611ee72c567bc1b2abec4cbc345bab9f15ba (mode 644)
--- /dev/null
+++ regress/template/02.expected
@@ -0,0 +1 @@
+hello, world!
blob - /dev/null
blob + eee67b419ef25bca620f07fbef7169020bd6a490 (mode 644)
--- /dev/null
+++ regress/template/03-block.tmpl
@@ -0,0 +1,24 @@
+{!
+#include <stdlib.h>
+
+#include "tmpl.h"
+
+int base(struct template *, const char *);
+!}
+
+{{ define base(struct template *tp, const char *title) }}
+{! char *foo = NULL; !}
+<!doctype html>
+<html>
+ <head>
+ <title>{{ title }}</title>
+ </head>
+ <body> {! /* TODO: frobnicate this line! */ !}
+ <h1>{{ title }}</h1>
+ {{ " | " }}
+ {{ "other stuff" }}
+ </body>
+</html>
+{{ finally }}
+{! free(foo); !}
+{{ end }}
blob - /dev/null
blob + 0f766820b9e5de1343ca8034bea3b2b0a4418221 (mode 644)
--- /dev/null
+++ regress/template/03.expected
@@ -0,0 +1,2 @@
+<!doctype html><html><head><title> *hello* </title></head><body> <h1> *hello* </h1> | other stuff</body></html>
+<!doctype html><html><head><title><hello></title></head><body> <h1><hello></h1> | other stuff</body></html>
blob - /dev/null
blob + 5494a40b208b5f39f24c8a161095e089309d47fb (mode 644)
--- /dev/null
+++ regress/template/04-flow.tmpl
@@ -0,0 +1,33 @@
+{!
+#include <stdlib.h>
+#include <string.h>
+
+#include "tmpl.h"
+
+int base(struct template *, const char *);
+!}
+
+{{ define base(struct template *tp, const char *title) }}
+{! char *foo = NULL; !}
+<!doctype html>
+<html>
+ <head>
+ <title>{{ title }}</title>
+ </head>
+ <body>
+ <h1>{{ title }}</h1>
+ {{ if strchr(title, '*') != NULL }}
+ <p>"{{ title }}" has a '*' in it</p>
+ {{ if 1 }}
+ <p>tautology!</p>
+ {{ end }}
+ {{ else if strchr(title, '=') != NULL }}
+ <p>"{{ title }}" has a '=' in it!</p>
+ {{ else }}
+ <p>"{{ title }}" doesn't have a '*' in it</p>
+ {{ end }}
+ </body>
+</html>
+{{ finally }}
+{! free(foo); !}
+{{ end }}
blob - /dev/null
blob + 32240e27d9a6dbeeb395f7e760e2a043e29b939c (mode 644)
--- /dev/null
+++ regress/template/04.expected
@@ -0,0 +1,2 @@
+<!doctype html><html><head><title> *hello* </title></head><body><h1> *hello* </h1><p>" *hello* " has a '*' in it</p><p>tautology!</p></body></html>
+<!doctype html><html><head><title><hello></title></head><body><h1><hello></h1><p>"<hello>" doesn't have a '*' in it</p></body></html>
blob - /dev/null
blob + 5cb351ad4205991f16bb17f7caffc03c7594f15e (mode 644)
--- /dev/null
+++ regress/template/05-loop.tmpl
@@ -0,0 +1,43 @@
+{!
+#include <sys/queue.h>
+#include <string.h>
+#include "lists.h"
+#include "tmpl.h"
+
+int list(struct template *, struct tailhead *);
+int base(struct template *, struct tailhead *);
+
+!}
+
+{{ define base(struct template *tp, struct tailhead *head) }}
+<!doctype html>
+<html>
+ <body>
+ {{ render list(tp, head) }}
+ </body>
+</html>
+{{ end }}
+
+{{ define list(struct template *tp, struct tailhead *head) }}
+{!
+ struct entry *np;
+ int i;
+!}
+ {{ if !TAILQ_EMPTY(head) }}
+ <p>items:</p>
+ <ul>
+ {{ tailq-foreach np head entries }}
+ <li>{{ np->text }}</li>
+ {{ end }}
+ </ul>
+ {{ else }}
+ <p>no items</p>
+ {{ end }}
+
+ <p>
+ {{ for i = 0; i < 3; ++i }}
+ hello{{ " " }}
+ {{ end }}
+ world!
+ </p>
+{{ end }}
blob - /dev/null
blob + d4c20d67eeee5e6e0217d8abd4c19f2d440ba6d9 (mode 644)
--- /dev/null
+++ regress/template/05.expected
@@ -0,0 +1,2 @@
+<!doctype html><html><body><p>items:</p><ul><li>1</li><li>2</li></ul><p>hello hello hello world!</p></body></html>
+<!doctype html><html><body><p>no items</p><p>hello hello hello world!</p></body></html>
blob - /dev/null
blob + 8a1d436209ad2a5cd22583fbb5c7c5b8cf06fdd7 (mode 644)
--- /dev/null
+++ regress/template/06-escape.tmpl
@@ -0,0 +1,19 @@
+{!
+#include <stdlib.h>
+
+#include "tmpl.h"
+
+int base(struct template *, const char *);
+!}
+
+{{ define base(struct template *tp, const char *title) }}
+<!doctype html>
+<html>
+ <head>
+ <title>{{ title | urlescape }}</title>
+ </head>
+ <body>
+ <h1>{{ title | unsafe }}</h1>
+ </body>
+</html>
+{{ end }}
blob - /dev/null
blob + 6a9d734b454093206236753143743d95d5e473af (mode 644)
--- /dev/null
+++ regress/template/06.expected
@@ -0,0 +1,2 @@
+<!doctype html><html><head><title>%20*hello*%20</title></head><body><h1> *hello* </h1></body></html>
+<!doctype html><html><head><title><hello></title></head><body><h1><hello></h1></body></html>
blob - /dev/null
blob + 0ad8f7070ecbf501efdda6004283ba83ce93ff15 (mode 644)
--- /dev/null
+++ regress/template/07-printf.tmpl
@@ -0,0 +1,13 @@
+{!
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl.h"
+
+int base(struct template *, const char *);
+
+!}
+
+{{ define base(struct template *tp, const char *title) }}
+{{ printf "%.2s:\t%d\n", title, 42 }}
+{{ end }}
blob - /dev/null
blob + 681daf3915b708a380ddc75467b47beceff9b367 (mode 644)
--- /dev/null
+++ regress/template/07.expected
@@ -0,0 +1,4 @@
+ *: 42
+
+<h: 42
+
blob - /dev/null
blob + f781b7ac44f61c8065695ac89ec64680ac84268c (mode 644)
--- /dev/null
+++ regress/template/Makefile
@@ -0,0 +1,55 @@
+REGRESS_TARGETS = 00-empty \
+ 01-noise-only \
+ 02-only-verbatim \
+ 03-block \
+ 04-flow \
+ 05-loop \
+ 06-escape \
+ 07-printf
+
+REGRESS_CLEANUP = clean-comp
+NO_OBJ = Yes
+
+CFLAGS += -I../../template
+
+.PATH:../../template
+
+clean-comp:
+ rm -f t got 0*.[cdo] runbase.[do] runlist.[do] tmpl.*
+
+.SUFFIXES: .tmpl .c .o
+
+.tmpl.c:
+ ../../template/obj/template -o $@ $<
+
+00-empty:
+ ../../template/obj/template 00-empty.tmpl >/dev/null
+
+01-noise-only:
+ ../../template/obj/template 01-noise-only.tmpl >/dev/null
+
+02-only-verbatim: 02-only-verbatim.o tmpl.o
+ ${CC} 02-only-verbatim.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/02.expected got
+
+03-block: 03-block.o runbase.o tmpl.o
+ ${CC} 03-block.o runbase.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/03.expected got
+
+04-flow: 04-flow.o runbase.o tmpl.o
+ ${CC} 04-flow.o runbase.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/04.expected got
+
+05-loop: 05-loop.o runlist.o tmpl.o
+ ${CC} 05-loop.o runlist.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/05.expected got
+
+06-escape: 06-escape.o runbase.o tmpl.o
+ ${CC} 06-escape.o runbase.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/06.expected got
+
+07-printf: 07-printf.o runbase.o tmpl.o
+ ${CC} 07-printf.o runbase.o tmpl.o -o t && ./t > got
+ diff -u ${.CURDIR}/07.expected got
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 7229706ecd6bfb0bb2f38be92950b7082f7a6c41 (mode 644)
--- /dev/null
+++ regress/template/lists.h
@@ -0,0 +1,7 @@
+#include <sys/queue.h>
+
+TAILQ_HEAD(tailhead, entry);
+struct entry {
+ char *text;
+ TAILQ_ENTRY(entry) entries;
+};
blob - /dev/null
blob + 0500bcadda792968ed1a7c8294ba9761ecc4ff71 (mode 644)
--- /dev/null
+++ regress/template/runbase.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl.h"
+
+int base(struct template *, const char *title);
+int my_putc(struct template *, int);
+int my_puts(struct template *, const char *);
+
+int
+my_putc(struct template *tp, int c)
+{
+ FILE *fp = tp->tp_arg;
+
+ if (putc(c, fp) < 0)
+ return (-1);
+
+ return (0);
+}
+
+int
+my_puts(struct template *tp, const char *s)
+{
+ FILE *fp = tp->tp_arg;
+
+ if (fputs(s, fp) < 0)
+ return (-1);
+
+ return (0);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct template *tp;
+
+ if ((tp = template(stdout, my_puts, my_putc)) == NULL)
+ err(1, "template");
+
+ if (base(tp, " *hello* ") == -1)
+ return (1);
+ puts("");
+
+ if (base(tp, "<hello>") == -1)
+ return (1);
+ puts("");
+
+ free(tp);
+ return (0);
+}
blob - /dev/null
blob + 0ec0a95185c3eccde7793744bde73bc5d87efb82 (mode 644)
--- /dev/null
+++ regress/template/runlist.c
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl.h"
+#include "lists.h"
+
+int base(struct template *, struct tailhead *);
+int my_putc(struct template *, int);
+int my_puts(struct template *, const char *);
+
+int
+my_putc(struct template *tp, int c)
+{
+ FILE *fp = tp->tp_arg;
+
+ if (putc(c, fp) < 0)
+ return (-1);
+
+ return (0);
+}
+
+int
+my_puts(struct template *tp, const char *s)
+{
+ FILE *fp = tp->tp_arg;
+
+ if (fputs(s, fp) < 0)
+ return (-1);
+
+ return (0);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct template *tp;
+ struct tailhead head;
+ struct entry *np;
+ int i;
+
+ if ((tp = template(stdout, my_puts, my_putc)) == NULL)
+ err(1, "template");
+
+ TAILQ_INIT(&head);
+ for (i = 0; i < 2; ++i) {
+ if ((np = calloc(1, sizeof(*np))) == NULL)
+ err(1, "calloc");
+ if (asprintf(&np->text, "%d", i+1) == -1)
+ err(1, "asprintf");
+ TAILQ_INSERT_TAIL(&head, np, entries);
+ }
+
+ if (base(tp, &head) == -1)
+ return (1);
+ puts("");
+
+ while ((np = TAILQ_FIRST(&head))) {
+ TAILQ_REMOVE(&head, np, entries);
+ free(np->text);
+ free(np);
+ }
+
+ if (base(tp, &head) == -1)
+ return (1);
+ puts("");
+
+ free(tp);
+
+ return (0);
+}
blob - /dev/null
blob + 7e95545a3d5b04f4123fd275efd2ad5186d17885 (mode 644)
--- /dev/null
+++ template/Makefile
@@ -0,0 +1,6 @@
+PROG= template
+SRCS= template.c tmpl.c parse.y
+
+MAN= template.1 template.7
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 34f42f83d0770ec2ab269538aac80935c414dfec (mode 644)
--- /dev/null
+++ template/parse.y
@@ -0,0 +1,737 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2007-2016 Reyk Floeter <reyk@openbsd.org>
+ * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl. All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier. All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt. All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+%{
+
+#include <sys/queue.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifndef nitems
+#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+ TAILQ_ENTRY(file) entry;
+ FILE *stream;
+ char *name;
+ size_t ungetpos;
+ size_t ungetsize;
+ unsigned char *ungetbuf;
+ int eof_reached;
+ int lineno;
+ int errors;
+} *file, *topfile;
+int parse(FILE *, const char *);
+struct file *pushfile(const char *, int);
+int popfile(void);
+int yyparse(void);
+int yylex(void);
+int yyerror(const char *, ...)
+ __attribute__((__format__ (printf, 1, 2)))
+ __attribute__((__nonnull__ (1)));
+int kw_cmp(const void *, const void *);
+int lookup(char *);
+int igetc(void);
+int lgetc(int);
+void lungetc(int);
+int findeol(void);
+
+void dbg(void);
+void printq(const char *);
+
+extern int nodebug;
+
+static FILE *fp;
+
+static int block;
+static int in_define;
+static int errors;
+static int lastline = -1;
+
+typedef struct {
+ union {
+ char *string;
+ } v;
+ int lineno;
+} YYSTYPE;
+
+%}
+
+%token DEFINE ELSE END ERROR FINALLY FOR IF INCLUDE PRINTF
+%token RENDER TQFOREACH UNSAFE URLESCAPE
+%token <v.string> STRING
+%type <v.string> string
+%type <v.string> stringy
+
+%%
+
+grammar : /* empty */
+ | grammar include
+ | grammar verbatim
+ | grammar block
+ | grammar error { file->errors++; }
+ ;
+
+include : INCLUDE STRING {
+ struct file *nfile;
+
+ if ((nfile = pushfile($2, 0)) == NULL) {
+ yyerror("failed to include file %s", $2);
+ free($2);
+ YYERROR;
+ }
+ free($2);
+
+ file = nfile;
+ lungetc('\n');
+ }
+ ;
+
+verbatim : '!' verbatim1 '!' {
+ if (in_define) {
+ /* TODO: check template status and exit in case */
+ }
+ }
+ ;
+
+verbatim1 : /* empty */
+ | verbatim1 STRING {
+ if (*$2 != '\0') {
+ dbg();
+ fprintf(fp, "%s\n", $2);
+ }
+ free($2);
+ }
+ ;
+
+verbatims : /* empty */
+ | verbatims verbatim
+ ;
+
+raw : STRING {
+ dbg();
+ fprintf(fp, "if ((tp_ret = tp->tp_puts(tp, ");
+ printq($1);
+ fputs(")) == -1) goto err;\n", fp);
+
+ free($1);
+ }
+ ;
+
+block : define body end {
+ fputs("err:\n", fp);
+ fputs("return tp_ret;\n", fp);
+ fputs("}\n", fp);
+ in_define = 0;
+ }
+ | define body finally end {
+ fputs("return tp_ret;\n", fp);
+ fputs("}\n", fp);
+ in_define = 0;
+ }
+ ;
+
+define : '{' DEFINE string '}' {
+ in_define = 1;
+
+ dbg();
+ fprintf(fp, "int\n%s\n{\n", $3);
+ fputs("int tp_ret = 0;\n", fp);
+
+ free($3);
+ }
+ ;
+
+body : /* empty */
+ | body verbatim
+ | body raw
+ | body special
+ ;
+
+special : '{' RENDER string '}' {
+ dbg();
+ fprintf(fp, "if ((tp_ret = %s) == -1) goto err;\n",
+ $3);
+ free($3);
+ }
+ | printf
+ | if body endif { fputs("}\n", fp); }
+ | loop
+ | '{' string '|' UNSAFE '}' {
+ dbg();
+ fprintf(fp,
+ "if ((tp_ret = tp->tp_puts(tp, %s)) == -1)\n",
+ $2);
+ fputs("goto err;\n", fp);
+ free($2);
+ }
+ | '{' string '|' URLESCAPE '}' {
+ dbg();
+ fprintf(fp,
+ "if ((tp_ret = tp_urlescape(tp, %s)) == -1)\n",
+ $2);
+ fputs("goto err;\n", fp);
+ free($2);
+ }
+ | '{' string '}' {
+ dbg();
+ fprintf(fp,
+ "if ((tp_ret = tp->tp_escape(tp, %s)) == -1)\n",
+ $2);
+ fputs("goto err;\n", fp);
+ free($2);
+ }
+ ;
+
+printf : '{' PRINTF {
+ dbg();
+ fprintf(fp, "if (asprintf(&tp->tp_tmp, ");
+ } printfargs '}' {
+ fputs(") == -1)\n", fp);
+ fputs("goto err;\n", fp);
+ fputs("if ((tp_ret = tp->tp_escape(tp, tp->tp_tmp)) "
+ "== -1)\n", fp);
+ fputs("goto err;\n", fp);
+ fputs("free(tp->tp_tmp);\n", fp);
+ fputs("tp->tp_tmp = NULL;\n", fp);
+ }
+ ;
+
+printfargs : /* empty */
+ | printfargs STRING {
+ fprintf(fp, " %s", $2);
+ free($2);
+ }
+ ;
+
+if : '{' IF stringy '}' {
+ dbg();
+ fprintf(fp, "if (%s) {\n", $3);
+ free($3);
+ }
+ ;
+
+endif : end
+ | else body end
+ | elsif body endif
+ ;
+
+elsif : '{' ELSE IF stringy '}' {
+ dbg();
+ fprintf(fp, "} else if (%s) {\n", $4);
+ free($4);
+ }
+ ;
+
+else : '{' ELSE '}' {
+ dbg();
+ fputs("} else {\n", fp);
+ }
+ ;
+
+loop : '{' FOR stringy '}' {
+ fprintf(fp, "for (%s) {\n", $3);
+ free($3);
+ } body end {
+ fputs("}\n", fp);
+ }
+ | '{' TQFOREACH STRING STRING STRING '}' {
+ fprintf(fp, "TAILQ_FOREACH(%s, %s, %s) {\n",
+ $3, $4, $5);
+ free($3);
+ free($4);
+ free($5);
+ } body end {
+ fputs("}\n", fp);
+ }
+ ;
+
+end : '{' END '}'
+ ;
+
+finally : '{' FINALLY '}' {
+ dbg();
+ fputs("err:\n", fp);
+ } verbatims
+ ;
+
+string : STRING string {
+ if (asprintf(&$$, "%s %s", $1, $2) == -1)
+ err(1, "asprintf");
+ free($1);
+ free($2);
+ }
+ | STRING
+ ;
+
+stringy : STRING
+ | STRING stringy {
+ if (asprintf(&$$, "%s %s", $1, $2) == -1)
+ err(1, "asprintf");
+ free($1);
+ free($2);
+ }
+ | '|' stringy {
+ if (asprintf(&$$, "|%s", $2) == -1)
+ err(1, "asprintf");
+ free($2);
+ }
+ ;
+
+%%
+
+struct keywords {
+ const char *k_name;
+ int k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+ va_list ap;
+ char *msg;
+
+ file->errors++;
+ va_start(ap, fmt);
+ if (vasprintf(&msg, fmt, ap) == -1)
+ err(1, "yyerror vasprintf");
+ va_end(ap);
+ fprintf(stderr, "%s:%d: %s\n", file->name, yylval.lineno, msg);
+ free(msg);
+ return (0);
+}
+
+int
+kw_cmp(const void *k, const void *e)
+{
+ return (strcmp(k, ((const struct keywords *)e)->k_name));
+}
+
+int
+lookup(char *s)
+{
+ /* this has to be sorted always */
+ static const struct keywords keywords[] = {
+ { "define", DEFINE },
+ { "else", ELSE },
+ { "end", END },
+ { "finally", FINALLY },
+ { "for", FOR },
+ { "if", IF },
+ { "include", INCLUDE },
+ { "printf", PRINTF },
+ { "render", RENDER },
+ { "tailq-foreach", TQFOREACH },
+ { "unsafe", UNSAFE },
+ { "urlescape", URLESCAPE },
+ };
+ const struct keywords *p;
+
+ p = bsearch(s, keywords, nitems(keywords), sizeof(keywords[0]),
+ kw_cmp);
+
+ if (p)
+ return (p->k_val);
+ else
+ return (STRING);
+}
+
+#define START_EXPAND 1
+#define DONE_EXPAND 2
+
+static int expanding;
+
+int
+igetc(void)
+{
+ int c;
+
+ while (1) {
+ if (file->ungetpos > 0)
+ c = file->ungetbuf[--file->ungetpos];
+ else
+ c = getc(file->stream);
+
+ if (c == START_EXPAND)
+ expanding = 1;
+ else if (c == DONE_EXPAND)
+ expanding = 0;
+ else
+ break;
+ }
+ return (c);
+}
+
+int
+lgetc(int quotec)
+{
+ int c;
+
+ if (quotec) {
+ if ((c = igetc()) == EOF) {
+ yyerror("reached end of filewhile parsing "
+ "quoted string");
+ if (file == topfile || popfile() == EOF)
+ return (EOF);
+ return (quotec);
+ }
+ return (c);
+ }
+
+ c = igetc();
+ if (c == '\t' || c == ' ') {
+ /* Compress blanks to a sigle space. */
+ do {
+ c = getc(file->stream);
+ } while (c == '\t' || c == ' ');
+ ungetc(c, file->stream);
+ c = ' ';
+ }
+
+ if (c == EOF) {
+ /*
+ * Fake EOL when hit EOF for the first time. This gets line
+ * count rigchtif last line included file is syntactically
+ * invalid and has no newline.
+ */
+ if (file->eof_reached == 0) {
+ file->eof_reached = 1;
+ return ('\n');
+ }
+ while (c == EOF) {
+ if (file == topfile || popfile() == EOF)
+ return (EOF);
+ c = igetc();
+ }
+ }
+ return (c);
+}
+
+void
+lungetc(int c)
+{
+ if (c == EOF)
+ return;
+
+ if (file->ungetpos >= file->ungetsize) {
+ void *p = reallocarray(file->ungetbuf, file->ungetsize, 2);
+ if (p == NULL)
+ err(1, "reallocarray");
+ file->ungetbuf = p;
+ file->ungetsize *= 2;
+ }
+ file->ungetbuf[file->ungetpos++] = c;
+}
+
+int
+findeol(void)
+{
+ int c;
+
+ /* skip to either EOF or the first real EOL */
+ while (1) {
+ c = lgetc(0);
+ if (c == '\n') {
+ file->lineno++;
+ break;
+ }
+ if (c == EOF)
+ break;
+ }
+ return (ERROR);
+}
+
+int
+yylex(void)
+{
+ char buf[8096];
+ char *p = buf;
+ int c;
+ int token;
+ int starting = 0;
+ int ending = 0;
+ int quote = 0;
+
+ if (!in_define && block == 0) {
+ while ((c = lgetc(0)) != '{' && c != EOF) {
+ if (c == '\n')
+ file->lineno++;
+ }
+
+ if (c == EOF)
+ return (0);
+
+newblock:
+ c = lgetc(0);
+ if (c == '{' || c == '!') {
+ if (c == '{')
+ block = '}';
+ else
+ block = c;
+ return (c);
+ }
+ if (c == '\n')
+ file->lineno++;
+ }
+
+ while ((c = lgetc(0)) == ' ' || c == '\t' || c == '\n') {
+ if (c == '\n')
+ file->lineno++;
+ }
+
+ if (c == EOF) {
+ yyerror("unterminated block");
+ return (0);
+ }
+
+ yylval.lineno = file->lineno;
+
+ if (block != 0 && c == block) {
+ if ((c = lgetc(0)) == '}') {
+ if (block == '!') {
+ block = 0;
+ return ('!');
+ }
+ block = 0;
+ return ('}');
+ }
+ lungetc(c);
+ c = block;
+ }
+
+ if (in_define && block == 0) {
+ if (c == '{')
+ goto newblock;
+
+ do {
+ if (starting) {
+ if (c == '!' || c == '{') {
+ lungetc(c);
+ lungetc('{');
+ break;
+ }
+ starting = 0;
+ lungetc(c);
+ c = '{';
+ } else if (c == '{') {
+ starting = 1;
+ continue;
+ }
+
+ *p++ = c;
+ if ((size_t)(p - buf) >= sizeof(buf)) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ } while ((c = lgetc(0)) != EOF && c != '\n');
+ *p = '\0';
+ if (c == EOF) {
+ yyerror("unterminated block");
+ return (0);
+ }
+ if (c == '\n')
+ file->lineno++;
+ if ((yylval.v.string = strdup(buf)) == NULL)
+ err(1, "strdup");
+ return (STRING);
+ }
+
+ if (block == '!') {
+ do {
+ if (ending) {
+ if (c == '}') {
+ lungetc(c);
+ lungetc(block);
+ break;
+ }
+ ending = 0;
+ lungetc(c);
+ c = block;
+ } else if (c == '!') {
+ ending = 1;
+ continue;
+ }
+
+ *p++ = c;
+ if ((size_t)(p - buf) >= sizeof(buf)) {
+ yyerror("line too long");
+ return (findeol());
+ }
+ } while ((c = lgetc(0)) != EOF && c != '\n');
+ *p = '\0';
+
+ if (c == EOF) {
+ yyerror("unterminated block");
+ return (0);
+ }
+ if (c == '\n')
+ file->lineno++;
+
+ if ((yylval.v.string = strdup(buf)) == NULL)
+ err(1, "strdup");
+ return (STRING);
+ }
+
+ if (c == '|')
+ return (c);
+
+ do {
+ if (!quote && isspace((unsigned char)c))
+ break;
+
+ if (c == '"')
+ quote = !quote;
+
+ if (!quote && c == '|') {
+ lungetc(c);
+ break;
+ }
+
+ if (ending) {
+ if (c == '}') {
+ lungetc(c);
+ lungetc('}');
+ break;
+ }
+ ending = 0;
+ lungetc(c);
+ c = block;
+ } else if (!quote && c == '}') {
+ ending = 1;
+ continue;
+ }
+
+ *p++ = c;
+ if ((size_t)(p - buf) >= sizeof(buf)) {
+ yyerror("string too long");
+ return (findeol());
+ }
+ } while ((c = lgetc(0)) != EOF);
+ *p = '\0';
+
+ if (c == EOF) {
+ yyerror(quote ? "unterminated quote" : "unterminated block");
+ return (0);
+ }
+ if (c == '\n')
+ file->lineno++;
+ if ((token = lookup(buf)) == STRING)
+ if ((yylval.v.string = strdup(buf)) == NULL)
+ err(1, "strdup");
+ return (token);
+}
+
+struct file *
+pushfile(const char *name, int secret)
+{
+ struct file *nfile;
+
+ if ((nfile = calloc(1, sizeof(*nfile))) == NULL)
+ err(1, "calloc");
+ if ((nfile->name = strdup(name)) == NULL)
+ err(1, "strdup");
+ if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
+ warn("can't open %s", nfile->name);
+ free(nfile->name);
+ free(nfile);
+ return (NULL);
+ }
+ nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0;
+ nfile->ungetsize = 16;
+ nfile->ungetbuf = malloc(nfile->ungetsize);
+ if (nfile->ungetbuf == NULL)
+ err(1, "malloc");
+ TAILQ_INSERT_TAIL(&files, nfile, entry);
+ return (nfile);
+}
+
+int
+popfile(void)
+{
+ struct file *prev;
+
+ if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
+ prev->errors += file->errors;
+
+ TAILQ_REMOVE(&files, file, entry);
+ fclose(file->stream);
+ free(file->name);
+ free(file->ungetbuf);
+ free(file);
+ file = prev;
+ return (file ? 0 : EOF);
+}
+
+int
+parse(FILE *outfile, const char *filename)
+{
+ fp = outfile;
+
+ if ((file = pushfile(filename, 0)) == 0)
+ return (-1);
+ topfile = file;
+
+ yyparse();
+ errors = file->errors;
+ popfile();
+
+ return (errors ? -1 : 0);
+}
+
+void
+dbg(void)
+{
+ if (nodebug)
+ return;
+
+ if (yylval.lineno == lastline + 1) {
+ lastline = yylval.lineno;
+ return;
+ }
+ lastline = yylval.lineno;
+
+ fprintf(fp, "#line %d ", yylval.lineno);
+ printq(file->name);
+ putc('\n', fp);
+}
+
+void
+printq(const char *str)
+{
+ putc('"', fp);
+ for (; *str; ++str) {
+ if (*str == '"')
+ putc('\\', fp);
+ putc(*str, fp);
+ }
+ putc('"', fp);
+}
blob - /dev/null
blob + 4c3a752a31718ca8ad29543f9f2f61b00c13cc4e (mode 644)
--- /dev/null
+++ template/template.1
@@ -0,0 +1,85 @@
+.\" Copyright (c) 2022 Omar Polo <op@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd November 25, 2022
+.Dt TEMPLATE 1
+.Os
+.Sh NAME
+.Nm template
+.Nd templating system compiler
+.Sh SYNOPSIS
+.Nm
+.Op Fl G
+.Op Fl o Ar out
+.Op Ar
+.Sh DESCRIPTION
+.Nm
+is an utility that converts files written in the
+.Xr template 7
+format format to a set of routine writtens in the C programming
+language.
+.Nm
+converts the files given as arguments or from standard input, and
+writes to standard output.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl G
+Do not emit debug info in the generated source.
+It's disabled by default, unless
+.Nm
+is reading from standard input.
+.It Fl o Ar out
+Write output to file.
+.Ar out
+will be created or truncated if exists and will be removed if
+.Nm
+encounters any error.
+.El
+.Sh EXIT STATUS
+.Ex
+.Sh SEE ALSO
+.Xr template 7
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+utility was written by
+.An Omar Polo Aq Mt op@openbsd.org .
+.Sh CAVEATS
+The compiler is very naive, so there are quite a few shortcomings:
+.Bl -bullet -compact
+.It
+No attempt is made to validate the C code provided inline, nor the
+validity of the arguments to many constructs.
+.It
+The generated code assumes that a variable called
+.Va tp
+of type
+.Vt struct template *
+is in scope inside each block.
+.It
+Each block may have additional variables used for the template
+generation implicitly defined: to avoid clashes, don't name variables
+or arguments with the
+.Sq tp_
+prefix.
+.It
+Blanks are, in most cases, trimmed.
+Normally this is not a problem, but a workaround is needed in case
+they need to be preserved, for e.g.:
+.Bd -literal -offset indent
+Name: {{ " " }} {{ render name_field(tp) }}
+.Ed
+.El
blob - /dev/null
blob + 31b32697b9affdd4231edc3b70f8abaf0bb8300c (mode 644)
--- /dev/null
+++ template/template.7
@@ -0,0 +1,125 @@
+.\" Copyright (c) 2022 Omar Polo <op@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd November 25, 2022
+.Dt TEMPLATE 7
+.Os
+.Sh NAME
+.Nm template
+.Nd templating language
+.Sh DESCRIPTION
+.Nm
+is a language used to define programs that output data in some way.
+These programs are called
+.Dq templates .
+A
+.Nm
+file is assumed to be compiled using the
+.Xr template 1
+utility into C code, to be further compiled as part of a bigger
+application.
+The language itself is format-agnostic and can thus be used to produce
+various type of outputs.
+.Pp
+There are two special sequences:
+.Bl -tag -width 9m
+.It Cm {{ Ar ... Cm }}
+used for
+.Nm
+special syntax.
+.It Cm {! Ar ... Cm !}
+used to include literal C code.
+This is the only special syntax permitted as top-level, except for block
+definition and includes.
+.El
+.Pp
+The basic unit of a
+.Nm
+file is the block.
+Each block is turned into a C function that output its content via some
+provided functions.
+Here's an example of a block:
+.Bd -literal -offset indent
+{{ define tp_base(struct template *tp, const char *title) }}
+<!doctype html>
+<html>
+ <head>
+ <title>{{ title }}</title>
+ </head>
+ <body>
+ {{ render tp->tp_body(tp) }}
+ </body>
+</html>
+{{ end }}
+.Ed
+.Ss SPECIAL SYNTAX
+This section is a reference for all the special syntaxes supported.
+.Bl -tag -indent Ds
+.It Cm {{ Ic include Ar file Cm }}
+Include additional template files.
+.It Cm {{ Ic define Ar name Ns ( Ar arguments ... ) Cm }} Ar body Cm {{ Ic end Cm }}
+Defines the block
+.Ar name
+with the given
+.Ar arguments .
+.Ar body
+will be outputted as-is via the provided functions
+.Pq i.e.\& is still escaped eventually
+and can contain all the special syntaxes documented here except
+.Ic include
+and
+.Ic define .
+.It Cm {{ Ic render Ar expression() Cm }}
+Executes
+.Ar expression()
+and terminate the template if it returns -1.
+It's used to render (call) another template.
+.It Cm {{ Ic printf Ar fmt , Ar arguments ... Cm }}
+Outputs the string that would be produced by calling
+.Xr printf 3
+with the given
+.Ar fmt
+format string and the given
+.Ar arguments .
+.It Cm {{ Ic if Ar expr Cm }} Ar ... Cm {{ Ic elseif Ar expr Cm }} Ar ... Cm {{ Ic else Cm }} Ar ... Cm {{ Ic end Cm }}
+Conditional evaluation.
+.Ic elseif
+can be provided zero or more times,
+.Ic else
+only zero or one time and always for last.
+.It Cm {{ Ic for Ar ... ; Ar ... ; Ar ... Cm }} Ar ... Cm {{ Ic end Cm }}
+Looping construct similar to the C loop.
+.It Cm {{ Ic tailq-foreach Ar var head fieldname Cm }} Ar .. Cm {{ Ic end Cm }}
+Looping construct similar to the queue.h macro TAILQ_FOREACH.
+.It Cm {{ Ar expression Cm | Ic unsafe Cm }}
+Output
+.Ar expression
+as-is.
+.It Cm {{ Ar expression Cm | Ic urlescape Cm }}
+Output
+.Ar expression
+escaped in a way that can be made part of an URL.
+.It Cm {{ Ar expression Cm }}
+Output
+.Ar expression
+with the default escaping.
+.El
+.Sh SEE ALSO
+.Xr template 1
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+reference was written by
+.Ar Omar Polo Aq Mt op@openbsd.org .
blob - /dev/null
blob + f52199db7b8acc13c6f1a44dc1cf4222b189b3e7 (mode 644)
--- /dev/null
+++ template/template.c
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int parse(FILE *, const char *);
+
+int nodebug;
+
+static void __dead
+usage(void)
+{
+ fprintf(stderr, "usage: %s [file...]\n",
+ getprogname());
+ exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+ FILE *fp = stdout;
+ const char *out = NULL;
+ int ch, i;
+
+ while ((ch = getopt(argc, argv, "Go:")) != -1) {
+ switch (ch) {
+ case 'G':
+ nodebug = 1;
+ break;
+ case 'o':
+ out = optarg;
+ break;
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (out && (fp = fopen(out, "w")) == NULL)
+ err(1, "can't open %s", out);
+
+ if (out && unveil(out, "wc") == -1)
+ err(1, "unveil %s", out);
+ if (unveil("/", "r") == -1)
+ err(1, "unveil /");
+ if (pledge(out ? "stdio rpath cpath" : "stdio rpath", NULL) == -1)
+ err(1, "pledge");
+
+ if (argc == 0) {
+ nodebug = 1;
+ if (parse(fp, "/dev/stdin") == -1)
+ goto err;
+ } else {
+ for (i = 0; i < argc; ++i)
+ if (parse(fp, argv[i]) == -1)
+ goto err;
+ }
+
+ if (ferror(fp))
+ goto err;
+
+ if (fclose(fp) == -1) {
+ fp = NULL;
+ goto err;
+ }
+
+ return (0);
+
+err:
+ if (fp)
+ fclose(fp);
+ if (out && unlink(out) == -1)
+ err(1, "unlink %s", out);
+ return (1);
+}
blob - /dev/null
blob + 36bb4882a5fec3f37fd3b75d26c1514f42bb77f5 (mode 644)
--- /dev/null
+++ template/tmpl.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl.h"
+
+int
+tp_urlescape(struct template *tp, const char *str)
+{
+ int r;
+ char tmp[4];
+
+ if (str == NULL)
+ return (0);
+
+ for (; *str; ++str) {
+ if (iscntrl((unsigned char)*str) ||
+ isspace((unsigned char)*str) ||
+ *str == '\'' || *str == '"' || *str == '\\') {
+ r = snprintf(tmp, sizeof(tmp), "%%%2X", *str);
+ if (r < 0 || (size_t)r >= sizeof(tmp))
+ return (0);
+ if (tp->tp_puts(tp, tmp) == -1)
+ return (-1);
+ } else {
+ if (tp->tp_putc(tp, *str) == -1)
+ return (-1);
+ }
+ }
+
+ return (0);
+}
+
+int
+tp_htmlescape(struct template *tp, const char *str)
+{
+ int r;
+
+ if (str == NULL)
+ return (0);
+
+ for (; *str; ++str) {
+ switch (*str) {
+ case '<':
+ r = tp->tp_puts(tp, "<");
+ break;
+ case '>':
+ r = tp->tp_puts(tp, ">");
+ break;
+ case '&':
+ r = tp->tp_puts(tp, "&");
+ break;
+ case '"':
+ r = tp->tp_puts(tp, """);
+ break;
+ case '\'':
+ r = tp->tp_puts(tp, "'");
+ break;
+ default:
+ r = tp->tp_putc(tp, *str);
+ break;
+ }
+
+ if (r == -1)
+ return (-1);
+ }
+
+ return (0);
+}
+
+struct template *
+template(void *arg, tmpl_puts putsfn, tmpl_putc putcfn)
+{
+ struct template *tp;
+
+ if ((tp = calloc(1, sizeof(*tp))) == NULL)
+ return (NULL);
+
+ tp->tp_arg = arg;
+ tp->tp_escape = tp_htmlescape;
+ tp->tp_puts = putsfn;
+ tp->tp_putc = putcfn;
+
+ return (tp);
+}
+
+void
+template_free(struct template *tp)
+{
+ free(tp->tp_tmp);
+ free(tp);
+}
blob - /dev/null
blob + 2367b76c1353b204d520091d5212fd0a438e15cf (mode 644)
--- /dev/null
+++ template/tmpl.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef TMPL_H
+#define TMPL_H
+
+struct template;
+
+typedef int (*tmpl_puts)(struct template *, const char *);
+typedef int (*tmpl_putc)(struct template *, int);
+
+struct template {
+ void *tp_arg;
+ char *tp_tmp;
+ tmpl_puts tp_escape;
+ tmpl_puts tp_puts;
+ tmpl_putc tp_putc;
+};
+
+int tp_urlescape(struct template *, const char *);
+int tp_htmlescape(struct template *, const char *);
+
+struct template *template(void *, tmpl_puts, tmpl_putc);
+void template_free(struct template *);
+
+#endif
gotwebd: templates take 2