Welcome to little lamb

Code » qmdoc » commit b0c204c

Transition to latest limb

author Olivier Brunel
2023-05-27 11:26:19 UTC
committer Olivier Brunel
2023-07-05 07:56:22 UTC
parent 173e3b06293409f2b714019b4a6b1687b3bf6e6b

Transition to latest limb

src/include/qmdoc.h +0 -11
src/qmdoc/qmdoc.c +499 -427

diff --git a/src/include/qmdoc.h b/src/include/qmdoc.h
index b4af1af..4629e78 100644
--- a/src/include/qmdoc.h
+++ b/src/include/qmdoc.h
@@ -1,17 +1,6 @@
 #ifndef QMDOC_H
 #define QMDOC_H
 
-enum {
-    ERR_NONE        =  0,
-    ERR_USAGE       = -1,
-    ERR_IO          = -5,
-    ERR_PARSER      = -6,
-    ERR_MEM         = -7,
-    ERR_NOTSUPP     = -8,
-    ERR_INVALID     = -9,
-    ERR_MISC        = -88,
-};
-
 const char metas[] =
 "<meta charset=\"utf-8\">"
 "<meta name=\"generator\" content=\"qmdoc\">"
diff --git a/src/qmdoc/qmdoc.c b/src/qmdoc/qmdoc.c
index 38863f1..8a88f7f 100644
--- a/src/qmdoc/qmdoc.c
+++ b/src/qmdoc/qmdoc.c
@@ -1,18 +1,14 @@
-#define _GNU_SOURCE /* memmem() */
-#include <stdio.h>
-#include <stdarg.h>
 #include <stdlib.h>
-#include <string.h>
-#include <fcntl.h>
 #include <unistd.h>
-#include <sys/stat.h>
 #include <time.h>
-#include <getopt.h>
-#include <errno.h>
 #include <limb/buffer.h>
+#include <limb/bytestr.h>
 #include <limb/djbunix.h>
 #include <limb/exitcode.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
+#include <limb/posixplz.h>
+#include <limb/samisc.h>
 #include <limb/u32.h>
 #include <limb/unix-transactional.h>
 #include "md4c.h"
@@ -44,24 +40,16 @@ enum {
     CSS_NO_TOC,
     NB_CSS
 };
-static struct css {
-    const char *file;
-    size_t offset;
-} css[NB_CSS] = {
-    { "qmdoc.css" },
-    { NULL },
-    { "no-toc.css" }
-};
 
 struct page {
-    const char *sce;
+    size_t sceoff;
     size_t fileoff;
     size_t titleoff;
     size_t nameoff;
     size_t veroff;
     size_t dateoff;
-    int fd;
     size_t size;
+    int fd;
 };
 
 enum {
@@ -72,26 +60,22 @@ enum {
     DOC_BUFFERED_A  = (1 << 4),
 };
 
-struct ctx {
-    int options;
+struct qmdoc {
     stralloc sa;
-    size_t otoc; /* where to include to page's TOC */
     stralloc sa_out;
-    int toc_lvl;
-    struct css *css;
-    struct page *pages;
-    int nb_pages;
-    int cur_page;
+    size_t opages;
+    size_t otoc; /* where to include to page's TOC */
+    size_t css[NB_CSS];
     struct {
-        const char *title;
-        const char *subtitle;
-        const char *author;
         const char *lang;
+        size_t otitle;
+        size_t osubtitle;
+        size_t oauthor;
         size_t oheader;
         size_t ofooter;
         int flags;
     } doc;
-    const char *manurl;
+    size_t omanurl;
     struct {
         stralloc sa;
         size_t salen;
@@ -104,8 +88,14 @@ struct ctx {
         int flags;
         int from;
     } code;
+    int toc_lvl;
+    int nb_pages;
+    int cur_page;
+    int options;
 };
 
+#define PAGE(ctx, n)        ((struct page *) ((ctx)->sa.s + (ctx)->opages))[n]
+
 #define BUFFERING_ON()  \
     ctx->doc.flags |= DOC_BUFFERING; \
     ctx->buf.salen = ctx->buf.sa.len
@@ -117,17 +107,19 @@ struct ctx {
     ctx->doc.flags &= ~DOC_BUFFERING
 
 enum {
-    ERR_PARSER_ENTER_BLOCK  = -100,
-    ERR_PARSER_LEAVE_BLOCK  = -101,
-    ERR_PARSER_ENTER_SPAN   = -102,
-    ERR_PARSER_LEAVE_SPAN   = -103,
-    ERR_PARSER_TEXT         = -104,
-    ERR_PARSER_BUFFERED     = -105,
-    ERR_PARSER_TOC          = -106,
+    ERR_INVALID         = -100,
+    ERR_NOTSUPP         = -101,
+    ERR_ENTER_BLOCK     = -100,
+    ERR_LEAVE_BLOCK     = -101,
+    ERR_ENTER_SPAN      = -102,
+    ERR_LEAVE_SPAN      = -103,
+    ERR_TEXT            = -104,
+    ERR_BUFFERED        = -105,
+    ERR_TOC             = -106,
 };
 
 static int
-raw_text(struct ctx *ctx, const char *text, size_t size)
+raw_text(struct qmdoc *ctx, const char *text, size_t size)
 {
     stralloc *sa = (ctx->doc.flags & DOC_BUFFERING) ? &ctx->buf.sa : &ctx->sa_out;
     return stralloc_catb(sa, text, size);
@@ -136,7 +128,7 @@ raw_text(struct ctx *ctx, const char *text, size_t size)
 #define raw_str(ctx,s)      raw_text(ctx, s, strlen(s))
 
 static int
-escape_text(struct ctx *ctx, const char *text, size_t size)
+escape_text(struct qmdoc *ctx, const char *text, size_t size)
 {
     size_t last = 0;
     for (size_t n = 0; n < size; ++n) {
@@ -166,7 +158,7 @@ escape_text(struct ctx *ctx, const char *text, size_t size)
 }
 
 static int
-anchor(struct ctx *ctx, const char *text, size_t size)
+anchor(struct qmdoc *ctx, const char *text, size_t size)
 {
     char s[size];
     size_t skipped = 0;
@@ -195,9 +187,9 @@ anchor(struct ctx *ctx, const char *text, size_t size)
 }
 
 static int
-strip_tags(struct ctx *ctx, const char *text, size_t size, int no_escaping)
+strip_tags(struct qmdoc *ctx, const char *text, size_t size, int no_escaping)
 {
-    int (*do_text) (struct ctx *, const char *, size_t) = (no_escaping) ? raw_text : escape_text;
+    int (*do_text) (struct qmdoc *, const char *, size_t) = (no_escaping) ? raw_text : escape_text;
     const char *s;
     for(;;) {
         s = memchr(text, '<', size);
@@ -216,7 +208,7 @@ strip_tags(struct ctx *ctx, const char *text, size_t size, int no_escaping)
 }
 
 static int
-highlight_escape_text(struct ctx *ctx, const char *text, size_t size)
+highlight_escape_text(struct qmdoc *ctx, const char *text, size_t size)
 {
     struct {
         const char *open;
@@ -257,7 +249,7 @@ highlight_escape_text(struct ctx *ctx, const char *text, size_t size)
 }
 
 static int
-attribute(struct ctx *ctx, MD_ATTRIBUTE *attr)
+attribute(struct qmdoc *ctx, MD_ATTRIBUTE *attr)
 {
     int n = 0;
     MD_SIZE l, cur = 0, size = attr->size;
@@ -336,76 +328,78 @@ get_section(MD_ATTRIBUTE *attr)
 static int
 enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 {
-    struct ctx *ctx = ctx_;
+    struct qmdoc *ctx = ctx_;
 
     switch (type) {
         case MD_BLOCK_DOC:
             {
-                struct page *p = &ctx->pages[ctx->cur_page];
-
-#define offset(i)       ((ctx->pages[i].nameoff) ? ctx->pages[i].nameoff : ctx->pages[i].titleoff)
+#define offset(i)       ((PAGE(ctx, i).nameoff) ? PAGE(ctx, i).nameoff : PAGE(ctx, i).titleoff)
 #define str_title(i)    ctx->sa.s + offset(i)
-#define str_file(i)    ctx->sa.s + ctx->pages[i].fileoff
-                size_t authlen = strlen(ctx->doc.author);
+#define str_file(i)     ctx->sa.s + PAGE(ctx, i).fileoff
                 if (!raw_str(ctx, "<!DOCTYPE html>\n<html lang=\"")
                         || !escape_text(ctx, ctx->doc.lang, strlen(ctx->doc.lang))
                         || !raw_str(ctx, "\"><head>")
                         || !raw_str(ctx, metas)
-                        || (authlen && (!raw_str(ctx, "<meta name=\"author\" content=\"")
-                                        || !escape_text(ctx, ctx->doc.author, authlen)
-                                        || !raw_str(ctx, "\">")))
+                        || (ctx->doc.oauthor != (size_t) -1
+                            && (!raw_str(ctx, "<meta name=\"author\" content=\"")
+                                || !escape_text(ctx, ctx->sa.s + ctx->doc.oauthor,
+                                                strlen(ctx->sa.s + ctx->doc.oauthor))
+                                || !raw_str(ctx, "\">")))
                         || !raw_str(ctx, "<title>"))
-                    return ERR_PARSER_ENTER_BLOCK;
-                if (p->nameoff) {
-                    if (!escape_text(ctx, ctx->sa.s + p->nameoff,
-                                strlen(ctx->sa.s + p->nameoff))
+                    return ERR_ENTER_BLOCK;
+                if (PAGE(ctx, ctx->cur_page).nameoff) {
+                    if (!escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff,
+                                strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff))
                             || !raw_str(ctx, " - "))
-                        return ERR_PARSER_ENTER_BLOCK;
+                        return ERR_ENTER_BLOCK;
                 }
-                if (!escape_text(ctx, ctx->sa.s + p->titleoff,
-                            strlen(ctx->sa.s + p->titleoff))
+                if (!escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).titleoff,
+                            strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).titleoff))
                         || !raw_str(ctx, "</title>"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
                 if (ctx->options & OPT_INLINE_CSS) {
                     for (int i = 0; i < NB_CSS; ++i) {
-                        if (ctx->css[i].file &&
+                        if (ctx->css[i] != (size_t) -1 &&
                                 (
                                     !raw_str(ctx, "<style>")
-                                    || !raw_str(ctx, ctx->sa.s + ctx->css[i].offset)
+                                    || !raw_str(ctx, ctx->sa.s + ctx->css[i])
                                     || !raw_str(ctx, "</style>")
                                 ))
-                            return ERR_PARSER_ENTER_BLOCK;
+                            return ERR_ENTER_BLOCK;
                     }
                 } else {
                     for (int i = 0; i < NB_CSS; ++i) {
                         if ((i == CSS_CUSTOM || !(ctx->options & OPT_NO_CSS))
-                                && ctx->css[i].file &&
+                                && ctx->css[i] != (size_t) -1 &&
                                 (
                                     !raw_str(ctx, "<link rel=\"stylesheet\" href=\"")
-                                    || !escape_text(ctx, ctx->css[i].file, strlen(ctx->css[i].file))
+                                    || !escape_text(ctx, ctx->sa.s + ctx->css[i],
+                                                    strlen(ctx->sa.s + ctx->css[i]))
                                     || !raw_str(ctx, "\">")
                                 ))
-                        return ERR_PARSER_ENTER_BLOCK;
+                        return ERR_ENTER_BLOCK;
                     }
                 }
                 if (!raw_str(ctx, "</head><body>")
                         || (ctx->doc.oheader && (ctx->options & OPT_WIDE_INC)
                             && !raw_str(ctx, ctx->sa.s + ctx->doc.oheader))
                         || !raw_str(ctx, "<main>"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
 
                 if (!(ctx->options & OPT_NO_TOC)) {
                     if (!raw_str(ctx, "<header class=\"toc\"><section><h1>")
-                        || !escape_text(ctx, ctx->doc.title, strlen(ctx->doc.title))
+                        || !escape_text(ctx, ctx->sa.s + ctx->doc.otitle,
+                                        strlen(ctx->sa.s + ctx->doc.otitle))
                         || !raw_str(ctx, "</h1>")
-                        || (ctx->doc.subtitle && (!raw_str(ctx, "<h2>")
-                                                  || !escape_text(ctx,
-                                                                  ctx->doc.subtitle,
-                                                                  strlen(ctx->doc.subtitle))
-                                                  || !raw_str(ctx, "</h2>"))
+                        || (ctx->doc.osubtitle != (size_t) -1
+                            && (!raw_str(ctx, "<h2>")
+                                || !escape_text(ctx,
+                                                ctx->sa.s + ctx->doc.osubtitle,
+                                                strlen(ctx->sa.s + ctx->doc.osubtitle))
+                                || !raw_str(ctx, "</h2>"))
                             )
                         || !raw_str(ctx, "</section><nav><ul class=\"toc page\">"))
-                        return ERR_PARSER_ENTER_BLOCK;
+                        return ERR_ENTER_BLOCK;
 
                     for (int i = 0; i < ctx->nb_pages; ++i) {
                         if (!raw_str(ctx, "<li><a href=\"")
@@ -415,7 +409,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                                 || !raw_str(ctx, "\">")
                                 || !escape_text(ctx, str_title(i), strlen(str_title(i)))
                                 || !raw_str(ctx, "</a>"))
-                            return ERR_PARSER_ENTER_BLOCK;
+                            return ERR_ENTER_BLOCK;
 
                         /* remember positions for TOC */
                         if (i == ctx->cur_page) {
@@ -425,7 +419,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                                 /* starting it? */
                                 if (i == 1) {
                                     if (!raw_str(ctx, "<ul class=\"toc page\">"))
-                                        return ERR_PARSER_TOC;
+                                        return ERR_TOC;
                                 }
                                 /* adding page title */
                                 if (i > 0 &&
@@ -436,7 +430,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                                         || !raw_str(ctx, "\">")
                                         || !escape_text(ctx, str_title(i), strlen(str_title(i)))
                                         || !raw_str(ctx, "</a>")))
-                                    return ERR_PARSER_TOC;
+                                    return ERR_TOC;
                             }
 
                             /* where it begins */
@@ -446,33 +440,33 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
                             /* open it */
                             if (!raw_str(ctx, "<ul class=\"toc h1\">"))
-                                return ERR_PARSER_TOC;
+                                return ERR_TOC;
 
                             ctx->toc_lvl = 1;
                             ctx->doc.flags &= ~DOC_BUFFERING;
                         }
                     }
                     if (!raw_str(ctx, "</ul></nav></header>"))
-                        return ERR_PARSER_ENTER_BLOCK;
+                        return ERR_ENTER_BLOCK;
                 }
 
                 if (!raw_str(ctx, "<section class=\"content\">")
                         || (ctx->doc.oheader && !(ctx->options & OPT_WIDE_INC)
                             && !raw_str(ctx, ctx->sa.s + ctx->doc.oheader)))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
 
-                if (p->nameoff) {
+                if (PAGE(ctx, ctx->cur_page).nameoff) {
                     if (!raw_str(ctx, "<header class=\"manpage\"><div class=\"left\">")
-                            || !escape_text(ctx, ctx->sa.s + p->nameoff,
-                                            strlen(ctx->sa.s + p->nameoff))
+                            || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff,
+                                            strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff))
                             || !raw_str(ctx, "</div><div class=\"middle\">")
-                            || !escape_text(ctx, ctx->sa.s + p->titleoff,
-                                            strlen(ctx->sa.s + p->titleoff))
+                            || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).titleoff,
+                                            strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).titleoff))
                             || !raw_str(ctx, "</div><div class=\"right\">")
-                            || !escape_text(ctx, ctx->sa.s + p->nameoff,
-                                            strlen(ctx->sa.s + p->nameoff))
+                            || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff,
+                                            strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff))
                             || !raw_str(ctx, "</div></header>"))
-                        return ERR_PARSER_ENTER_BLOCK;
+                        return ERR_ENTER_BLOCK;
                 }
 #undef str_title
 #undef str_file
@@ -481,12 +475,12 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
         case MD_BLOCK_QUOTE:
             if (!raw_str(ctx, "<blockquote>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_INDENT:
             if (!raw_str(ctx, "<div class=\"indent\">"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_BOX:
@@ -497,22 +491,22 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
             if (!raw_str(ctx, "<div class=\"box ")
                     || !raw_str(ctx, box_types[idx].class)
                     || !raw_str(ctx, "\"><span>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
 
             if (d->title.substr_offsets[0] + d->title.substr_offsets[1] == 0) {
                 if (!raw_str(ctx, box_types[idx].title))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
             } else {
                 if (!attribute(ctx, &d->title))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
             }
             if (!raw_str(ctx, "</span>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_UL:
             if (!raw_str(ctx, "<ul>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_OL:
@@ -526,7 +520,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                                               || !raw_str(ctx, buf))
                            )
                         || !raw_str(ctx, ">"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
             }
             break;
 
@@ -540,13 +534,13 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                              || !raw_str(ctx, "\"")
                             ))
                         || !raw_str(ctx, ">"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
             }
             break;
 
         case MD_BLOCK_HR:
             if (!raw_str(ctx, "<hr>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_H:
@@ -555,7 +549,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                 ctx->title.level = d->level;
 
                 if ((ctx->doc.flags & DOC_HAS_TITLE) && !raw_str(ctx, "</section>"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_ENTER_BLOCK;
                 ctx->doc.flags |= DOC_HAS_TITLE | DOC_BUFFERING;
 
                 if (!(ctx->options & OPT_NO_TOC)) {
@@ -565,11 +559,11 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                         if (!raw_str(ctx, "<ul class=\"toc h")
                                 || !raw_text(ctx, &n, 1)
                                 || !raw_str(ctx, "\">"))
-                            return ERR_PARSER_TOC;
+                            return ERR_TOC;
                     }
                     for ( ; ctx->toc_lvl > d->level; --ctx->toc_lvl) {
                         if (!raw_str(ctx, "</ul>"))
-                            return ERR_PARSER_TOC;
+                            return ERR_TOC;
                     }
                     ctx->buf.salen = ctx->buf.sa.len;
                 }
@@ -583,7 +577,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                 ctx->code.flags = 0;
 
                 if (!d->info.text || !strncmp(d->info.text, "pre\n", 4)) {
-                    return (raw_str(ctx, "<pre>")) ? 0 : ERR_PARSER_ENTER_BLOCK;
+                    return (raw_str(ctx, "<pre>")) ? 0 : ERR_ENTER_BLOCK;
                 } else {
                     const char *t = d->info.text;
                     size_t l = d->info.size;
@@ -629,7 +623,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                     if (ctx->code.flags & CODE_BUFFERED) {
                         BUFFERING_ON();
                     } else {
-                        return (raw_str(ctx, "<pre>")) ? 0 : ERR_PARSER_ENTER_BLOCK;
+                        return (raw_str(ctx, "<pre>")) ? 0 : ERR_ENTER_BLOCK;
                     }
                 }
             }
@@ -640,7 +634,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
         case MD_BLOCK_P:
             if (!raw_str(ctx, "<p>"))
-                return ERR_PARSER_ENTER_BLOCK;
+                return ERR_ENTER_BLOCK;
             break;
 
         case MD_BLOCK_TABLE:
@@ -658,13 +652,11 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 static int
 leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 {
-    struct ctx *ctx = ctx_;
+    struct qmdoc *ctx = ctx_;
 
     switch (type) {
         case MD_BLOCK_DOC:
             ;
-            struct page *p = &ctx->pages[ctx->cur_page];
-
             char year[U32_FMT];
             struct timespec ts;
             clock_gettime(CLOCK_REALTIME, &ts);
@@ -672,25 +664,25 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
             localtime_r(&ts.tv_sec, &tm);
             year[u32_fmt(year, (u32) 1900 + tm.tm_year)] = '\0';
             if ((ctx->doc.flags & DOC_HAS_TITLE) && !raw_str(ctx, "</section>"))
-                return ERR_PARSER_LEAVE_BLOCK;
-            if (p->nameoff) {
+                return ERR_LEAVE_BLOCK;
+            if (PAGE(ctx, ctx->cur_page).nameoff) {
                 if (!raw_str(ctx, "<footer class=\"manpage\"><div class=\"left\">")
-                        || !escape_text(ctx, ctx->sa.s + p->veroff,
-                                        strlen(ctx->sa.s + p->veroff))
+                        || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).veroff,
+                                        strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).veroff))
                         || !raw_str(ctx, "</div><div class=\"middle\">")
-                        || !escape_text(ctx, ctx->sa.s + p->dateoff,
-                                        strlen(ctx->sa.s + p->dateoff))
+                        || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).dateoff,
+                                        strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).dateoff))
                         || !raw_str(ctx, "</div><div class=\"right\">")
-                        || !escape_text(ctx, ctx->sa.s + p->nameoff,
-                                        strlen(ctx->sa.s + p->nameoff))
+                        || !escape_text(ctx, ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff,
+                                        strlen(ctx->sa.s + PAGE(ctx, ctx->cur_page).nameoff))
                         || !raw_str(ctx, "</div></footer>"))
-                    return ERR_PARSER_ENTER_BLOCK;
+                    return ERR_LEAVE_BLOCK;
             }
             if (ctx->options & OPT_BUTTONS) {
-#define str_title(i)    ctx->sa.s + ctx->pages[i].titleoff
-#define str_file(i)    ctx->sa.s + ctx->pages[i].fileoff
+#define str_title(i)    ctx->sa.s + PAGE(ctx, i).titleoff
+#define str_file(i)     ctx->sa.s + PAGE(ctx, i).fileoff
                 if (!raw_str(ctx, "<section id=\"navbuttons\">"))
-                    return ERR_PARSER_LEAVE_BLOCK;
+                    return ERR_LEAVE_BLOCK;
                 if (ctx->cur_page > 0
                         && (!raw_str(ctx, "<a class=\"prev\" href=\"")
                             || !escape_text(ctx, str_file(ctx->cur_page - 1),
@@ -699,7 +691,7 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                             || !escape_text(ctx, str_title(ctx->cur_page - 1),
                                             strlen(str_title(ctx->cur_page - 1)))
                             || !raw_str(ctx, "\">Previous</a>")))
-                    return ERR_PARSER_LEAVE_BLOCK;
+                    return ERR_LEAVE_BLOCK;
                 if (ctx->cur_page < ctx->nb_pages - 1
                         && (!raw_str(ctx, "<a class=\"next\" href=\"")
                             || !escape_text(ctx, str_file(ctx->cur_page + 1),
@@ -708,16 +700,18 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                             || !escape_text(ctx, str_title(ctx->cur_page + 1),
                                             strlen(str_title(ctx->cur_page + 1)))
                             || !raw_str(ctx, "\">Next</a>")))
-                    return ERR_PARSER_LEAVE_BLOCK;
+                    return ERR_LEAVE_BLOCK;
                 if (!raw_str(ctx, "</section>"))
-                    return ERR_PARSER_LEAVE_BLOCK;
+                    return ERR_LEAVE_BLOCK;
 #undef str_file
 #undef str_title
             }
             if (!raw_str(ctx, "<footer class=\"page\">Copyright &copy; ")
                     || !raw_str(ctx, year)
-                    || !raw_str(ctx, " ")
-                    || !escape_text(ctx, ctx->doc.author, strlen(ctx->doc.author))
+                    || (ctx->doc.oauthor != (size_t) -1
+                        && !raw_str(ctx, " ")
+                        && !escape_text(ctx, ctx->sa.s + ctx->doc.oauthor,
+                                        strlen(ctx->sa.s + ctx->doc.oauthor)))
                     || !raw_str(ctx, "<br>"
                                 "<span class=\"generated\">Generated with qmdoc</span>"
                                 "</footer>")
@@ -727,37 +721,37 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                     || (ctx->doc.ofooter && (ctx->options & OPT_WIDE_INC)
                         && !raw_str(ctx, ctx->sa.s + ctx->doc.ofooter))
                     || !raw_str(ctx, "</body></html>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_QUOTE:
             if (!raw_str(ctx, "</blockquote>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_INDENT:
             if (!raw_str(ctx, "</div>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_BOX:
             if (!raw_str(ctx, "</div>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_UL:
             if (!raw_str(ctx, "</ul>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_OL:
             if (!raw_str(ctx, "</ol>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_LI:
             if (!raw_str(ctx, "</li>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_HR:
@@ -786,13 +780,13 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                         || !raw_str(ctx, "\"></a></h")
                         || !raw_str(ctx, buf)
                         || !raw_str(ctx, ">"))
-                    return ERR_PARSER_LEAVE_BLOCK;
+                    return ERR_LEAVE_BLOCK;
 
                 if (!(ctx->options & OPT_NO_TOC)) {
                     /* TOC */
                     char toc[l];
                     memcpy(toc, s, l);
-                    const char *file = ctx->sa.s + ctx->pages[ctx->cur_page].fileoff;
+                    const char *file = ctx->sa.s + PAGE(ctx, ctx->cur_page).fileoff;
                     ctx->doc.flags |= DOC_BUFFERING;
                     if (!raw_str(ctx, "<li><a href=\"")
                             || !escape_text(ctx, file, strlen(file))
@@ -803,7 +797,7 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                             || !raw_str(ctx, "\">")
                             || !strip_tags(ctx, toc, l, 1)
                             || !raw_str(ctx, "</a>"))
-                        return ERR_PARSER_TOC;
+                        return ERR_TOC;
                     ctx->doc.flags &= ~DOC_BUFFERING;
                 }
             }
@@ -811,10 +805,10 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
         case MD_BLOCK_CODE:
             {
-                struct ctx *ctx = ctx_;
+                struct qmdoc *ctx = ctx_;
 
                 if (!(ctx->code.flags & CODE_BUFFERED)) {
-                    return (raw_str(ctx, "</pre>")) ? 0 : ERR_PARSER_LEAVE_BLOCK;
+                    return (raw_str(ctx, "</pre>")) ? 0 : ERR_LEAVE_BLOCK;
                 } else {
                     BUFFERING_OFF(buf, blen);
 
@@ -830,7 +824,7 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                         codelen = (e) ? e - d->info.text : d->info.size;
 
                         if (!raw_str(ctx, "<div class=\"code\"><pre class=\"lineno\">"))
-                            return ERR_PARSER_BUFFERED;
+                            return ERR_BUFFERED;
                         for (int n = ctx->code.from; n > 0 && s; ++n) {
                             const char *e = memchr(s, '\n', l);
                             if (!e) break;
@@ -839,17 +833,17 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                             char buf[U32_FMT];
                             buf[u32_fmt(buf, (u32) n)] = '\0';
                             if (!raw_str(ctx, buf) || !raw_str(ctx, "\n"))
-                                return ERR_PARSER_BUFFERED;
+                                return ERR_BUFFERED;
                         }
                         if (!raw_str(ctx, "</pre>") || !raw_str(ctx, "<pre>")
                                 || !raw_str(ctx, "<span>")
                                 || !escape_text(ctx, d->info.text, codelen)
                                 || !raw_str(ctx, "</span>")
                                 || !raw_str(ctx, "<code>"))
-                            return ERR_PARSER_BUFFERED;
+                            return ERR_BUFFERED;
                     } else {
                         if (!raw_str(ctx, "<pre>"))
-                            return ERR_PARSER_BUFFERED;
+                            return ERR_BUFFERED;
                     }
 
                     int r;
@@ -857,14 +851,14 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                         r = highlight_escape_text(ctx, buf, blen);
                     else
                         r = escape_text(ctx, buf, blen);
-                    if (!r) return ERR_PARSER_BUFFERED;
+                    if (!r) return ERR_BUFFERED;
 
                     if (ctx->code.flags & CODE_LINES) {
                         if (!raw_str(ctx, "</code></pre></div>"))
-                            return ERR_PARSER_BUFFERED;
+                            return ERR_BUFFERED;
                     } else {
                         if (!raw_str(ctx, "</pre>"))
-                            return ERR_PARSER_BUFFERED;
+                            return ERR_BUFFERED;
                     }
 
                     ctx->code.flags = 0;
@@ -877,7 +871,7 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
         case MD_BLOCK_P:
             if (!raw_str(ctx, "</p>"))
-                return ERR_PARSER_LEAVE_BLOCK;
+                return ERR_LEAVE_BLOCK;
             break;
 
         case MD_BLOCK_TABLE:
@@ -895,17 +889,17 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 static int
 enter_span(MD_SPANTYPE type, void *details, void *ctx_)
 {
-    struct ctx *ctx = ctx_;
+    struct qmdoc *ctx = ctx_;
 
     switch (type) {
         case MD_SPAN_EM:
             if (!raw_str(ctx, "<em>"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
 
         case MD_SPAN_STRONG:
             if (!raw_str(ctx, "<strong>"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
 
         case MD_SPAN_A:
@@ -917,17 +911,17 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
                     ctx->doc.flags |= DOC_BUFFERED_A;
                 } else if (!d->href.size) {
                     warn("Link without target");
-                    return ERR_PARSER_ENTER_SPAN;
+                    return ERR_ENTER_SPAN;
                 } else {
                     if (!raw_str(ctx, "<a href=\"") || !attribute(ctx, &d->href))
-                        return ERR_PARSER_ENTER_SPAN;
+                        return ERR_ENTER_SPAN;
 
                     if (d->title.text
                             && (!raw_str(ctx, "\" title=\"") || !attribute(ctx, &d->title)))
-                        return ERR_PARSER_ENTER_SPAN;
+                        return ERR_ENTER_SPAN;
 
                     if (!raw_str(ctx, "\">"))
-                        return ERR_PARSER_ENTER_SPAN;
+                        return ERR_ENTER_SPAN;
                 }
             }
             break;
@@ -938,17 +932,17 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
 
         case MD_SPAN_CODE:
             if (!raw_str(ctx, "<code>"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
 
         case MD_SPAN_DEL:
             if (!raw_str(ctx, "<s>"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
 
         case MD_SPAN_HIGHLIGHT:
             if (!raw_str(ctx, "<span class=\"highlighted\">"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
 
         case MD_SPAN_WIKILINK:
@@ -964,8 +958,8 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
                  *   link to its anchor
                  */
                 for (int i = 0; i < ctx->nb_pages; ++i) {
-                    if (!memcmp(s, ctx->sa.s + ctx->pages[i].fileoff, l)
-                            && !strcmp(".html", ctx->sa.s + ctx->pages[i].fileoff + l)) {
+                    if (!memcmp(s, ctx->sa.s + PAGE(ctx, i).fileoff, l)
+                            && !strcmp(".html", ctx->sa.s + PAGE(ctx, i).fileoff + l)) {
                         page = i;
                         break;
                     }
@@ -973,14 +967,14 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
 
                 if (!raw_str(ctx, "<a href=\"")
                         || (page >= 0 && (
-                                !raw_str(ctx, ctx->sa.s + ctx->pages[page].fileoff)
+                                !raw_str(ctx, ctx->sa.s + PAGE(ctx, page).fileoff)
                                 ))
                         || (page < 0 && (
                                 !raw_text(ctx, "#", 1)
                                 || !anchor(ctx, s, l)
                                 ))
                         || !raw_str(ctx, "\">"))
-                    return ERR_PARSER_ENTER_SPAN;
+                    return ERR_ENTER_SPAN;
             }
             break;
 
@@ -990,7 +984,7 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
 
         case MD_SPAN_U:
             if (!raw_str(ctx, "<u>"))
-                return ERR_PARSER_ENTER_SPAN;
+                return ERR_ENTER_SPAN;
             break;
     }
 
@@ -1000,17 +994,17 @@ enter_span(MD_SPANTYPE type, void *details, void *ctx_)
 static int
 leave_span(MD_SPANTYPE type, void *details, void *ctx_)
 {
-    struct ctx *ctx = ctx_;
+    struct qmdoc *ctx = ctx_;
 
     switch (type) {
         case MD_SPAN_EM:
             if (!raw_str(ctx, "</em>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_STRONG:
             if (!raw_str(ctx, "</strong>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_A:
@@ -1023,7 +1017,7 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
                     ctx->doc.flags &= ~DOC_BUFFERED_A;
 
                     if (section < 0)
-                        return ERR_PARSER_LEAVE_SPAN;
+                        return ERR_LEAVE_SPAN;
 
                     char buf[U32_FMT + 2];
                     int e;
@@ -1034,7 +1028,7 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
 
                     const char *file;
                     for (int i = 0; i < ctx->nb_pages; ++i) {
-                        file = ctx->sa.s + ctx->pages[i].fileoff;
+                        file = ctx->sa.s + PAGE(ctx, i).fileoff;
                         if (!memcmp(file, s, l)
                                 && file[l] == '.'
                                 && file[l + 1] == section + '0'
@@ -1043,9 +1037,9 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
                         file = NULL;
                     }
 
-                    if (file || ctx->manurl) {
+                    if (file || ctx->omanurl != (size_t) -1) {
                         if (!raw_str(ctx, "<a href=\"")
-                                || (!file && !raw_str(ctx, ctx->manurl))
+                                || (!file && !raw_str(ctx, ctx->sa.s + ctx->omanurl))
                                 || !raw_text(ctx, s, l)
                                 || !raw_text(ctx, ".", 1)
                                 || !raw_text(ctx, buf + 1, 1)
@@ -1056,17 +1050,17 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
                                 || !raw_str(ctx, "</strong>")
                                 || !raw_str(ctx, buf)
                                 || !raw_str(ctx, "</a>"))
-                            return ERR_PARSER_ENTER_SPAN;
+                            return ERR_LEAVE_SPAN;
                     } else {
                         if (!raw_str(ctx, "<strong>")
                                 || !raw_text(ctx, s, l)
                                 || !raw_str(ctx, "</strong>")
                                 || !raw_str(ctx, buf))
-                            return ERR_PARSER_LEAVE_SPAN;
+                            return ERR_LEAVE_SPAN;
                     }
                 } else {
                     if (!raw_str(ctx, "</a>"))
-                        return ERR_PARSER_LEAVE_SPAN;
+                        return ERR_LEAVE_SPAN;
                 }
             }
             break;
@@ -1077,22 +1071,22 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
 
         case MD_SPAN_CODE:
             if (!raw_str(ctx, "</code>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_DEL:
             if (!raw_str(ctx, "</s>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_HIGHLIGHT:
             if (!raw_str(ctx, "</span>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_WIKILINK:
             if (!raw_str(ctx, "</a>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
 
         case MD_SPAN_LATEXMATH:
@@ -1101,7 +1095,7 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
 
         case MD_SPAN_U:
             if (!raw_str(ctx, "</u>"))
-                return ERR_PARSER_LEAVE_SPAN;
+                return ERR_LEAVE_SPAN;
             break;
     }
 
@@ -1111,12 +1105,12 @@ leave_span(MD_SPANTYPE type, void *details, void *ctx_)
 static int
 text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
 {
-    struct ctx *ctx = ctx_;
+    struct qmdoc *ctx = ctx_;
 
     switch (type) {
         case MD_TEXT_NORMAL:
             if (!escape_text(ctx, text, size))
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             break;
 
         case MD_TEXT_NULLCHAR:
@@ -1124,25 +1118,25 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
 
         case MD_TEXT_BR:
             if (!raw_str(ctx, "<br>"))
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             break;
 
         case MD_TEXT_SOFTBR:
             if (!raw_str(ctx, "\n"))
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             break;
 
         case MD_TEXT_ENTITY:
             if (!escape_text(ctx, text, size))
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             break;
 
         case MD_TEXT_CODE:
             if (ctx->code.flags & CODE_BUFFERED) {
                 if (!raw_text(ctx, text, size))
-                    return ERR_PARSER_TEXT;
+                    return ERR_TEXT;
             } else if (!escape_text(ctx, text, size)) {
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             }
             break;
 
@@ -1171,7 +1165,7 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
                 /* buf.otoc is where our current page's toc begins, which we
                  * don't want to include in the full toc */
                 if (!raw_text(ctx, ctx->buf.sa.s, ctx->buf.otoc))
-                    return ERR_PARSER_TOC;
+                    return ERR_TOC;
                 return 0;
             }
 
@@ -1179,12 +1173,12 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
             for (i = 0, n = sizeof(tags) / sizeof(*tags); i < n; ++i) {
                 if (size == tags[i].len && !strncmp(text, tags[i].name, tags[i].len)) {
                     if (!raw_str(ctx, (tags[i].repl) ? tags[i].repl : tags[i].name))
-                        return ERR_PARSER_TEXT;
+                        return ERR_TEXT;
                     break;
                 }
             }
             if (i == n && !escape_text(ctx, text, size))
-                return ERR_PARSER_TEXT;
+                return ERR_TEXT;
             break;
 
         case MD_TEXT_LATEXMATH:
@@ -1195,45 +1189,41 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
 }
 
 static int
-load_source(struct ctx *ctx, size_t *salen)
+load_source(struct qmdoc *ctx, size_t *salen)
 {
-    struct page *p = &ctx->pages[ctx->cur_page];
-
     *salen = ctx->sa.len;
-    if (!stralloc_readyplus(&ctx->sa, p->size + 1))
-        return ERR_MEM;
+    if (!stralloc_readyplus(&ctx->sa, PAGE(ctx, ctx->cur_page).size + 1))
+        return EX_TEMPFAIL;
 
-    if (allread(p->fd, ctx->sa.s + *salen, p->size) != p->size)
-        retwusys(ERR_IO, "read source file");
-    ctx->sa.len += p->size;
+    if (allread(PAGE(ctx, ctx->cur_page).fd, ctx->sa.s + *salen,
+                PAGE(ctx, ctx->cur_page).size) != PAGE(ctx, ctx->cur_page).size)
+        retwusys(EX_NOINPUT, "read source file");
+    ctx->sa.len += PAGE(ctx, ctx->cur_page).size;
 
     /* ending on a new line allows parser optimization */
-    if (ctx->sa.s[ctx->sa.len - 1] != '\n') {
+    if (ctx->sa.s[ctx->sa.len - 1] != '\n')
         stralloc_catb(&ctx->sa, "\n", 1);
-    }
 
     return 0;
 }
 
 static int
-convert_page(struct ctx *ctx, int fddest)
+convert_page(struct qmdoc *ctx, int fddest)
 {
-    struct page *p = &ctx->pages[ctx->cur_page];
-
-    const char *dst = ctx->sa.s + p->fileoff;
+    const char *dst = ctx->sa.s + PAGE(ctx, ctx->cur_page).fileoff;
     int fd;
     if (ctx->options & OPT_OVERWRITE)
         fd = open_truncat(fddest, dst);
     else
         fd = open_exclat(fddest, dst);
-    if (fd < 0) retwusys(ERR_IO, "create destination");
+    if (fd < 0) retwusys(EX_IOERR, "create destination");
 
     size_t salen;
     const char *sce;
 
-    if (p->fd >= 0) {
+    if (PAGE(ctx, ctx->cur_page).fd >= 0) {
         int r = load_source(ctx, &salen);
-        if (r < 0) return r;
+        if (r) return r;
         sce = ctx->sa.s + salen;
     } else {
         salen = ctx->sa.len;
@@ -1255,22 +1245,16 @@ convert_page(struct ctx *ctx, int fddest)
         .leave_span = leave_span,
         .text = text,
     };
-    int r = md_parse(sce, p->size, &parser, ctx);
-    if (r != 0) {
-        char buf[U32_FMT];
-        buf[u32_fmt(buf, (u32) (r < 0) ? -r : r)] = '\0';
-        retw(ERR_PARSER, (r < 0) ? "parser internal error " : "parser error ", buf);
-    }
+    int r = md_parse(sce, PAGE(ctx, ctx->cur_page).size, &parser, ctx);
+    if (r)
+        retw(EX_DATA_ERR, "parser error ", PMINT(r));
 
     if (!(ctx->options & OPT_NO_TOC)) {
         /* close TOC */
         ctx->doc.flags |= DOC_BUFFERING;
         for ( ; ctx->toc_lvl > 0; --ctx->toc_lvl) {
-            if (!raw_str(ctx, "</ul>")) {
-                char buf[U32_FMT];
-                buf[u32_fmt(buf, (u32) ERR_PARSER_TOC)] = '\0';
-                retw(ERR_PARSER, "parser internal error ", buf);
-            }
+            if (!raw_str(ctx, "</ul>"))
+                retw(EX_DATA_ERR, "parser error ", PMINT(ERR_TOC));
         }
         ctx->doc.flags &= ~DOC_BUFFERING;
     }
@@ -1285,7 +1269,7 @@ convert_page(struct ctx *ctx, int fddest)
             || allwrite(fd, ctx->sa_out.s + ctx->otoc, ctx->sa_out.len - ctx->otoc)
             != ctx->sa_out.len - ctx->otoc
        )
-        retwusys(ERR_IO, "write destination");
+        retwusys(EX_IOERR, "write destination");
     fd_close(fd);
 
     /* reset sa_out position */
@@ -1294,7 +1278,7 @@ convert_page(struct ctx *ctx, int fddest)
     if (!(ctx->doc.flags & DOC_FULL_TOC))
         ctx->buf.sa.len = 0;
 
-    fd_close(p->fd);
+    fd_close(PAGE(ctx, ctx->cur_page).fd);
 
     ctx->sa.len = salen;
     return 0;
@@ -1309,29 +1293,33 @@ empty(const char *s)
 }
 
 static int
-load_page_from_file(const char *file, struct page *page, stralloc *sa)
+load_page_from_file(stralloc *sa, size_t fileoff, size_t flen, int pgn, struct qmdoc *ctx)
 {
-    page->fd = open_read(file);
-    if (page->fd < 0) retwusys(ERR_IO, "open '", file, "'");
-
-    page->sce = strrchr(file, '/');
-    if (!page->sce) page->sce = file;
-    else ++page->sce;
-
-    size_t l = strlen(page->sce);
-    page->fileoff = sa->len;
-    if (!stralloc_catb(sa, page->sce, l - 2)
-            || !stralloc_catb(sa, "html", 5))
-        retwusys(EX_TEMPFAIL, "load page title from '", file, "'");
+#define file()      (sa->s + fileoff)
+    PAGE(ctx, pgn).fd = open_read(file());
+    if (PAGE(ctx, pgn).fd < 0) retwusys(EX_NOINPUT, "open ", ESC, file(), ESC);
+
+    PAGE(ctx, pgn).sceoff = byte_rchr(file(), flen, '/');
+    if (PAGE(ctx, pgn).sceoff == flen) PAGE(ctx, pgn).sceoff = 0;
+    else ++PAGE(ctx, pgn).sceoff;
+
+    /* l = strlen(file() + PAGE(ctx, pgn).sceoff) */
+    size_t l = flen - PAGE(ctx, pgn).sceoff;
+    PAGE(ctx, pgn).sceoff += fileoff;
+    PAGE(ctx, pgn).fileoff = sa->len;
+    if (!stralloc_readyplus(sa, l - 2 + 5))
+        retwusys(EX_TEMPFAIL, "load page title from ", ESC, file(), ESC);
+    stralloc_catb(sa, sa->s + PAGE(ctx, pgn).sceoff, l - 2);
+    stralloc_catb(sa, "html", 5);
 
     char buf_[256], buf[sizeof(buf_)];
-    buffer buffer = BUFFER_INIT(&fd_readv, page->fd, buf_, sizeof(buf_));
+    buffer buffer = BUFFER_INIT(&fd_readv, PAGE(ctx, pgn).fd, buf_, sizeof(buf_));
 
     ssize_t left = buffer_get(&buffer, buf, sizeof(buf));
     if (left <= 0)
-        retwu(EX_IOERR, "load page title from '", file, "'");
+        retwu(EX_NOINPUT, "load page title from ", ESC, file(), ESC);
 
-    page->titleoff = page->fileoff;
+    PAGE(ctx, pgn).titleoff = PAGE(ctx, pgn).fileoff;
 
     char *b = buf;
     int line = 1, begin = 1, is_hdr = 0;
@@ -1344,20 +1332,21 @@ load_page_from_file(const char *file, struct page *page, stralloc *sa)
         size_t *offset = NULL;
         switch (line) {
             case 1:
-                offset = &page->titleoff;
+                offset = &PAGE(ctx, pgn).titleoff;
                 break;
             case 2:
-                offset = &page->nameoff;
+                offset = &PAGE(ctx, pgn).nameoff;
                 break;
             case 3:
-                offset = &page->veroff;
+                offset = &PAGE(ctx, pgn).veroff;
                 break;
             case 4:
-                offset = &page->dateoff;
+                offset = &PAGE(ctx, pgn).dateoff;
                 break;
             default:
                 if (begin && is_hdr && !warned) {
-                    err("warning: header too long in '", file, "': Only 4 lines supported");
+                    err("warning: header too long in ", ESC, file(), ESC, ": ",
+                        "Only 4 lines supported");
                     warned = 1;
                 }
         }
@@ -1367,7 +1356,7 @@ load_page_from_file(const char *file, struct page *page, stralloc *sa)
                     && (!stralloc_catb(sa, b + ((is_hdr) ? 2 : 0),
                                        ((e) ? e - b : left) - ((is_hdr) ? 2 : 0))
                         || (e && !stralloc_0(sa))))
-                retwusys(EX_TEMPFAIL, "load page title from '", file, "'");
+                retwusys(EX_TEMPFAIL, "load page title from ", ESC, file(), ESC);
         }
         if (e) {
             int l = e - b + 1;
@@ -1383,204 +1372,250 @@ load_page_from_file(const char *file, struct page *page, stralloc *sa)
             b = buf;
             left = buffer_get(&buffer, buf, sizeof(buf));
             if (left <= 0)
-                retwusys(EX_IOERR, "load page title from '", file, "'");
+                retwusys(EX_DATA_ERR, "load page title from ", ESC, file(), ESC);
             if (!e) begin = 0;
         }
     }
 
-    if (empty(sa->s + page->titleoff))
-        page->titleoff = page->fileoff;
+    if (empty(sa->s + PAGE(ctx, pgn).titleoff))
+        PAGE(ctx, pgn).titleoff = PAGE(ctx, pgn).fileoff;
 
-    if (page->nameoff && empty(sa->s + page->nameoff))
-        page->nameoff = 0;
+    if (PAGE(ctx, pgn).nameoff && empty(sa->s + PAGE(ctx, pgn).nameoff))
+        PAGE(ctx, pgn).nameoff = 0;
 
-    if (page->nameoff) {
-        if (!page->veroff || empty(sa->s + page->veroff))
-            page->veroff = page->nameoff;
-        if (!page->dateoff || empty(sa->s + page->dateoff))
-            page->dateoff = page->titleoff;
+    if (PAGE(ctx, pgn).nameoff) {
+        if (!PAGE(ctx, pgn).veroff || empty(sa->s + PAGE(ctx, pgn).veroff))
+            PAGE(ctx, pgn).veroff = PAGE(ctx, pgn).nameoff;
+        if (!PAGE(ctx, pgn).dateoff || empty(sa->s + PAGE(ctx, pgn).dateoff))
+            PAGE(ctx, pgn).dateoff = PAGE(ctx, pgn).titleoff;
     }
 
-    page->size = lseek(page->fd, 0, SEEK_END);
-    if (page->size == (off_t) -1 || lseek(page->fd, done, SEEK_SET) < 0)
-        retwusys(EX_IOERR, "seek into '", file, "'");
-    page->size -= done;
+    PAGE(ctx, pgn).size = lseek(PAGE(ctx, pgn).fd, 0, SEEK_END);
+    if (PAGE(ctx, pgn).size == (off_t) -1 || lseek(PAGE(ctx, pgn).fd, done, SEEK_SET) < 0)
+        retwusys(EX_IOERR, "seek into ", ESC, file(), ESC);
+    PAGE(ctx, pgn).size -= done;
     return 0;
+#undef file
 }
 
-static void
-help(void)
-{
-    diehelp(0, "[OPTION..] FILE...",
-            " -a, --author AUTHOR           Set AUTHOR as author (in meta in footer)\n"
-            " -b, --buttons                 Put Previous & Next buttons on pages\n"
-            " -C, --no-css                  Do not use CSS (still process --css if any)\n"
-            " -c, --css FILE                Add FILE as additional CSS\n"
-            " -d, --destdir DIR             Write files into DIR\n"
-            " -F, --footer FILE             Insert FILE as common footer\n"
-            " -H, --header FILE             Insert FILE as common header\n"
-            " -h, --help                    Show this help screen and exit\n"
-            " -I, --inline-css              Use inline CSS instead of external files\n"
-            " -i, --index                   Force index mode\n"
-            " -l, --lang LNG                Set LNG as language attribute\n"
-            " -M, --man-url URL             Use URL as prefix for external man-page links\n"
-            " -o, --overwrite               Overwrite destination files if already exist\n"
-            " -s, --subtitle TEXT           Set TEXT as general subtitle\n"
-            " -T, --no-toc                  Don't write a TOC on each page. Implies --no-index\n"
-            " -t, --title TITLE             Set TITLE as general (across all pages) title\n"
-            " -V, --version                 Show version screen and exit\n"
-            " -W, --wide-include            Include header/footer right within <body>\n"
-            " -X, --no-index                Disable index mode\n"
-           );
-}
+struct parse {
+    struct qmdoc *qmdoc;
+    size_t destdir;
+    size_t footer;
+    size_t header;
+    size_t ffile;
+};
 
-static void
-usage(int err)
-{
-    dieusage((err) ? -ERR_USAGE : -ERR_NONE, "[OPTION..] FILE...");
-}
+enum {
+    OPTID_VERSION = OPTID_FIRST,
+    ARGID_FILE
+};
 
-int
-main (int argc, char *argv[])
+static int
+parse_cmdline(int argc, const char *argv[], const char usage[], struct parse *ctx)
 {
-    PROG = strrchr(argv[0], '/');
-    if (PROG) ++PROG;
-    else PROG = argv[0];
-
-    char *destdir = ".";
-    const char *footer = NULL;
-    const char *header = NULL;
-    struct ctx ctx = {
-        .options = 0,
-        .sa = STRALLOC_ZERO,
-        .sa_out = STRALLOC_ZERO,
-        .css = css,
-        .doc.title = "Documentation",
-        .doc.subtitle = NULL,
-        .doc.author = "",
-        .doc.lang = "en",
-        .buf.sa = STRALLOC_ZERO,
+    const struct option options[] = {
+        OPTION_ARG_REQ( 'a', "author",                  OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE('b', "buttons",                 0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('C', "no-css",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'c', "css",                     OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'd', "destdir",                 OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'F', "footer",                  OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'H', "header",                  OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE('h', "help",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('I', "inline-css",              0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('i', "index",                   0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'l', "lang",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 'M', "man-url",                 OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE('o', "overwrite",               0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 's', "subtitle",                OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE('T', "no-toc",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ( 't', "title",                   OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE( 0 , "version",                 0,          OPTID_VERSION),
+        OPTION_ARG_NONE('W', "wide-includes",           0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('X', "no-index",                0,          OPTID_SHORTOPT),
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "file",                           OPT_PATH | OPT_RPT,   ARGID_FILE),
+        LOADOPT_DONE
     };
+    struct loadopt lo = LOADOPT_ZERO;
+    int nfile = 0;
 
     int c;
-    struct option opts[] = {
-        { "author",         required_argument,  NULL,   'a' },
-        { "buttons",        no_argument,        NULL,   'b' },
-        { "no-css",         no_argument,        NULL,   'C' },
-        { "css",            required_argument,  NULL,   'c' },
-        { "destdir",        required_argument,  NULL,   'd' },
-        { "footer",         required_argument,  NULL,   'F' },
-        { "header",         required_argument,  NULL,   'H' },
-        { "help",           no_argument,        NULL,   'h' },
-        { "inline-css",     no_argument,        NULL,   'I' },
-        { "index",          no_argument,        NULL,   'i' },
-        { "lang",           no_argument,        NULL,   'l' },
-        { "man-url",        required_argument,  NULL,   'M' },
-        { "overwrite",      no_argument,        NULL,   'o' },
-        { "subtitle",       required_argument,  NULL,   's' },
-        { "no-toc",         no_argument,        NULL,   'T' },
-        { "title",          required_argument,  NULL,   't' },
-        { "version",        no_argument,        NULL,   'V' },
-        { "wide-includes",  no_argument,        NULL,   'W' },
-        { "no-index",       no_argument,        NULL,   'X' },
-        { NULL,             0,                  NULL,    0  },
-    };
-    while ((c = getopt_long(argc, argv, "a:bCc:d:F:H:hIil:M:os:Tt:VWX", opts, NULL)) != -1) switch (c) {
+    while ((c = loadopt(&ctx->qmdoc->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'a':
-            ctx.doc.author = optarg;
+            ctx->qmdoc->doc.oauthor = LO_OFF(&lo);
             break;
         case 'b':
-            ctx.options |= OPT_BUTTONS;
+            ctx->qmdoc->options |= OPT_BUTTONS;
             break;
         case 'C':
-            ctx.options |= OPT_NO_CSS;
+            ctx->qmdoc->options |= OPT_NO_CSS;
             break;
         case 'c':
-            ctx.css[CSS_CUSTOM].file = optarg;
+            ctx->qmdoc->css[CSS_CUSTOM] = LO_OFF(&lo);
             break;
         case 'd':
-            destdir = optarg;
+            ctx->destdir = LO_OFF(&lo);
             break;
         case 'F':
-            footer = optarg;
+            ctx->footer = LO_OFF(&lo);
             break;
         case 'H':
-            header = optarg;
+            ctx->header = LO_OFF(&lo);
             break;
         case 'h':
-            help();
+            diehelp(0, usage,
+" -a, --author AUTHOR               Set AUTHOR as author (in meta in footer)\n"
+" -b, --buttons                     Put Previous & Next buttons on pages\n"
+" -C, --no-css                      Do not use CSS (still process --css if any)\n"
+" -c, --css FILE                    Add FILE as additional CSS\n"
+" -d, --destdir DIR                 Write files into DIR\n"
+" -F, --footer FILE                 Insert FILE as common footer\n"
+" -H, --header FILE                 Insert FILE as common header\n"
+" -h, --help                        Show this help screen and exit\n"
+" -I, --inline-css                  Use inline CSS instead of external files\n"
+" -i, --index                       Force index mode\n"
+" -l, --lang LNG                    Set LNG as language attribute\n"
+" -M, --man-url URL                 Use URL as prefix for external man-page links\n"
+" -o, --overwrite                   Overwrite destination files if already exist\n"
+" -s, --subtitle TEXT               Set TEXT as general subtitle\n"
+" -T, --no-toc                      Don't write a TOC on each page. Implies --no-index\n"
+" -t, --title TITLE                 Set TITLE as general (across all pages) title\n"
+" -V, --version                     Show version screen and exit\n"
+" -W, --wide-include                Include header/footer right within <body>\n"
+" -X, --no-index                    Disable index mode\n"
+);
         case 'I':
-            ctx.options |= OPT_INLINE_CSS;
+            ctx->qmdoc->options |= OPT_INLINE_CSS;
             break;
         case 'i':
-            ctx.options |= OPT_INDEX;
+            ctx->qmdoc->options |= OPT_INDEX;
             break;
         case 'l':
-            ctx.doc.lang = optarg;
+            ctx->qmdoc->doc.lang = LO_ARG(&lo);
             break;
         case 'M':
-            ctx.manurl = optarg;
+            ctx->qmdoc->omanurl = LO_OFF(&lo);
             break;
         case 'o':
-            ctx.options |= OPT_OVERWRITE;
+            ctx->qmdoc->options |= OPT_OVERWRITE;
             break;
         case 's':
-            ctx.doc.subtitle = optarg;
+            ctx->qmdoc->doc.osubtitle = LO_OFF(&lo);
             break;
         case 'T':
-            ctx.options |= OPT_NO_TOC | OPT_NO_INDEX;
+            ctx->qmdoc->options |= OPT_NO_TOC | OPT_NO_INDEX;
             break;
         case 't':
-            ctx.doc.title = optarg;
-            break;
-        case 'V':
-            dieversion(QMDOC_VERSION, "2023", QMDOC_CURYEAR, QMDOC_AUTHOR, QMDOC_URL, NULL);
+            ctx->qmdoc->doc.otitle = LO_OFF(&lo);
             break;
         case 'W':
-            ctx.options |= OPT_WIDE_INC;
+            ctx->qmdoc->options |= OPT_WIDE_INC;
             break;
         case 'X':
-            ctx.options |= OPT_NO_INDEX;
+            ctx->qmdoc->options |= OPT_NO_INDEX;
             break;
-        case '?':
-            usage(1);
+        case OPTID_VERSION:
+            dieversion(QMDOC_VERSION, "2023", QMDOC_CURYEAR, QMDOC_AUTHOR, QMDOC_URL, NULL);
+            break;
+
+        case ARGID_FILE:
+            if (nfile++ == 0)
+                ctx->ffile = LO_OFF(&lo);
+            break;
+
+        case -1:
+            dieusage(EX_USAGE, usage);
+        default:
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
+    }
+
+    return nfile;
+}
+
+int
+main (int argc, const char *argv[])
+{
+    PROG = strrchr(argv[0], '/');
+    if (PROG) ++PROG;
+    else PROG = argv[0];
+
+    struct qmdoc ctx = {
+        .options = 0,
+        .sa = STRALLOC_ZERO,
+        .sa_out = STRALLOC_ZERO,
+        .css[CSS_CUSTOM] = (size_t) -1,
+        .doc.otitle = (size_t) -1,
+        .doc.oauthor = (size_t) -1,
+        .doc.osubtitle = (size_t) -1,
+        .doc.lang = "en",
+        .omanurl = (size_t) -1,
+        .buf.sa = STRALLOC_ZERO,
+    };
+    struct parse parse = { .qmdoc = &ctx, .destdir = (size_t) -1,
+        .header = (size_t) -1, .footer = (size_t) -1, .ffile = (size_t) -1 };
+    const char usage[] = "[OPTION..] FILE..";
+
+    ctx.nb_pages = parse_cmdline(argc, argv, usage, &parse);
+    size_t lfile = ctx.sa.len;
+
+    if (ctx.doc.otitle == (size_t) -1) {
+        ctx.doc.otitle = ctx.sa.len;
+        if (!stralloc_cats0(&ctx.sa, "Documentation"))
+            diefusys(EX_TEMPFAIL, "initialize");
+    }
+
+    if (parse.destdir == (size_t) -1) {
+        parse.destdir = ctx.sa.len;
+        if (!stralloc_cats0(&ctx.sa, "."))
+            diefusys(EX_TEMPFAIL, "initialize");
     }
-    if (optind == argc) usage(1);
+
     if ((ctx.options & (OPT_NO_CSS | OPT_INLINE_CSS)) == (OPT_NO_CSS | OPT_INLINE_CSS))
-        dief(EX_USAGE, "cannot use '", "--no-css", "' and '", "--inline-css", "' together");
+        dief(EX_USAGE, "cannot use ", "--no-css", " and ", "--inline-css", " together");
     if ((ctx.options & (OPT_INDEX | OPT_NO_INDEX)) == (OPT_INDEX | OPT_NO_INDEX))
-        dief(EX_USAGE, "cannot use '", "--index", "' and '", "--no-index", "' together");
+        dief(EX_USAGE, "cannot use ", "--index", " and ", "--no-index", " together");
+
+    if (!(ctx.options & OPT_NO_TOC))
+        ctx.css[CSS_NO_TOC] = (size_t) -1;
 
-    int fddest = open(destdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
-    if (fddest < 0) diefusys(EX_IOERR, "open '", destdir, "'");
+    int fddest = open(ctx.sa.s + parse.destdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+    if (fddest < 0) diefusys(EX_IOERR, "open ", ESC, ctx.sa.s + parse.destdir, ESC);
 
-    if (!(ctx.options & OPT_NO_TOC)) css[CSS_NO_TOC].file = NULL;
+    {
+        /* +1 in case we'll need to add our internal index */
+        size_t len = (ctx.nb_pages + 1) * sizeof(struct page);
 
-    /* +1 in case we'll need to add our internal index */
-    struct page pages[argc - optind + 1];
-    ctx.pages = pages;
-    ctx.nb_pages = sizeof(pages) / sizeof(*pages) - 1;
-    memset(pages, 0, ctx.nb_pages * sizeof(*pages));
+        ctx.opages = ctx.sa.len;
+        if (!stralloc_readyplus(&ctx.sa, len))
+            diefusys(EX_TEMPFAIL, "initialize");
+        ctx.sa.len += len;
+        memset(ctx.sa.s + ctx.opages, 0, len);
+    }
 
     int err = 0;
     int idx_page = -1;
     out("Scanning pages...");
-    for (int i = optind; i < argc; ++i) {
-        const char *file = argv[i];
-        size_t len = strlen(file);
-
-        if (strcmp(file + len - 3, ".md")) {
-            warn("File '", file, "' not a markdown file (*.md)");
-            pages[i - optind].fd = -1;
+    size_t off = ctx.sa.len;
+    ctx.nb_pages = sacoloff(&ctx.sa, parse.ffile, lfile);
+    for (int i = 0; i < ctx.nb_pages; ++i) {
+        size_t *fileoff = (size_t *) (ctx.sa.s + off);
+        const char *file = ctx.sa.s + fileoff[i];
+        size_t flen = strlen(file);
+
+        if (strcmp(file + flen - 3, ".md")) {
+            warn("File ", ESC, file, ESC, " not a markdown file (*.md)");
+            PAGE(&ctx, i).fd = -1;
             err = EX_DATA_ERR;
             continue;
         }
 
-        int r = load_page_from_file(file, &pages[i - optind], &ctx.sa);
+        int r = load_page_from_file(&ctx.sa, fileoff[i], flen, i, &ctx);
         if (r) err = r;
 
         if (!(ctx.options & OPT_NO_INDEX)
-                && !strcmp(ctx.sa.s + pages[i - optind].fileoff, "index.html"))
+                && !strcmp(ctx.sa.s + PAGE(&ctx, i).fileoff, "index.html"))
             idx_page = i - optind;
     }
 
@@ -1595,105 +1630,142 @@ main (int argc, char *argv[])
 
         if (idx_page < 0) { /* no index, add our internal page first */
             /* move all pages up by one */
-            memmove(&pages[1], &pages[0], ctx.nb_pages * sizeof(*pages));
+            memmove(&PAGE(&ctx, 1), &PAGE(&ctx, 0), ctx.nb_pages * sizeof(struct page));
 
             /* add our internal index */
-            pages[0].sce = "<internal index>";
-            pages[0].fileoff = ctx.sa.len;
+            PAGE(&ctx, 0).sceoff = ctx.sa.len;
+            if (!stralloc_cats0(&ctx.sa, "<internal index>"))
+                diefusys(EX_TEMPFAIL, "set internal page title");
+
+            PAGE(&ctx, 0).fileoff = ctx.sa.len;
             if (!stralloc_cats0(&ctx.sa, "index.html"))
-                diefusys(EX_TEMPFAIL, "load page title from '", pages[0].sce, "'");
+                diefusys(EX_TEMPFAIL, "load page title from ",
+                         ESC, ctx.sa.s + PAGE(&ctx, 0).sceoff, ESC);
 
-            pages[0].titleoff = ctx.sa.len;
+            PAGE(&ctx, 0).titleoff = ctx.sa.len;
             if (!stralloc_cats0(&ctx.sa, index_title))
-                diefusys(EX_TEMPFAIL, "load page title from '", pages[0].sce, "'");
+                diefusys(EX_TEMPFAIL, "load page title from ",
+                         ESC, ctx.sa.s + PAGE(&ctx, 0).sceoff, ESC);
 
             /* fd == -1 means use index_md instead of reading from fd */
-            pages[0].fd = -1;
-            pages[0].size = strlen(index_md);
-            pages[0].nameoff = pages[0].veroff = pages[0].dateoff = 0;
+            PAGE(&ctx, 0).fd = -1;
+            PAGE(&ctx, 0).size = strlen(index_md);
+            PAGE(&ctx, 0).nameoff = PAGE(&ctx, 0).veroff = PAGE(&ctx, 0).dateoff = 0;
 
             ++ctx.nb_pages;
         } else if (idx_page > 0) { /* move index's page to first */
-            /* move index's page into last slot */
-            memmove(&pages[ctx.nb_pages], &pages[idx_page], sizeof(*pages));
+            struct page pg;
+            /* "extract" the index page */
+            pg = PAGE(&ctx, idx_page);
             /* move everything before up by one */
-            memmove(&pages[1], &pages[0], idx_page * sizeof(*pages));
-            /* put index back in first */
-            memmove(&pages[0], &pages[ctx.nb_pages], sizeof(*pages));
+            memmove(&PAGE(&ctx, 1), &PAGE(&ctx, 0), idx_page * sizeof(struct page));
+            /* put index page first */
+            PAGE(&ctx, 0) = pg;
         }
     }
 
-    if (header || footer) {
+    if (parse.header != (size_t) -1 || parse.footer != (size_t) -1) {
         out("Loading files...");
 
-        if (header) {
+        if (parse.header != (size_t) -1) {
             ctx.doc.oheader = ctx.sa.len;
-            if (!openreadfileclose(header, &ctx.sa, 0) || !stralloc_0(&ctx.sa))
-                diefusys(EX_IOERR, "load data from '", header, "'");
+            if (!open_slurp_close(&ctx.sa, ctx.sa.s + parse.header) || !stralloc_0(&ctx.sa))
+                diefusys((errno == ENOMEM) ? EX_TEMPFAIL : EX_NOINPUT,
+                         "load data from '", ctx.sa.s + parse.header, "'");
         }
-        if (footer) {
+        if (parse.footer != (size_t) -1) {
             ctx.doc.ofooter = ctx.sa.len;
-            if (!openreadfileclose(footer, &ctx.sa, 0) || !stralloc_0(&ctx.sa))
-                diefusys(EX_IOERR, "load data from '", footer, "'");
+            if (!open_slurp_close(&ctx.sa, ctx.sa.s + parse.footer) || !stralloc_0(&ctx.sa))
+                diefusys((errno == ENOMEM) ? EX_TEMPFAIL : EX_NOINPUT,
+                         "load data from '", ctx.sa.s + parse.footer, "'");
         }
     }
 
-    if (!(ctx.options & OPT_NO_CSS) || ctx.css[CSS_CUSTOM].file) {
+    if (!(ctx.options & OPT_NO_CSS) || ctx.css[CSS_CUSTOM] != (size_t) -1) {
         out((ctx.options & OPT_INLINE_CSS) ? "Loading" : "Copying", " CSS files...");
+        int dirfd;
+        if (!(ctx.options & OPT_NO_CSS)) {
+            dirfd = open2(QMDOC_SHAREDIR, O_RDONLY | O_DIRECTORY);
+            if (dirfd < 0)
+                diefusys(EX_IOERR, "open ", ESC, QMDOC_SHAREDIR, ESC);
+        } else {
+            dirfd = AT_FDCWD;
+        }
+
         for (int i = 0; i < NB_CSS; ++i) {
-            if (!css[i].file) continue;
-            size_t flen = strlen(css[i].file);
-            char file[strlen(QMDOC_SHAREDIR) + 1 + flen + 1];
-            if (i != CSS_CUSTOM) {
-                memcpy(file, QMDOC_SHAREDIR, strlen(QMDOC_SHAREDIR));
-                file[strlen(QMDOC_SHAREDIR)] = '/';
-                memcpy(file + strlen(QMDOC_SHAREDIR) + 1, css[i].file, flen + 1);
-            } else {
-                memcpy(file, css[i].file, flen + 1);
+            if ((ctx.options & OPT_NO_CSS) || ctx.css[i] == (size_t) -1)
+                continue;
+
+            const char *file;
+            size_t off = ctx.css[CSS_CUSTOM];
+            switch (i) {
+                case CSS_QMDOC:
+                    file = "qmdoc.css";
+                    break;
+                case CSS_CUSTOM:
+                    file = ctx.sa.s + off;
+                    break;
+                case CSS_NO_TOC:
+                    file = "no-toc.css";
+                    break;
             }
+
+            int from;
+            from = open_readat((i == CSS_CUSTOM) ? AT_FDCWD : dirfd, file);
+            if (from < 0)
+                diefusys(EX_IOERR, "open ", ESC, file, ESC);
+
             if (ctx.options & OPT_INLINE_CSS) {
-                css[i].offset = ctx.sa.len;
-                if (!openreadfileclose(file, &ctx.sa, 0)
-                        || !stralloc_0(&ctx.sa))
-                    diefusys((errno == ENOMEM) ? EX_TEMPFAIL : EX_IOERR,
-                             "load CSS from '", file, "'");
+                ctx.css[i] = ctx.sa.len;
+                if (!slurp(&ctx.sa, from) || !stralloc_0(&ctx.sa))
+                    diefusys((errno == ENOMEM) ? EX_TEMPFAIL : EX_NOINPUT,
+                             "load CSS from ", ESC,
+                             (i == CSS_CUSTOM) ? ctx.sa.s + off : file,
+                             ESC);
             } else {
-                int from, to;
-                from = open_read(file);
-                if (from < 0) diefusys(-ERR_IO, "open '", file, "'");
+                int to;
                 if (ctx.options & OPT_OVERWRITE)
-                    to = open_truncat(fddest, css[i].file);
+                    to = open_truncat(fddest, file);
                 else
-                    to = open_exclat(fddest, css[i].file);
-                if (to < 0) diefusys(-ERR_IO, "create '", destdir, "/", css[i].file, "'");
-                if (fd_cat(from, to) < 0)
-                    diefusys(EX_IOERR, "copy CSS to '", destdir, "/", css[i].file, "'");
-                fd_close(from);
+                    to = open_exclat(fddest, file);
+                if (to < 0 || fd_cat(from, to) < 0)
+                    diefusys(EX_IOERR, "copy CSS to ",
+                             ESC, ctx.sa.s + parse.destdir, "/", file, ESC);
                 fd_close(to);
+
+                if (i != CSS_CUSTOM) {
+                    ctx.css[i] = ctx.sa.len;
+                    if (!stralloc_cats0(&ctx.sa, file))
+                        diefusys(EX_TEMPFAIL, "copy CSS to ",
+                                 ESC, ctx.sa.s + parse.destdir, "/", file, ESC);
+                }
             }
+            fd_close(from);
         }
+        if (dirfd != AT_FDCWD) fd_close(dirfd);
     }
 
-    for (ctx.cur_page = 0; ctx.cur_page < ctx.nb_pages; ++ctx.cur_page) {
-        if ((ctx.doc.flags & DOC_FULL_TOC) && ctx.cur_page == 0)
-            continue;
+    ctx.cur_page = 0;
+    for (;;) {
+        if ((ctx.doc.flags & (DOC_FULL_TOC | DOC_IS_INDEX)) == DOC_FULL_TOC
+                && ctx.cur_page == 0)
+            ++ctx.cur_page;
 
-        out("Converting ", pages[ctx.cur_page].sce, "...");
+        out("Converting ", ESC, ctx.sa.s + PAGE(&ctx, ctx.cur_page).sceoff, ESC, "...");
 
         int r = convert_page(&ctx, fddest);
-        if (r < 0) diefu(-r, "convert '", pages[ctx.cur_page].sce, "' to '",
-                         destdir, "/", ctx.sa.s + pages[ctx.cur_page].fileoff, "'");
-    }
+        if (r)
+            diefu(r, "convert ", ESC, ctx.sa.s + PAGE(&ctx, ctx.cur_page).sceoff, ESC,
+                  " to ", ESC, ctx.sa.s + parse.destdir, "/",
+                  ctx.sa.s + PAGE(&ctx, ctx.cur_page).fileoff, ESC);
 
-    if (ctx.doc.flags & DOC_FULL_TOC) {
-        ctx.cur_page = 0;
-
-        out("Converting ", pages[ctx.cur_page].sce, "...");
-
-        ctx.doc.flags |= DOC_IS_INDEX;
-        int r = convert_page(&ctx, fddest);
-        if (r < 0) diefu(-r, "convert '", pages[ctx.cur_page].sce, "' to '",
-                         destdir, "/", ctx.sa.s + pages[ctx.cur_page].fileoff, "'");
+        if (++ctx.cur_page == ctx.nb_pages) {
+            if (!(ctx.doc.flags & DOC_FULL_TOC))
+                break;
+            ctx.cur_page = 0;
+            ctx.doc.flags |= DOC_IS_INDEX;
+        } else if (ctx.doc.flags & DOC_IS_INDEX)
+            break;
     }
 
     stralloc_free(&ctx.buf.sa);