Download raw body.
gotwebd: templates take 2
Hi everyone, Here's a second take on my quest of integrating a templating system into gotwebd. Some time ago I wrote for some personal projects a small templating system that compiles templates to C code and started to wondering how it'd fit gotwebd. Initially I tried to just rewrite all the page generations armed with these templates but it quickly turned out to be a tedious, big and time consuming task. So, here's a second attempt, where I'm just adding the template system and converted only a few bits, things can be improved with time diff after diff instead that all in one go I guess. Diff below is long; I've also set up a branch on my got mirror should it be handier; check out the branch `tmpl' from: https://git.omarpolo.com/?action=summary&headref=tmpl&path=got.git (my gotwebd instance is running with this applied.) There is a small hack in exporing gotweb_render_navs, will go away in a follow-up diff, and there is a bit of duplication that will also go away with the time. So, to recap, the idea is to use a custom grammar to alleviate the pain of dumping html via fcgi_printf (or similar) and escaping what's needed. As an example, instead of writing this familiar style of (pseudo) code: const struct got_error * gotweb_render_foo(args..) { char *thing = some->thing ? some->thing : ""; char *escaped_thing = gotweb_escape_html(thing); if (fcgi_printf(c, "Here's a thing: <b>%s</b>", escaped_thing) == -1) goto err; /* .. */ err: free(escaped_thing); return NULL; } We can go with (almost literally literally) {{ define gw_foo(struct template *tp, args...) }} Here's a thing: <b>{{ some->thing }}</b> {{ end }} that not only achieve the same thing but it also avoids allocating temporary strings (skipping the overhead of malloc/free all over the places) and it escapes values *by default*. The templating system has syntax also sugar for ifs, for and TAILQ_FOREACH, and eventually allows to easily define other kinds of sugar around other constructs. The idea then is to slowly replace the various gotweb_render_* functions with these templates, having stuff easier to read, to write and to change in the future. This kinds of templates thusly compiles to C code and *it is not* executed at runtime, you can see it as a gigantic syntax sugar layer for outputting HTML. Have to admit that there are some drawbacks that we can work out later in tree eventually. Two things comes to mind: - the template strips out most whitespaces and newlines, leading to fundamentally minified HTML. it's both a pro and a cons i think - the error handling and logging needs attention. Since I'm carrying these templates over from a separate project it uses -1/0 to signal error/success, and I'd like to keep the differences minimal so that I can port back and forward the improvements, we can't bubble up `got_error's on failure. I don't think it's a huge loss, since currently we already are using a style that `goto err' when fcgi_printf fails, but needs to be kept in mind. Last thing to note; gotwebd would now need this `template' executable during the build phase, much like it needs yacc to generate parts of its sources. I'm achieving this using PROGS instead of PROG, and since we're already using `realinstall' it should not be installed. (the diff is also long due to the additional manpages for template -- than won't be installed, i wrote it as internal documentation, and the regress for template.) I'm aware that this is a huge chunk of code to digest in one go and it won't be fun to review, I'm apologising in advance. Future diffs will be smaller! diff refs/heads/main refs/heads/tmpl commit - 6970304f7fbe7bb6534af3f344013b472a1a9698 commit + a7430904a16e4f67990a2cabe17d0cd8007f89b5 blob - 8744ffb84ade1da058f9e9837fdbbd91cd5130fc blob + 399c42bd353b419b1fd5afd702655e3f9916bc04 --- gotwebd/Makefile +++ gotwebd/Makefile @@ -1,14 +1,15 @@ .PATH:${.CURDIR}/../lib +.PATH:${.CURDIR}/template SUBDIR = libexec .include "../got-version.mk" .include "Makefile.inc" -PROG = gotwebd -SRCS = config.c sockets.c log.c gotwebd.c parse.y proc.c \ +PROGS = template gotwebd +SRCS_gotwebd = config.c sockets.c tmpl.c log.c gotwebd.c parse.y proc.c \ fcgi.c gotweb.c got_operations.c -SRCS += blame.c commit_graph.c delta.c diff.c \ +SRCS_gotwebd += 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 \ privsep.c reference.c repository.c sha1.c worktree.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 +SRCS_template = template.c comp.y + +TEMPLATES = pages.tmpl + +.for t in ${TEMPLATES} +SRCS_gotwebd += ${t:.tmpl=.c} +${t:.tmpl=.c}: $t template + ./template -o $@ ${.CURDIR}/$t +.endfor + 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} @@ -44,7 +56,7 @@ realinstall: if [ ! -d ${DESTDIR}${PUB_REPOS_DIR}/. ]; then \ ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PUB_REPOS_DIR}; \ fi - ${INSTALL} -c -o root -g daemon -m 0755 ${PROG} ${BINDIR}/${PROG} + ${INSTALL} -c -o root -g daemon -m 0755 gotwebd ${BINDIR}/gotwebd if [ ! -d ${DESTDIR}${HTTPD_DIR}/. ]; then \ ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${HTTPD_DIR}; \ fi @@ -52,6 +64,6 @@ realinstall: ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PROG_DIR}; \ fi ${INSTALL} -c -o ${WWWUSR} -g ${WWWGRP} -m 0644 \ - ${.CURDIR}/files/htdocs/${PROG}/* ${DESTDIR}${PROG_DIR} + ${.CURDIR}/files/htdocs/gotwebd/* ${DESTDIR}${PROG_DIR} .include <bsd.prog.mk> 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 + 95bbf5b0e1806e62a8c5630dd5794b3a9f5046c2 --- 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 (gw_head(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 (gw_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); + gw_foot(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 (gw_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 (gw_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 (gw_briefs(c->tp) == -1) goto done; - } error = gotweb_render_tags(c); if (error) { blob - 14426b3f4ad9787e7876dc233211e8fb8875cab6 blob + 7f297c9274a3c586dc6da0d25ff005e8fcf1996a --- 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 */ @@ -440,6 +447,14 @@ void gotweb_free_transport(struct transport *); void gotweb_process_request(struct request *); void gotweb_free_transport(struct transport *); +/* pages.tmpl */ +int gw_url(struct template *, struct gotweb_url *); +int gw_head(struct template *); +int gw_foot(struct template *); +int gw_repo_table_hdr(struct template *); +int gw_repo_fragment(struct template *, struct repo_dir *); +int gw_briefs(struct template *); + /* parse.y */ int parse_config(const char *, struct gotwebd *); int cmdline_symset(char *); @@ -450,6 +465,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 + 52a4552a401106b22ca6456a760526d8624003c4 (mode 644) --- /dev/null +++ gotwebd/pages.tmpl @@ -0,0 +1,388 @@ +{! +/* + * 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 const char * +action_name(int action) +{ + switch (action) { + case BLAME: + return "blame"; + case BLOB: + return "blob"; + case BRIEFS: + return "briefs"; + case COMMITS: + return "commits"; + case DIFF: + return "diff"; + case ERR: + return "err"; + case INDEX: + return "index"; + case SUMMARY: + return "summary"; + case TAG: + return "tag"; + case TAGS: + return "tags"; + case TREE: + return "tree"; + default: + return NULL; + } +} + +static int +gw_age(struct template *tp, time_t committer_time, int ref_tm) +{ + struct tm tm; + long long diff_time; + const char *years = "years ago", *months = "months ago"; + const char *weeks = "weeks ago", *days = "days ago"; + const char *hours = "hours ago", *minutes = "minutes ago"; + const char *seconds = "seconds ago", *now = "right now"; + char datebuf[32], tmp[32]; + int n = -1; + + switch (ref_tm) { + case TM_DIFF: + diff_time = time(NULL) - committer_time; + if (diff_time > 60 * 60 * 24 * 365 * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60 / 60 / 24 / 365), + years); + } else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60 / 60 / 24 / (365 / 12)), + months); + } else if (diff_time > 60 * 60 * 24 * 7 * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60 / 60 / 24 / 7), weeks); + } else if (diff_time > 60 * 60 * 24 * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60 / 60 / 24), days); + } else if (diff_time > 60 * 60 * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60 / 60), hours); + } else if (diff_time > 60 * 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", (diff_time / 60), minutes); + } else if (diff_time > 2) { + n = snprintf(datebuf, sizeof(datebuf), + "%lld %s", diff_time, seconds); + } else + n = strlcpy(datebuf, now, sizeof(datebuf)); + break; + case TM_LONG: + if (gmtime_r(&committer_time, &tm) == NULL) + return -1; + + if (asctime_r(&tm, tmp) == NULL) + return -1; + + n = snprintf(datebuf, sizeof(datebuf), "%s UTC", tmp); + break; + } + + if (n < 0 || (size_t)n > sizeof(datebuf)) + return -1; + return tp->tp_escape(tp, datebuf); +} + +!} + +{{ define gw_url(struct template *tp, struct gotweb_url *url) }} +?action={{ action_name(url->action) }} +{{ if url->commit }}&commit={{ url->commit }}{{ end }} +{{ if url->previd }}&previd={{ url->previd }}{{ end }} +{{ if url->prevset }}&previd={{ url->prevset }}{{ end }} +{{ if url->file }}&file={{ url->file | urlescape }}{{ end }} +{{ if url->folder }}&folder={{ url->folder | urlescape }}{{ end }} +{{ if url->headref }}&headref={{ url->headref | urlescape }}{{ end }} +{{ if url->index_page != -1 }}&headref={{ printf "%d", url->index_page }}{{ end }} +{{ if url->path }}&path={{ url->path | urlescape }}{{ end }} +{{ if url->page != -1 }}&page={{ printf "%d", url->page }}{{ end }} +{{ end }} + +{{ define gw_head(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 gw_url(tp, &u_path)}}"> + {{ qs->path }} + </a> + {{ end }} + {{ if qs->action != INDEX }} + {{ " / " }}{{ action_name(qs->action) }} + {{ end }} + </div> + </div> + <div id="content"> +{{ end }} + +{{ define gw_foot(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 gw_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 gw_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 gw_url(tp, &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 gw_url(tp, &summary) }}">summary</a> + {{ " | " }} + <a href="{{ render gw_url(tp, &briefs) }}">briefs</a> + {{ " | " }} + <a href="{{ render gw_url(tp, &commits) }}">commits</a> + {{ " | " }} + <a href="{{ render gw_url(tp, &tags) }}">tags</a> + {{ " | " }} + <a href="{{ render gw_url(tp, &tree) }}">tree</a> + </div> + <div class="dotted_line"></div> + </div> +</div> +{{ end }} + +{{ define gw_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 gw_age(tp, rc->committer_time, TM_DIFF) }} + </div> + <div class="briefs_author"> + {{ rc->author }} + </div> + <div class="briefs_log"> + <a href="{{ render gw_url(tp, &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 gw_url(tp, &diff_url) }}">diff</a> + {{ " | " }} + <a href="{{ render gw_url(tp, &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 + 525d753fa8605da41633c1d886b022f049d24ac3 (mode 644) --- /dev/null +++ gotwebd/template/Makefile @@ -0,0 +1,11 @@ +# only for regress; template is also built by gotwebd' makefile + +PROG= template +SRCS= template.c tmpl.c comp.y + +MAN= template.1 template.7 + +# XXX: would break regress/ +NOOBJ= skip + +.include <bsd.prog.mk> blob - /dev/null blob + 34f42f83d0770ec2ab269538aac80935c414dfec (mode 644) --- /dev/null +++ gotwebd/template/comp.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 + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644) blob - /dev/null blob + c2d0fad50754e9ec3740571ad5e77d7dc475ab26 (mode 644) --- /dev/null +++ gotwebd/template/regress/01-noise-only.tmpl @@ -0,0 +1,2 @@ +only +noise blob - /dev/null blob + c35618a4287faab518c419c4bbbe06ecbca7f157 (mode 644) --- /dev/null +++ gotwebd/template/regress/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 +++ gotwebd/template/regress/02.expected @@ -0,0 +1 @@ +hello, world! blob - /dev/null blob + d866f69531b2f1248c5bb6940f9b8c40adf87c82 (mode 644) --- /dev/null +++ gotwebd/template/regress/03-block.tmpl @@ -0,0 +1,22 @@ +{! +#include <stdlib.h> + +#include "tmpl.h" +!} + +{{ 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 +++ gotwebd/template/regress/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 + aa8c5edc9b8934fea4803f8b465a7f00fb389e23 (mode 644) --- /dev/null +++ gotwebd/template/regress/04-flow.tmpl @@ -0,0 +1,31 @@ +{! +#include <stdlib.h> +#include <string.h> + +#include "tmpl.h" +!} + +{{ 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 +++ gotwebd/template/regress/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 + d47673dc5bf15202d96329fe8d971d117bc930eb (mode 644) --- /dev/null +++ gotwebd/template/regress/05-loop.tmpl @@ -0,0 +1,42 @@ +{! +#include <sys/queue.h> +#include <string.h> +#include "lists.h" +#include "tmpl.h" + +int list(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 +++ gotwebd/template/regress/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 + f5450fbc2e7134e6fb5f5798e14983545f6347d4 (mode 644) --- /dev/null +++ gotwebd/template/regress/06-escape.tmpl @@ -0,0 +1,17 @@ +{! +#include <stdlib.h> + +#include "tmpl.h" +!} + +{{ 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 +++ gotwebd/template/regress/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 + 8b48d50f1257a72de04bbd33038320dd05edca49 (mode 644) --- /dev/null +++ gotwebd/template/regress/07-printf.tmpl @@ -0,0 +1,10 @@ +{! +#include <stdio.h> +#include <stdlib.h> + +#include "tmpl.h" +!} + +{{ 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 +++ gotwebd/template/regress/07.expected @@ -0,0 +1,4 @@ + *: 42 + +<h: 42 + blob - /dev/null blob + 5b41a3b36ca0223dab174dafa40e73c572f9a203 (mode 644) --- /dev/null +++ gotwebd/template/regress/Makefile @@ -0,0 +1,60 @@ +REGRESS_TARGETS = 00-empty \ + 01-noise-only \ + 02-only-verbatim \ + 03-block \ + 04-flow \ + 05-loop \ + 06-escape \ + 07-printf + +REGRESS_SETUP_ONCE = setup-comp +REGRESS_CLEANUP = clean-comp +NO_OBJ = Yes + +CFLAGS += -I${.CURDIR}/../ + +setup-comp: + cp ${.CURDIR}/../tmpl.c . + ln -f ${.CURDIR}/../template template || \ + ln -f ${.CURDIR}/../obj/template template + +clean-comp: + rm template + rm -f t got 0*.[cdo] runbase.[do] runlist.[do] tmpl.* + +.SUFFIXES: .tmpl .c .o + +.tmpl.c: + ./template -o $@ $? + +00-empty: + ./template 00-empty.tmpl >/dev/null + +01-noise-only: + ./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 +++ gotwebd/template/regress/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 + 587f2ed68d2caf23a4828887385dae846d090cfe (mode 644) --- /dev/null +++ gotwebd/template/regress/runbase.c @@ -0,0 +1,65 @@ +/* + * 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 *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 + e5a35a7dfefa2010c52a71d596f2aa798602ab39 (mode 644) --- /dev/null +++ gotwebd/template/regress/runlist.c @@ -0,0 +1,85 @@ +/* + * 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 *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 + 4c3a752a31718ca8ad29543f9f2f61b00c13cc4e (mode 644) --- /dev/null +++ gotwebd/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 +++ gotwebd/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 +++ gotwebd/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 +++ gotwebd/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 +++ gotwebd/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