"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: gotwebd: templates take 2
To:
Omar Polo <op@omarpolo.com>
Cc:
gameoftrees@openbsd.org
Date:
Tue, 13 Dec 2022 09:29:28 -0700

Download raw body.

Thread
On Tue, Dec 13, 2022 at 11:42:34AM +0100, Omar Polo wrote:
> 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.
> 

I like this diredtion. There are some small grammatical tweaks needed in
template.7, but that can be fixed in tree. I'm ok with this unless
anyone else objects to this direction.

> 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 = "&nbsp;";
> -	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>&lt;hello&gt;</title></head><body> <h1>&lt;hello&gt;</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>&lt;hello&gt;</title></head><body><h1>&lt;hello&gt;</h1><p>"&lt;hello&gt;" 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
> +
> +&lt;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, "&lt;");
> +			break;
> +		case '>':
> +			r = tp->tp_puts(tp, "&gt;");
> +			break;
> +		case '&':
> +			r = tp->tp_puts(tp, "&amp;");
> +			break;
> +		case '"':
> +			r = tp->tp_puts(tp, "&quot;");
> +			break;
> +		case '\'':
> +			r = tp->tp_puts(tp, "&apos;");
> +			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

-- 

Tracey Emery