dag

Djmoch's Auto Generator
git clone git://git.danielmoch.com/dag.git
Log | Files | Refs | README | LICENSE

commit c7e6009f9b72065e2c8d7d3a4cc36989ad8630b8
parent 8f7b8d94b22fd0b933e5c61ad867aecadc9147b0
Author: Daniel Moch <daniel@danielmoch.com>
Date:   Mon,  8 Nov 2021 17:06:03 -0500

dagindex working; sitemap still TBD

Diffstat:
MMakefile | 9+++++----
MREADME | 21+++++++++++++++++----
Mconfig.mk | 4++--
Mdag.c | 2++
Adagindex.c | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb.c | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adb.h | 27+++++++++++++++++++++++++++
Ahtml.c | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahtml.h | 7+++++++
Mstring.h | 2+-
Aterm.c | 18++++++++++++++++++
Aterm.h | 7+++++++
Axml.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Axml.h | 9+++++++++
14 files changed, 767 insertions(+), 11 deletions(-)

diff --git a/Makefile b/Makefile @@ -24,19 +24,20 @@ dag: ${DOBJ} ${CC} -o $@ ${DOBJ} ${LDFLAGS} dagindex: ${IOBJ} - ${CC} -o $@ ${IOBJ} ${LDFLAGS} + ${CC} -o $@ ${IOBJ} string_test: string_test.o string.o - ${CC} -o $@ string_test.o string.o ${LDFLAGS} + ${CC} -o $@ string_test.o string.o -install: dag +install: dag dagindex install -Dm755 dag ${DESTDIR}${PREFIX}/bin/dag + install -Dm755 dagindex ${DESTDIR}${PREFIX}/bin/dagindex uninstall: rm -f ${DESTDIR}${PREFIX}/bin/dag clean: - rm -f *.o dag *_test y.tab.* lex.yy.c + rm -f *.o dag dagindex *_test y.tab.* lex.yy.c test: string_test ./string_test diff --git a/README b/README @@ -1,12 +1,25 @@ DAG - Djmoch's Automatic Generator ================================== -usage: %s [-hv] [-Dname=value ...] [-m intype:outtype:xform ...] - in ... out +usage: dag [-Vhv] [-f file] -Dag copies file trees from the input directories to the output directory. Each -m flag specifies a transform, xform, to apply to files of intype to create files of outtype where both intype and outtype are file extensions. Each -D flag specifies a macro that will be passed into each transform (see Transforms section below). +Dag copies file trees from the input directories to the output +directory. Before reaching their destination, files are filtered +according to the rules declared in a file (default: Dagfile) in the +working directory. + +usage: dagindex -Vh + dagindex -A [-a author] [-c category] [-d description] + [-p date_published] [-s slug] [-t title] [-u date_updated] + dagindex -G -o fmt + +Dagindex maintains a database of index entries and refers to the +database in order to create index files. Index files can be either +HTML for XML formatted (for a landing page and RSS feed, respectively). +In generate mode (-G), output files are sent to stdout. Intent ------ -Dag was created to overcome some of the shortcomings of make(1). +Dag, though perhaps reminiscent of make(1), is designed as a more +targeted solution for generating static websites and the like. diff --git a/config.mk b/config.mk @@ -11,7 +11,7 @@ X11BASE := /usr/X11R6 HDRS = dagfile.h string.h y.tab.h DSRC = dag.c dagfile.c string.c y.tab.c lex.yy.c -ISRC = dagindex.c string.c +ISRC = dagindex.c db.c html.c string.c term.c xml.c DIST_SRC = ${SRC} Makefile README config.mk DOBJ = ${DSRC:.c=.o} IOBJ = ${ISRC:.c=.o} @@ -19,5 +19,5 @@ LIBS = -ll -ly VERSION = 0.1.0dev0 CPPFLAGS := -DVERSION=\"${VERSION}\" -CFLAGS := -std=c99 -pedantic-errors -Wall -Wextra -Werror -O1 -g -c -pipe +CFLAGS := -std=c99 -pedantic-errors -Wall -Wextra -Werror -O0 -g -c -pipe LDFLAGS := ${LIBS} diff --git a/dag.c b/dag.c @@ -90,4 +90,6 @@ main(int argc, char **argv) retval = process_dagfile(dagfile); free_dagfile(dagfile); + + return retval; } diff --git a/dagindex.c b/dagindex.c @@ -0,0 +1,238 @@ +/* See LICENSE file for copyright and license details */ +#include <err.h> +#include <errno.h> +#include <libgen.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "db.h" +#include "html.h" +#include "string.h" +#include "term.h" + +enum { + ERR_NONE, + ERR_UNKNOWN_OPTION, + ERR_ARGS +}; + +enum { + NONE, + ADD, + GENERATE +}; + +static const char index_file[] = "index.db"; +static char *argv0; +static int mode = NONE; +int verbose = 0; + +static void +usage(int rv) +{ + FILE *fp; + + if (rv) { + fp = stderr; + } + else { + fp = stdout; + } + + fprintf(fp, "usage: %s -Vh\n", argv0); + fprintf(fp, "\t%s -A -t title -s slug -p date_published [-a author]\n", argv0); + fprintf(fp, "\t\t[-u date_updated] [-c category] [-d description]\n"); + fprintf(fp, "\t%s -G -o fmt\n", argv0); + exit(rv); +} + +static char * +populate_field(const char *src, const char *dft) +{ + char *dst; + + if (src) { + if (verbose >= 2) { + fprintf(stderr, "DEBUG: populate_field src is non-NULL: %s\n", src); + } + dst = strdup(src); + } + else { + if (verbose >= 2) { + fprintf(stderr, "DEBUG: populate_field src is NULL\n"); + } + dst = strdup(dft); + } + + return dst; +} + +int +main(int argc, char **argv) +{ + struct db_entry *entry; + struct db_index *index; + char ch, *author = NULL, *cat = NULL, *desc = NULL, + *fmt = NULL, *pub = NULL, *slug = NULL, + *title = NULL, *updated = NULL; + + argv0 = basename(argv[0]); + + while ((ch = getopt(argc, argv, "AGVa:c:d:ho:p:s:t:u:v")) != -1) { + switch (ch) { + case 'A': + if (mode != NONE) { + warnx("specifying -A here makes no sense"); + usage(ERR_ARGS); + } + mode = ADD; + entry = malloc(sizeof(struct db_entry)); + break; + case 'G': + if (mode != NONE) { + warnx("specifying -G here makes no sense"); + usage(ERR_ARGS); + } + mode = GENERATE; + break; + case 'V': + printf("%s %s\n", argv0, VERSION); + exit(ERR_NONE); + break; + case 'a': + if (mode != ADD) { + warnx("specifying -a without -A makes no sense"); + usage(ERR_ARGS); + } + author = optarg; + break; + case 'c': + if (mode != ADD) { + warnx("specifying -c without -A makes no sense"); + usage(ERR_ARGS); + } + cat = optarg; + break; + case 'd': + if (mode != ADD) { + warnx("specifying -d without -A makes no sense"); + usage(ERR_ARGS); + } + desc = optarg; + break; + case 'h': + usage(ERR_NONE); + break; + case 'o': + if (mode != GENERATE) { + warnx("specifying -o without -G makes no sense"); + usage(ERR_ARGS); + } + fmt = optarg; + break; + case 'p': + if (mode != ADD) { + warnx("specifying -p without -A makes no sense"); + usage(ERR_ARGS); + } + pub = optarg; + break; + case 's': + if (mode != ADD) { + warnx("specifying -s without -A makes no sense"); + usage(ERR_ARGS); + } + slug = optarg; + break; + case 't': + if (mode != ADD) { + warnx("specifying -t without -A makes no sense"); + usage(ERR_ARGS); + } + title = optarg; + break; + case 'u': + if (mode != ADD) { + warnx("specifying -u without -A makes no sense"); + usage(ERR_ARGS); + } + updated = optarg; + break; + case 'v': + verbose += 1; + break; + default: + usage(ERR_UNKNOWN_OPTION); + } + } + argc -= optind; + argv += optind; + + if (argc > 0) { + warnx("received %d arguments, but expected none", argc); + usage(ERR_ARGS); + } + if (mode == ADD && title == NULL) { + warnx("must specify -t with -A"); + usage(ERR_ARGS); + } + if (mode == ADD && slug == NULL) { + warnx("must specify -s with -A"); + usage(ERR_ARGS); + } + if (mode == ADD && pub == NULL) { + warnx("must specify -p with -A"); + usage(ERR_ARGS); + } + + /* TODO: pledge and unveil */ + + if (verbose >= 2) { + fputs("DEBUG: received options:\n", stderr); + fprintf(stderr, "\tauthor = %s\n", author); + fprintf(stderr, "\tcategory = %s\n", cat); + fprintf(stderr, "\tdescription = %s\n", desc); + fprintf(stderr, "\tdate_published = %s\n", pub); + fprintf(stderr, "\tslug = %s\n", slug); + fprintf(stderr, "\ttitle = %s\n", title); + fprintf(stderr, "\tdate_updated = %s\n", updated); + } + + index = db_index_open(index_file); + + switch (mode) { + case ADD: + entry->author = populate_field(author, ""); + entry->category = populate_field(cat, ""); + entry->description = populate_field(desc, ""); + entry->date_published = populate_field(pub, ""); + entry->slug = populate_field(slug, ""); + entry->title = populate_field(title, ""); + entry->date_updated = populate_field(updated, ""); + entry->next = NULL; + + db_entry_add(index, entry); + db_index_write(index); + break; + case GENERATE: + if (strncmp(fmt, "term", 5) == 0) { + term_db_fmt(index); + } + else if (strncmp(fmt, "html", 5) == 0) { + html_db_fmt(index); + } + else { + warnx("unknown output format: %s", fmt); + } + break; + default: + warnx("unexpected mode: %d", mode); + db_index_close(index); + usage(ERR_ARGS); + } + + db_index_close(index); + return 0; +} diff --git a/db.c b/db.c @@ -0,0 +1,298 @@ +/* See LICENSE file for copyright and license details */ +#include <err.h> +#include <errno.h> +#include <libgen.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include "db.h" + +static int DATE_STRING_LENGTH = 30; +extern int verbose; + +static struct db_entry *getentry(FILE *fp); +static void free_entry(struct db_entry *entry); +static char *getstring(FILE *fp); +static int entrycmp(struct db_entry *e1, struct db_entry *e2); + +struct db_index * +db_index_open(const char *db_path) +{ + char *dir, *path = malloc(strlen(db_path + 1)); + struct stat sb; + struct db_index *index = malloc(sizeof(struct db_index)); + struct db_entry *cur_entry = NULL, *next_entry = NULL; + FILE *fp = fopen(db_path, "rb"); + + strcpy(path, db_path); + dir = dirname(path); + index->db_path = db_path; + index->entries = NULL; + + if (stat(dir, &sb)) { + err(errno, "failed to stat parent directory %s", dir); + } + if (stat(db_path, &sb)) { + warnx("creating new db file: %s", db_path); + index->db_path = db_path; + index->entries = NULL; + goto exit; + } + + if (fp == NULL) { + err(errno, "failed to open %s for reading", db_path); + } + + if ((cur_entry = getentry(fp)) == NULL) { + warnx("empty db file: %s", db_path); + goto exit; + } + + index->entries = cur_entry; + + while ((next_entry = getentry(fp)) != NULL) { + cur_entry->next = next_entry; + cur_entry = next_entry; + } + +exit: + if (fp != NULL) { + fclose(fp); + } + free(path); + return index; +} + +void +db_index_write(struct db_index *index) +{ + FILE *fp = fopen(index->db_path, "wb"); + const char nul = '\0'; + struct db_entry *entry = index->entries; + + if (fp == NULL) { + err(errno, "failed to open %s for writing", index->db_path); + } + + while (entry != NULL) { + if (verbose) { + warnx("writing entry:"); + db_entry_fprint(stderr, entry); + } + fputs(entry->author, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->category, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->description, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->date_published, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->slug, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->title, fp); + fwrite(&nul, sizeof(char), 1, fp); + fputs(entry->date_updated, fp); + fwrite(&nul, sizeof(char), 1, fp); + entry = entry->next; + } + + fclose(fp); +} + +void +db_index_close(struct db_index *index) +{ + struct db_entry *entry = index->entries, + *next_entry = NULL; + + while (entry != NULL) { + if (verbose >= 2) { + fprintf(stderr, "DEBUG: freeing index entry\n"); + } + next_entry = entry->next; + free_entry(entry); + entry = next_entry; + } + + free(index); +} + +void +db_entry_add(struct db_index *index, struct db_entry *entry) +{ + struct db_entry *cur_entry = index->entries, + *prev_entry = NULL; + + if (verbose) { + warnx("adding entry:"); + db_entry_fprint(stderr, entry); + } + + if (cur_entry == NULL) { + if (verbose) { + warnx("writing empty index"); + } + index->entries = entry; + return; + } + + while (cur_entry != NULL) { + int dcmp = strncmp(entry->date_published, + cur_entry->date_published, DATE_STRING_LENGTH); + int scmp = strcmp(entry->slug, cur_entry->slug); + if (verbose >= 2) { + fprintf(stderr, "DEBUG: dcmp = %d, scmp = %d\n", dcmp, scmp); + } + if (dcmp > 0) { + if (verbose) { + warnx("inserting new entry %s", entry->slug); + } + entry->next = cur_entry; + if (prev_entry != NULL) { + prev_entry->next = entry; + } + else { + index->entries = entry; + } + break; + } + else if ((dcmp == 0) && (scmp == 0)) { + if (!entrycmp(cur_entry, entry)) { + warnx("new entry %s matches existing one", entry->slug); + break; + } + + if (verbose) { + warnx("updating existing entry %s", entry->slug); + } + if (prev_entry != NULL) { + prev_entry->next = entry; + } + else { + index->entries = entry; + } + entry->next = cur_entry->next; + free_entry(cur_entry); + break; + } + else if (cur_entry->next == NULL) { + if (verbose) { + warnx("inserting new entry %s at end of list", entry->slug); + } + cur_entry->next = entry; + break; + } + + prev_entry = cur_entry; + cur_entry = cur_entry->next; + } +} + +void +db_entry_fprint(FILE *fp, struct db_entry *entry) +{ + fprintf(fp, "\tauthor = %s\n", entry->author); + fprintf(fp, "\tcategory = %s\n", entry->category); + fprintf(fp, "\tdescription = %s\n", entry->description); + fprintf(fp, "\tdate_published = %s\n", entry->date_published); + fprintf(fp, "\tslug = %s\n", entry->slug); + fprintf(fp, "\ttitle = %s\n", entry->title); + fprintf(fp, "\tdate_updated = %s\n", entry->date_updated); +} + +static struct db_entry * +getentry(FILE *fp) +{ + struct db_entry *entry = NULL; + + /* + * EOF is only permissible at the entry boundary + */ + char * str = getstring(fp); + if (str == NULL) { + return NULL; + } + + entry = malloc(sizeof(struct db_entry)); + entry->author = str; + + if ((entry->category = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + if((entry->description = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + if((entry->date_published = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + if((entry->slug = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + if((entry->title = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + if((entry->date_updated = getstring(fp)) == NULL) { + errx(1, "index db file corrupt!"); + } + entry->next = NULL; + + return entry; +} + +static void +free_entry(struct db_entry *entry) +{ + free(entry->author); + free(entry->category); + free(entry->description); + free(entry->date_published); + free(entry->slug); + free(entry->title); + free(entry->date_updated); + free(entry); +} + +static char * +getstring(FILE * fp) +{ + char *str = NULL; + size_t bufsize = 0; + + if (getdelim(&str, &bufsize, '\0', fp) == -1) { + return NULL; + } + + return str; +} + +static int +entrycmp(struct db_entry *e1, struct db_entry *e2) +{ + int cmp; + + if ((cmp = strcmp(e1->author, e2->author)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->category, e2->category)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->description, e2->description)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->date_published, e2->date_published)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->slug, e2->slug)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->title, e2->title)) != 0) { + return cmp; + } + if ((cmp = strcmp(e1->date_updated, e2->date_updated)) != 0) { + return cmp; + } + + return 0; +} diff --git a/db.h b/db.h @@ -0,0 +1,27 @@ +/* See LICENSE file for copyright and license details */ +#ifndef __DB_H +#define __DB_H + +struct db_entry { + char *author; + char *category; + char *description; + char *date_published; + char *slug; + char *title; + char *date_updated; + struct db_entry *next; +}; + +struct db_index { + const char *db_path; + struct db_entry *entries; +}; + +struct db_index *db_index_open(const char *db_file); +void db_index_write(struct db_index *index); +void db_index_close(struct db_index *index); +void db_entry_add(struct db_index *index, struct db_entry *entry); +void db_entry_fprint(FILE *fp, struct db_entry *entry); + +#endif diff --git a/html.c b/html.c @@ -0,0 +1,65 @@ +/* See LICENSE file for copyright and license details */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "db.h" +#include "string.h" + +static void htmlescape(char *s); + +void +html_db_fmt(struct db_index *index) +{ + puts("<table>"); + struct db_entry *entry = index->entries; + + while (entry != NULL) { + char *date = strdup(entry->date_published); + char *title = strdup(entry->title); + + date[10] = '\0'; /* only interested in YYYY-MM-DD */ + + htmlescape(date); + htmlescape(title); + + printf("<tr><td><time>%s</time></td>", date); + printf("<td><a href=\"/%s\">%s</a></td></tr>\n", entry->slug, title); + entry = entry->next; + + free(date); + free(title); + } + puts("</table>"); +} + +static void +htmlescape(char *s) +{ + size_t len = strlen(s), ct = 0, maxlen = 0; + + for (size_t i=0; i<len; i++) { + switch (s[i]) { + case '"': + case '\'': + case '&': + case '<': + case '>': + ct += 1; + break; + } + } + + if (ct == 0) { + return; + } + + maxlen = len + (ct * strlen("&quot;")); + s = realloc(s, maxlen); + + strnswp(s, "'", "&#39;", maxlen); + strnswp(s, "\"", "&quot;", maxlen); + strnswp(s, "&", "&amp;", maxlen); + strnswp(s, "<", "&lt;", maxlen); + strnswp(s, ">", "&gt;", maxlen); +} diff --git a/html.h b/html.h @@ -0,0 +1,7 @@ +/* See LICENSE file for copyright and license details */ +#ifndef __HTML_H +#define __HTML_H + +void html_db_fmt(struct db_index *index); + +#endif diff --git a/string.h b/string.h @@ -9,7 +9,7 @@ int strbegin(const char *big, const char *little); int strend(const char *big, const char *little); -char *strnswp(char *big, const char *old, const char *new, size_t max); +void strnswp(char *big, const char *old, const char *new, size_t max); char *unquote(char *string); char *strnesc(char *str, int maxlen); int strcnt(char *str, char c); diff --git a/term.c b/term.c @@ -0,0 +1,18 @@ +/* See LICENSE file for copyright and license details */ +#include <stdio.h> + +#include "db.h" + +void +term_db_fmt(struct db_index *index) +{ + int i = 1; + struct db_entry *entry = index->entries; + + while (entry != NULL) { + printf("Entry %d\n", i); + db_entry_fprint(stdout, entry); + entry = entry->next; + i += 1; + } +} diff --git a/term.h b/term.h @@ -0,0 +1,7 @@ +/* See LICENSE file for copyright and license details */ +#ifndef __TERM_H +#define __TERM_H + +void term_db_fmt(struct db_index *index); + +#endif diff --git a/xml.c b/xml.c @@ -0,0 +1,71 @@ +/* See LICENSE file for copyright and license details */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "db.h" +#include "string.h" + +static void xmlescape(char *s); + +void +html_db_fmt(struct db_index *index) +{ + puts("<table>"); + struct db_entry *entry = index->entries; + + while (entry != NULL) { + char *date = strdup(entry->date_published); + char *title = strdup(entry->title); + + date[10] = '\0'; /* only interested in YYYY-MM-DD */ + + htmlescape(date); + htmlescape(title); + + printf("<tr><td><time>%s</time></td>", date); + printf("<td><a href=\"/%s\">%s</a></td></tr>\n", entry->slug, title); + entry = entry->next; + + free(date); + free(title); + } + puts("</table>"); +} + +static void +xmlescape(char *s) +{ + size_t len = strlen(s), ct = 0, maxlen = 0; + + for (size_t i=0; i<len; i++) { + switch (s[i]) { + case '"': + case '\'': + case '&': + case '<': + case '>': + case '\t': + case '\n': + case '\r': + ct += 1; + break; + } + } + + if (ct == 0) { + return; + } + + maxlen = len + (ct * strlen("&quot;")); + s = realloc(s, maxlen); + + strnswp(s, "'", "&#39;", maxlen); + strnswp(s, "\"", "&quot;", maxlen); + strnswp(s, "&", "&amp;", maxlen); + strnswp(s, "<", "&lt;", maxlen); + strnswp(s, ">", "&gt;", maxlen); + strnswp(s, "\t", "&#x9;", maxlen); + strnswp(s, "\n", "&#xA;", maxlen); + strnswp(s, "\r", "&#xD;", maxlen); +} diff --git a/xml.h b/xml.h @@ -0,0 +1,9 @@ +/* See LICENSE file for copyright and license details */ +#ifndef __XML_H +#define __XML_H + +void xml_db_fmt_sitemapindex(struct db_index *index); +void xml_db_fmt_sitemap(struct db_index *index); +void xml_db_fmt_rss(struct db_index *index); + +#endif